##// END OF EJS Templates
filemerge: be more strict when detecting conflict markers, add `|` markers...
Kyle Lippincott -
r48753:053dd53a stable
parent child Browse files
Show More
@@ -1,1320 +1,1324 b''
1 # filemerge.py - file-level merge handling for Mercurial
1 # filemerge.py - file-level merge handling for Mercurial
2 #
2 #
3 # Copyright 2006, 2007, 2008 Olivia Mackall <olivia@selenic.com>
3 # Copyright 2006, 2007, 2008 Olivia Mackall <olivia@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import contextlib
10 import contextlib
11 import os
11 import os
12 import re
12 import re
13 import shutil
13 import shutil
14
14
15 from .i18n import _
15 from .i18n import _
16 from .node import (
16 from .node import (
17 hex,
17 hex,
18 short,
18 short,
19 )
19 )
20 from .pycompat import (
20 from .pycompat import (
21 getattr,
21 getattr,
22 open,
22 open,
23 )
23 )
24
24
25 from . import (
25 from . import (
26 encoding,
26 encoding,
27 error,
27 error,
28 formatter,
28 formatter,
29 match,
29 match,
30 pycompat,
30 pycompat,
31 registrar,
31 registrar,
32 scmutil,
32 scmutil,
33 simplemerge,
33 simplemerge,
34 tagmerge,
34 tagmerge,
35 templatekw,
35 templatekw,
36 templater,
36 templater,
37 templateutil,
37 templateutil,
38 util,
38 util,
39 )
39 )
40
40
41 from .utils import (
41 from .utils import (
42 procutil,
42 procutil,
43 stringutil,
43 stringutil,
44 )
44 )
45
45
46
46
47 def _toolstr(ui, tool, part, *args):
47 def _toolstr(ui, tool, part, *args):
48 return ui.config(b"merge-tools", tool + b"." + part, *args)
48 return ui.config(b"merge-tools", tool + b"." + part, *args)
49
49
50
50
51 def _toolbool(ui, tool, part, *args):
51 def _toolbool(ui, tool, part, *args):
52 return ui.configbool(b"merge-tools", tool + b"." + part, *args)
52 return ui.configbool(b"merge-tools", tool + b"." + part, *args)
53
53
54
54
55 def _toollist(ui, tool, part):
55 def _toollist(ui, tool, part):
56 return ui.configlist(b"merge-tools", tool + b"." + part)
56 return ui.configlist(b"merge-tools", tool + b"." + part)
57
57
58
58
59 internals = {}
59 internals = {}
60 # Merge tools to document.
60 # Merge tools to document.
61 internalsdoc = {}
61 internalsdoc = {}
62
62
63 internaltool = registrar.internalmerge()
63 internaltool = registrar.internalmerge()
64
64
65 # internal tool merge types
65 # internal tool merge types
66 nomerge = internaltool.nomerge
66 nomerge = internaltool.nomerge
67 mergeonly = internaltool.mergeonly # just the full merge, no premerge
67 mergeonly = internaltool.mergeonly # just the full merge, no premerge
68 fullmerge = internaltool.fullmerge # both premerge and merge
68 fullmerge = internaltool.fullmerge # both premerge and merge
69
69
70 # IMPORTANT: keep the last line of this prompt very short ("What do you want to
70 # IMPORTANT: keep the last line of this prompt very short ("What do you want to
71 # do?") because of issue6158, ideally to <40 English characters (to allow other
71 # do?") because of issue6158, ideally to <40 English characters (to allow other
72 # languages that may take more columns to still have a chance to fit in an
72 # languages that may take more columns to still have a chance to fit in an
73 # 80-column screen).
73 # 80-column screen).
74 _localchangedotherdeletedmsg = _(
74 _localchangedotherdeletedmsg = _(
75 b"file '%(fd)s' was deleted in other%(o)s but was modified in local%(l)s.\n"
75 b"file '%(fd)s' was deleted in other%(o)s but was modified in local%(l)s.\n"
76 b"You can use (c)hanged version, (d)elete, or leave (u)nresolved.\n"
76 b"You can use (c)hanged version, (d)elete, or leave (u)nresolved.\n"
77 b"What do you want to do?"
77 b"What do you want to do?"
78 b"$$ &Changed $$ &Delete $$ &Unresolved"
78 b"$$ &Changed $$ &Delete $$ &Unresolved"
79 )
79 )
80
80
81 _otherchangedlocaldeletedmsg = _(
81 _otherchangedlocaldeletedmsg = _(
82 b"file '%(fd)s' was deleted in local%(l)s but was modified in other%(o)s.\n"
82 b"file '%(fd)s' was deleted in local%(l)s but was modified in other%(o)s.\n"
83 b"You can use (c)hanged version, leave (d)eleted, or leave (u)nresolved.\n"
83 b"You can use (c)hanged version, leave (d)eleted, or leave (u)nresolved.\n"
84 b"What do you want to do?"
84 b"What do you want to do?"
85 b"$$ &Changed $$ &Deleted $$ &Unresolved"
85 b"$$ &Changed $$ &Deleted $$ &Unresolved"
86 )
86 )
87
87
88
88
89 class absentfilectx(object):
89 class absentfilectx(object):
90 """Represents a file that's ostensibly in a context but is actually not
90 """Represents a file that's ostensibly in a context but is actually not
91 present in it.
91 present in it.
92
92
93 This is here because it's very specific to the filemerge code for now --
93 This is here because it's very specific to the filemerge code for now --
94 other code is likely going to break with the values this returns."""
94 other code is likely going to break with the values this returns."""
95
95
96 def __init__(self, ctx, f):
96 def __init__(self, ctx, f):
97 self._ctx = ctx
97 self._ctx = ctx
98 self._f = f
98 self._f = f
99
99
100 def __bytes__(self):
100 def __bytes__(self):
101 return b'absent file %s@%s' % (self._f, self._ctx)
101 return b'absent file %s@%s' % (self._f, self._ctx)
102
102
103 def path(self):
103 def path(self):
104 return self._f
104 return self._f
105
105
106 def size(self):
106 def size(self):
107 return None
107 return None
108
108
109 def data(self):
109 def data(self):
110 return None
110 return None
111
111
112 def filenode(self):
112 def filenode(self):
113 return self._ctx.repo().nullid
113 return self._ctx.repo().nullid
114
114
115 _customcmp = True
115 _customcmp = True
116
116
117 def cmp(self, fctx):
117 def cmp(self, fctx):
118 """compare with other file context
118 """compare with other file context
119
119
120 returns True if different from fctx.
120 returns True if different from fctx.
121 """
121 """
122 return not (
122 return not (
123 fctx.isabsent()
123 fctx.isabsent()
124 and fctx.changectx() == self.changectx()
124 and fctx.changectx() == self.changectx()
125 and fctx.path() == self.path()
125 and fctx.path() == self.path()
126 )
126 )
127
127
128 def flags(self):
128 def flags(self):
129 return b''
129 return b''
130
130
131 def changectx(self):
131 def changectx(self):
132 return self._ctx
132 return self._ctx
133
133
134 def isbinary(self):
134 def isbinary(self):
135 return False
135 return False
136
136
137 def isabsent(self):
137 def isabsent(self):
138 return True
138 return True
139
139
140
140
141 def _findtool(ui, tool):
141 def _findtool(ui, tool):
142 if tool in internals:
142 if tool in internals:
143 return tool
143 return tool
144 cmd = _toolstr(ui, tool, b"executable", tool)
144 cmd = _toolstr(ui, tool, b"executable", tool)
145 if cmd.startswith(b'python:'):
145 if cmd.startswith(b'python:'):
146 return cmd
146 return cmd
147 return findexternaltool(ui, tool)
147 return findexternaltool(ui, tool)
148
148
149
149
150 def _quotetoolpath(cmd):
150 def _quotetoolpath(cmd):
151 if cmd.startswith(b'python:'):
151 if cmd.startswith(b'python:'):
152 return cmd
152 return cmd
153 return procutil.shellquote(cmd)
153 return procutil.shellquote(cmd)
154
154
155
155
156 def findexternaltool(ui, tool):
156 def findexternaltool(ui, tool):
157 for kn in (b"regkey", b"regkeyalt"):
157 for kn in (b"regkey", b"regkeyalt"):
158 k = _toolstr(ui, tool, kn)
158 k = _toolstr(ui, tool, kn)
159 if not k:
159 if not k:
160 continue
160 continue
161 p = util.lookupreg(k, _toolstr(ui, tool, b"regname"))
161 p = util.lookupreg(k, _toolstr(ui, tool, b"regname"))
162 if p:
162 if p:
163 p = procutil.findexe(p + _toolstr(ui, tool, b"regappend", b""))
163 p = procutil.findexe(p + _toolstr(ui, tool, b"regappend", b""))
164 if p:
164 if p:
165 return p
165 return p
166 exe = _toolstr(ui, tool, b"executable", tool)
166 exe = _toolstr(ui, tool, b"executable", tool)
167 return procutil.findexe(util.expandpath(exe))
167 return procutil.findexe(util.expandpath(exe))
168
168
169
169
170 def _picktool(repo, ui, path, binary, symlink, changedelete):
170 def _picktool(repo, ui, path, binary, symlink, changedelete):
171 strictcheck = ui.configbool(b'merge', b'strict-capability-check')
171 strictcheck = ui.configbool(b'merge', b'strict-capability-check')
172
172
173 def hascapability(tool, capability, strict=False):
173 def hascapability(tool, capability, strict=False):
174 if tool in internals:
174 if tool in internals:
175 return strict and internals[tool].capabilities.get(capability)
175 return strict and internals[tool].capabilities.get(capability)
176 return _toolbool(ui, tool, capability)
176 return _toolbool(ui, tool, capability)
177
177
178 def supportscd(tool):
178 def supportscd(tool):
179 return tool in internals and internals[tool].mergetype == nomerge
179 return tool in internals and internals[tool].mergetype == nomerge
180
180
181 def check(tool, pat, symlink, binary, changedelete):
181 def check(tool, pat, symlink, binary, changedelete):
182 tmsg = tool
182 tmsg = tool
183 if pat:
183 if pat:
184 tmsg = _(b"%s (for pattern %s)") % (tool, pat)
184 tmsg = _(b"%s (for pattern %s)") % (tool, pat)
185 if not _findtool(ui, tool):
185 if not _findtool(ui, tool):
186 if pat: # explicitly requested tool deserves a warning
186 if pat: # explicitly requested tool deserves a warning
187 ui.warn(_(b"couldn't find merge tool %s\n") % tmsg)
187 ui.warn(_(b"couldn't find merge tool %s\n") % tmsg)
188 else: # configured but non-existing tools are more silent
188 else: # configured but non-existing tools are more silent
189 ui.note(_(b"couldn't find merge tool %s\n") % tmsg)
189 ui.note(_(b"couldn't find merge tool %s\n") % tmsg)
190 elif symlink and not hascapability(tool, b"symlink", strictcheck):
190 elif symlink and not hascapability(tool, b"symlink", strictcheck):
191 ui.warn(_(b"tool %s can't handle symlinks\n") % tmsg)
191 ui.warn(_(b"tool %s can't handle symlinks\n") % tmsg)
192 elif binary and not hascapability(tool, b"binary", strictcheck):
192 elif binary and not hascapability(tool, b"binary", strictcheck):
193 ui.warn(_(b"tool %s can't handle binary\n") % tmsg)
193 ui.warn(_(b"tool %s can't handle binary\n") % tmsg)
194 elif changedelete and not supportscd(tool):
194 elif changedelete and not supportscd(tool):
195 # the nomerge tools are the only tools that support change/delete
195 # the nomerge tools are the only tools that support change/delete
196 # conflicts
196 # conflicts
197 pass
197 pass
198 elif not procutil.gui() and _toolbool(ui, tool, b"gui"):
198 elif not procutil.gui() and _toolbool(ui, tool, b"gui"):
199 ui.warn(_(b"tool %s requires a GUI\n") % tmsg)
199 ui.warn(_(b"tool %s requires a GUI\n") % tmsg)
200 else:
200 else:
201 return True
201 return True
202 return False
202 return False
203
203
204 # internal config: ui.forcemerge
204 # internal config: ui.forcemerge
205 # forcemerge comes from command line arguments, highest priority
205 # forcemerge comes from command line arguments, highest priority
206 force = ui.config(b'ui', b'forcemerge')
206 force = ui.config(b'ui', b'forcemerge')
207 if force:
207 if force:
208 toolpath = _findtool(ui, force)
208 toolpath = _findtool(ui, force)
209 if changedelete and not supportscd(toolpath):
209 if changedelete and not supportscd(toolpath):
210 return b":prompt", None
210 return b":prompt", None
211 else:
211 else:
212 if toolpath:
212 if toolpath:
213 return (force, _quotetoolpath(toolpath))
213 return (force, _quotetoolpath(toolpath))
214 else:
214 else:
215 # mimic HGMERGE if given tool not found
215 # mimic HGMERGE if given tool not found
216 return (force, force)
216 return (force, force)
217
217
218 # HGMERGE takes next precedence
218 # HGMERGE takes next precedence
219 hgmerge = encoding.environ.get(b"HGMERGE")
219 hgmerge = encoding.environ.get(b"HGMERGE")
220 if hgmerge:
220 if hgmerge:
221 if changedelete and not supportscd(hgmerge):
221 if changedelete and not supportscd(hgmerge):
222 return b":prompt", None
222 return b":prompt", None
223 else:
223 else:
224 return (hgmerge, hgmerge)
224 return (hgmerge, hgmerge)
225
225
226 # then patterns
226 # then patterns
227
227
228 # whether binary capability should be checked strictly
228 # whether binary capability should be checked strictly
229 binarycap = binary and strictcheck
229 binarycap = binary and strictcheck
230
230
231 for pat, tool in ui.configitems(b"merge-patterns"):
231 for pat, tool in ui.configitems(b"merge-patterns"):
232 mf = match.match(repo.root, b'', [pat])
232 mf = match.match(repo.root, b'', [pat])
233 if mf(path) and check(tool, pat, symlink, binarycap, changedelete):
233 if mf(path) and check(tool, pat, symlink, binarycap, changedelete):
234 if binary and not hascapability(tool, b"binary", strict=True):
234 if binary and not hascapability(tool, b"binary", strict=True):
235 ui.warn(
235 ui.warn(
236 _(
236 _(
237 b"warning: check merge-patterns configurations,"
237 b"warning: check merge-patterns configurations,"
238 b" if %r for binary file %r is unintentional\n"
238 b" if %r for binary file %r is unintentional\n"
239 b"(see 'hg help merge-tools'"
239 b"(see 'hg help merge-tools'"
240 b" for binary files capability)\n"
240 b" for binary files capability)\n"
241 )
241 )
242 % (pycompat.bytestr(tool), pycompat.bytestr(path))
242 % (pycompat.bytestr(tool), pycompat.bytestr(path))
243 )
243 )
244 toolpath = _findtool(ui, tool)
244 toolpath = _findtool(ui, tool)
245 return (tool, _quotetoolpath(toolpath))
245 return (tool, _quotetoolpath(toolpath))
246
246
247 # then merge tools
247 # then merge tools
248 tools = {}
248 tools = {}
249 disabled = set()
249 disabled = set()
250 for k, v in ui.configitems(b"merge-tools"):
250 for k, v in ui.configitems(b"merge-tools"):
251 t = k.split(b'.')[0]
251 t = k.split(b'.')[0]
252 if t not in tools:
252 if t not in tools:
253 tools[t] = int(_toolstr(ui, t, b"priority"))
253 tools[t] = int(_toolstr(ui, t, b"priority"))
254 if _toolbool(ui, t, b"disabled"):
254 if _toolbool(ui, t, b"disabled"):
255 disabled.add(t)
255 disabled.add(t)
256 names = tools.keys()
256 names = tools.keys()
257 tools = sorted(
257 tools = sorted(
258 [(-p, tool) for tool, p in tools.items() if tool not in disabled]
258 [(-p, tool) for tool, p in tools.items() if tool not in disabled]
259 )
259 )
260 uimerge = ui.config(b"ui", b"merge")
260 uimerge = ui.config(b"ui", b"merge")
261 if uimerge:
261 if uimerge:
262 # external tools defined in uimerge won't be able to handle
262 # external tools defined in uimerge won't be able to handle
263 # change/delete conflicts
263 # change/delete conflicts
264 if check(uimerge, path, symlink, binary, changedelete):
264 if check(uimerge, path, symlink, binary, changedelete):
265 if uimerge not in names and not changedelete:
265 if uimerge not in names and not changedelete:
266 return (uimerge, uimerge)
266 return (uimerge, uimerge)
267 tools.insert(0, (None, uimerge)) # highest priority
267 tools.insert(0, (None, uimerge)) # highest priority
268 tools.append((None, b"hgmerge")) # the old default, if found
268 tools.append((None, b"hgmerge")) # the old default, if found
269 for p, t in tools:
269 for p, t in tools:
270 if check(t, None, symlink, binary, changedelete):
270 if check(t, None, symlink, binary, changedelete):
271 toolpath = _findtool(ui, t)
271 toolpath = _findtool(ui, t)
272 return (t, _quotetoolpath(toolpath))
272 return (t, _quotetoolpath(toolpath))
273
273
274 # internal merge or prompt as last resort
274 # internal merge or prompt as last resort
275 if symlink or binary or changedelete:
275 if symlink or binary or changedelete:
276 if not changedelete and len(tools):
276 if not changedelete and len(tools):
277 # any tool is rejected by capability for symlink or binary
277 # any tool is rejected by capability for symlink or binary
278 ui.warn(_(b"no tool found to merge %s\n") % path)
278 ui.warn(_(b"no tool found to merge %s\n") % path)
279 return b":prompt", None
279 return b":prompt", None
280 return b":merge", None
280 return b":merge", None
281
281
282
282
283 def _eoltype(data):
283 def _eoltype(data):
284 """Guess the EOL type of a file"""
284 """Guess the EOL type of a file"""
285 if b'\0' in data: # binary
285 if b'\0' in data: # binary
286 return None
286 return None
287 if b'\r\n' in data: # Windows
287 if b'\r\n' in data: # Windows
288 return b'\r\n'
288 return b'\r\n'
289 if b'\r' in data: # Old Mac
289 if b'\r' in data: # Old Mac
290 return b'\r'
290 return b'\r'
291 if b'\n' in data: # UNIX
291 if b'\n' in data: # UNIX
292 return b'\n'
292 return b'\n'
293 return None # unknown
293 return None # unknown
294
294
295
295
296 def _matcheol(file, back):
296 def _matcheol(file, back):
297 """Convert EOL markers in a file to match origfile"""
297 """Convert EOL markers in a file to match origfile"""
298 tostyle = _eoltype(back.data()) # No repo.wread filters?
298 tostyle = _eoltype(back.data()) # No repo.wread filters?
299 if tostyle:
299 if tostyle:
300 data = util.readfile(file)
300 data = util.readfile(file)
301 style = _eoltype(data)
301 style = _eoltype(data)
302 if style:
302 if style:
303 newdata = data.replace(style, tostyle)
303 newdata = data.replace(style, tostyle)
304 if newdata != data:
304 if newdata != data:
305 util.writefile(file, newdata)
305 util.writefile(file, newdata)
306
306
307
307
308 @internaltool(b'prompt', nomerge)
308 @internaltool(b'prompt', nomerge)
309 def _iprompt(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
309 def _iprompt(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
310 """Asks the user which of the local `p1()` or the other `p2()` version to
310 """Asks the user which of the local `p1()` or the other `p2()` version to
311 keep as the merged version."""
311 keep as the merged version."""
312 ui = repo.ui
312 ui = repo.ui
313 fd = fcd.path()
313 fd = fcd.path()
314 uipathfn = scmutil.getuipathfn(repo)
314 uipathfn = scmutil.getuipathfn(repo)
315
315
316 # Avoid prompting during an in-memory merge since it doesn't support merge
316 # Avoid prompting during an in-memory merge since it doesn't support merge
317 # conflicts.
317 # conflicts.
318 if fcd.changectx().isinmemory():
318 if fcd.changectx().isinmemory():
319 raise error.InMemoryMergeConflictsError(
319 raise error.InMemoryMergeConflictsError(
320 b'in-memory merge does not support file conflicts'
320 b'in-memory merge does not support file conflicts'
321 )
321 )
322
322
323 prompts = partextras(labels)
323 prompts = partextras(labels)
324 prompts[b'fd'] = uipathfn(fd)
324 prompts[b'fd'] = uipathfn(fd)
325 try:
325 try:
326 if fco.isabsent():
326 if fco.isabsent():
327 index = ui.promptchoice(_localchangedotherdeletedmsg % prompts, 2)
327 index = ui.promptchoice(_localchangedotherdeletedmsg % prompts, 2)
328 choice = [b'local', b'other', b'unresolved'][index]
328 choice = [b'local', b'other', b'unresolved'][index]
329 elif fcd.isabsent():
329 elif fcd.isabsent():
330 index = ui.promptchoice(_otherchangedlocaldeletedmsg % prompts, 2)
330 index = ui.promptchoice(_otherchangedlocaldeletedmsg % prompts, 2)
331 choice = [b'other', b'local', b'unresolved'][index]
331 choice = [b'other', b'local', b'unresolved'][index]
332 else:
332 else:
333 # IMPORTANT: keep the last line of this prompt ("What do you want to
333 # IMPORTANT: keep the last line of this prompt ("What do you want to
334 # do?") very short, see comment next to _localchangedotherdeletedmsg
334 # do?") very short, see comment next to _localchangedotherdeletedmsg
335 # at the top of the file for details.
335 # at the top of the file for details.
336 index = ui.promptchoice(
336 index = ui.promptchoice(
337 _(
337 _(
338 b"file '%(fd)s' needs to be resolved.\n"
338 b"file '%(fd)s' needs to be resolved.\n"
339 b"You can keep (l)ocal%(l)s, take (o)ther%(o)s, or leave "
339 b"You can keep (l)ocal%(l)s, take (o)ther%(o)s, or leave "
340 b"(u)nresolved.\n"
340 b"(u)nresolved.\n"
341 b"What do you want to do?"
341 b"What do you want to do?"
342 b"$$ &Local $$ &Other $$ &Unresolved"
342 b"$$ &Local $$ &Other $$ &Unresolved"
343 )
343 )
344 % prompts,
344 % prompts,
345 2,
345 2,
346 )
346 )
347 choice = [b'local', b'other', b'unresolved'][index]
347 choice = [b'local', b'other', b'unresolved'][index]
348
348
349 if choice == b'other':
349 if choice == b'other':
350 return _iother(repo, mynode, orig, fcd, fco, fca, toolconf, labels)
350 return _iother(repo, mynode, orig, fcd, fco, fca, toolconf, labels)
351 elif choice == b'local':
351 elif choice == b'local':
352 return _ilocal(repo, mynode, orig, fcd, fco, fca, toolconf, labels)
352 return _ilocal(repo, mynode, orig, fcd, fco, fca, toolconf, labels)
353 elif choice == b'unresolved':
353 elif choice == b'unresolved':
354 return _ifail(repo, mynode, orig, fcd, fco, fca, toolconf, labels)
354 return _ifail(repo, mynode, orig, fcd, fco, fca, toolconf, labels)
355 except error.ResponseExpected:
355 except error.ResponseExpected:
356 ui.write(b"\n")
356 ui.write(b"\n")
357 return _ifail(repo, mynode, orig, fcd, fco, fca, toolconf, labels)
357 return _ifail(repo, mynode, orig, fcd, fco, fca, toolconf, labels)
358
358
359
359
360 @internaltool(b'local', nomerge)
360 @internaltool(b'local', nomerge)
361 def _ilocal(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
361 def _ilocal(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
362 """Uses the local `p1()` version of files as the merged version."""
362 """Uses the local `p1()` version of files as the merged version."""
363 return 0, fcd.isabsent()
363 return 0, fcd.isabsent()
364
364
365
365
366 @internaltool(b'other', nomerge)
366 @internaltool(b'other', nomerge)
367 def _iother(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
367 def _iother(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
368 """Uses the other `p2()` version of files as the merged version."""
368 """Uses the other `p2()` version of files as the merged version."""
369 if fco.isabsent():
369 if fco.isabsent():
370 # local changed, remote deleted -- 'deleted' picked
370 # local changed, remote deleted -- 'deleted' picked
371 _underlyingfctxifabsent(fcd).remove()
371 _underlyingfctxifabsent(fcd).remove()
372 deleted = True
372 deleted = True
373 else:
373 else:
374 _underlyingfctxifabsent(fcd).write(fco.data(), fco.flags())
374 _underlyingfctxifabsent(fcd).write(fco.data(), fco.flags())
375 deleted = False
375 deleted = False
376 return 0, deleted
376 return 0, deleted
377
377
378
378
379 @internaltool(b'fail', nomerge)
379 @internaltool(b'fail', nomerge)
380 def _ifail(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
380 def _ifail(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
381 """
381 """
382 Rather than attempting to merge files that were modified on both
382 Rather than attempting to merge files that were modified on both
383 branches, it marks them as unresolved. The resolve command must be
383 branches, it marks them as unresolved. The resolve command must be
384 used to resolve these conflicts."""
384 used to resolve these conflicts."""
385 # for change/delete conflicts write out the changed version, then fail
385 # for change/delete conflicts write out the changed version, then fail
386 if fcd.isabsent():
386 if fcd.isabsent():
387 _underlyingfctxifabsent(fcd).write(fco.data(), fco.flags())
387 _underlyingfctxifabsent(fcd).write(fco.data(), fco.flags())
388 return 1, False
388 return 1, False
389
389
390
390
391 def _underlyingfctxifabsent(filectx):
391 def _underlyingfctxifabsent(filectx):
392 """Sometimes when resolving, our fcd is actually an absentfilectx, but
392 """Sometimes when resolving, our fcd is actually an absentfilectx, but
393 we want to write to it (to do the resolve). This helper returns the
393 we want to write to it (to do the resolve). This helper returns the
394 underyling workingfilectx in that case.
394 underyling workingfilectx in that case.
395 """
395 """
396 if filectx.isabsent():
396 if filectx.isabsent():
397 return filectx.changectx()[filectx.path()]
397 return filectx.changectx()[filectx.path()]
398 else:
398 else:
399 return filectx
399 return filectx
400
400
401
401
402 def _premerge(repo, fcd, fco, fca, toolconf, files, labels=None):
402 def _premerge(repo, fcd, fco, fca, toolconf, files, labels=None):
403 tool, toolpath, binary, symlink, scriptfn = toolconf
403 tool, toolpath, binary, symlink, scriptfn = toolconf
404 if symlink or fcd.isabsent() or fco.isabsent():
404 if symlink or fcd.isabsent() or fco.isabsent():
405 return 1
405 return 1
406 unused, unused, unused, back = files
406 unused, unused, unused, back = files
407
407
408 ui = repo.ui
408 ui = repo.ui
409
409
410 validkeep = [b'keep', b'keep-merge3', b'keep-mergediff']
410 validkeep = [b'keep', b'keep-merge3', b'keep-mergediff']
411
411
412 # do we attempt to simplemerge first?
412 # do we attempt to simplemerge first?
413 try:
413 try:
414 premerge = _toolbool(ui, tool, b"premerge", not binary)
414 premerge = _toolbool(ui, tool, b"premerge", not binary)
415 except error.ConfigError:
415 except error.ConfigError:
416 premerge = _toolstr(ui, tool, b"premerge", b"").lower()
416 premerge = _toolstr(ui, tool, b"premerge", b"").lower()
417 if premerge not in validkeep:
417 if premerge not in validkeep:
418 _valid = b', '.join([b"'" + v + b"'" for v in validkeep])
418 _valid = b', '.join([b"'" + v + b"'" for v in validkeep])
419 raise error.ConfigError(
419 raise error.ConfigError(
420 _(b"%s.premerge not valid ('%s' is neither boolean nor %s)")
420 _(b"%s.premerge not valid ('%s' is neither boolean nor %s)")
421 % (tool, premerge, _valid)
421 % (tool, premerge, _valid)
422 )
422 )
423
423
424 if premerge:
424 if premerge:
425 mode = b'merge'
425 mode = b'merge'
426 if premerge in {b'keep-merge3', b'keep-mergediff'}:
426 if premerge in {b'keep-merge3', b'keep-mergediff'}:
427 if not labels:
427 if not labels:
428 labels = _defaultconflictlabels
428 labels = _defaultconflictlabels
429 if len(labels) < 3:
429 if len(labels) < 3:
430 labels.append(b'base')
430 labels.append(b'base')
431 if premerge == b'keep-mergediff':
431 if premerge == b'keep-mergediff':
432 mode = b'mergediff'
432 mode = b'mergediff'
433 r = simplemerge.simplemerge(
433 r = simplemerge.simplemerge(
434 ui, fcd, fca, fco, quiet=True, label=labels, mode=mode
434 ui, fcd, fca, fco, quiet=True, label=labels, mode=mode
435 )
435 )
436 if not r:
436 if not r:
437 ui.debug(b" premerge successful\n")
437 ui.debug(b" premerge successful\n")
438 return 0
438 return 0
439 if premerge not in validkeep:
439 if premerge not in validkeep:
440 # restore from backup and try again
440 # restore from backup and try again
441 _restorebackup(fcd, back)
441 _restorebackup(fcd, back)
442 return 1 # continue merging
442 return 1 # continue merging
443
443
444
444
445 def _mergecheck(repo, mynode, orig, fcd, fco, fca, toolconf):
445 def _mergecheck(repo, mynode, orig, fcd, fco, fca, toolconf):
446 tool, toolpath, binary, symlink, scriptfn = toolconf
446 tool, toolpath, binary, symlink, scriptfn = toolconf
447 uipathfn = scmutil.getuipathfn(repo)
447 uipathfn = scmutil.getuipathfn(repo)
448 if symlink:
448 if symlink:
449 repo.ui.warn(
449 repo.ui.warn(
450 _(b'warning: internal %s cannot merge symlinks for %s\n')
450 _(b'warning: internal %s cannot merge symlinks for %s\n')
451 % (tool, uipathfn(fcd.path()))
451 % (tool, uipathfn(fcd.path()))
452 )
452 )
453 return False
453 return False
454 if fcd.isabsent() or fco.isabsent():
454 if fcd.isabsent() or fco.isabsent():
455 repo.ui.warn(
455 repo.ui.warn(
456 _(
456 _(
457 b'warning: internal %s cannot merge change/delete '
457 b'warning: internal %s cannot merge change/delete '
458 b'conflict for %s\n'
458 b'conflict for %s\n'
459 )
459 )
460 % (tool, uipathfn(fcd.path()))
460 % (tool, uipathfn(fcd.path()))
461 )
461 )
462 return False
462 return False
463 return True
463 return True
464
464
465
465
466 def _merge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels, mode):
466 def _merge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels, mode):
467 """
467 """
468 Uses the internal non-interactive simple merge algorithm for merging
468 Uses the internal non-interactive simple merge algorithm for merging
469 files. It will fail if there are any conflicts and leave markers in
469 files. It will fail if there are any conflicts and leave markers in
470 the partially merged file. Markers will have two sections, one for each side
470 the partially merged file. Markers will have two sections, one for each side
471 of merge, unless mode equals 'union' which suppresses the markers."""
471 of merge, unless mode equals 'union' which suppresses the markers."""
472 ui = repo.ui
472 ui = repo.ui
473
473
474 r = simplemerge.simplemerge(ui, fcd, fca, fco, label=labels, mode=mode)
474 r = simplemerge.simplemerge(ui, fcd, fca, fco, label=labels, mode=mode)
475 return True, r, False
475 return True, r, False
476
476
477
477
478 @internaltool(
478 @internaltool(
479 b'union',
479 b'union',
480 fullmerge,
480 fullmerge,
481 _(
481 _(
482 b"warning: conflicts while merging %s! "
482 b"warning: conflicts while merging %s! "
483 b"(edit, then use 'hg resolve --mark')\n"
483 b"(edit, then use 'hg resolve --mark')\n"
484 ),
484 ),
485 precheck=_mergecheck,
485 precheck=_mergecheck,
486 )
486 )
487 def _iunion(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
487 def _iunion(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
488 """
488 """
489 Uses the internal non-interactive simple merge algorithm for merging
489 Uses the internal non-interactive simple merge algorithm for merging
490 files. It will use both left and right sides for conflict regions.
490 files. It will use both left and right sides for conflict regions.
491 No markers are inserted."""
491 No markers are inserted."""
492 return _merge(
492 return _merge(
493 repo, mynode, orig, fcd, fco, fca, toolconf, files, labels, b'union'
493 repo, mynode, orig, fcd, fco, fca, toolconf, files, labels, b'union'
494 )
494 )
495
495
496
496
497 @internaltool(
497 @internaltool(
498 b'merge',
498 b'merge',
499 fullmerge,
499 fullmerge,
500 _(
500 _(
501 b"warning: conflicts while merging %s! "
501 b"warning: conflicts while merging %s! "
502 b"(edit, then use 'hg resolve --mark')\n"
502 b"(edit, then use 'hg resolve --mark')\n"
503 ),
503 ),
504 precheck=_mergecheck,
504 precheck=_mergecheck,
505 )
505 )
506 def _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
506 def _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
507 """
507 """
508 Uses the internal non-interactive simple merge algorithm for merging
508 Uses the internal non-interactive simple merge algorithm for merging
509 files. It will fail if there are any conflicts and leave markers in
509 files. It will fail if there are any conflicts and leave markers in
510 the partially merged file. Markers will have two sections, one for each side
510 the partially merged file. Markers will have two sections, one for each side
511 of merge."""
511 of merge."""
512 return _merge(
512 return _merge(
513 repo, mynode, orig, fcd, fco, fca, toolconf, files, labels, b'merge'
513 repo, mynode, orig, fcd, fco, fca, toolconf, files, labels, b'merge'
514 )
514 )
515
515
516
516
517 @internaltool(
517 @internaltool(
518 b'merge3',
518 b'merge3',
519 fullmerge,
519 fullmerge,
520 _(
520 _(
521 b"warning: conflicts while merging %s! "
521 b"warning: conflicts while merging %s! "
522 b"(edit, then use 'hg resolve --mark')\n"
522 b"(edit, then use 'hg resolve --mark')\n"
523 ),
523 ),
524 precheck=_mergecheck,
524 precheck=_mergecheck,
525 )
525 )
526 def _imerge3(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
526 def _imerge3(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
527 """
527 """
528 Uses the internal non-interactive simple merge algorithm for merging
528 Uses the internal non-interactive simple merge algorithm for merging
529 files. It will fail if there are any conflicts and leave markers in
529 files. It will fail if there are any conflicts and leave markers in
530 the partially merged file. Marker will have three sections, one from each
530 the partially merged file. Marker will have three sections, one from each
531 side of the merge and one for the base content."""
531 side of the merge and one for the base content."""
532 if not labels:
532 if not labels:
533 labels = _defaultconflictlabels
533 labels = _defaultconflictlabels
534 if len(labels) < 3:
534 if len(labels) < 3:
535 labels.append(b'base')
535 labels.append(b'base')
536 return _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels)
536 return _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels)
537
537
538
538
539 @internaltool(
539 @internaltool(
540 b'merge3-lie-about-conflicts',
540 b'merge3-lie-about-conflicts',
541 fullmerge,
541 fullmerge,
542 b'',
542 b'',
543 precheck=_mergecheck,
543 precheck=_mergecheck,
544 )
544 )
545 def _imerge3alwaysgood(*args, **kwargs):
545 def _imerge3alwaysgood(*args, **kwargs):
546 # Like merge3, but record conflicts as resolved with markers in place.
546 # Like merge3, but record conflicts as resolved with markers in place.
547 #
547 #
548 # This is used for `diff.merge` to show the differences between
548 # This is used for `diff.merge` to show the differences between
549 # the auto-merge state and the committed merge state. It may be
549 # the auto-merge state and the committed merge state. It may be
550 # useful for other things.
550 # useful for other things.
551 b1, junk, b2 = _imerge3(*args, **kwargs)
551 b1, junk, b2 = _imerge3(*args, **kwargs)
552 # TODO is this right? I'm not sure what these return values mean,
552 # TODO is this right? I'm not sure what these return values mean,
553 # but as far as I can tell this will indicate to callers tha the
553 # but as far as I can tell this will indicate to callers tha the
554 # merge succeeded.
554 # merge succeeded.
555 return b1, False, b2
555 return b1, False, b2
556
556
557
557
558 @internaltool(
558 @internaltool(
559 b'mergediff',
559 b'mergediff',
560 fullmerge,
560 fullmerge,
561 _(
561 _(
562 b"warning: conflicts while merging %s! "
562 b"warning: conflicts while merging %s! "
563 b"(edit, then use 'hg resolve --mark')\n"
563 b"(edit, then use 'hg resolve --mark')\n"
564 ),
564 ),
565 precheck=_mergecheck,
565 precheck=_mergecheck,
566 )
566 )
567 def _imerge_diff(
567 def _imerge_diff(
568 repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None
568 repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None
569 ):
569 ):
570 """
570 """
571 Uses the internal non-interactive simple merge algorithm for merging
571 Uses the internal non-interactive simple merge algorithm for merging
572 files. It will fail if there are any conflicts and leave markers in
572 files. It will fail if there are any conflicts and leave markers in
573 the partially merged file. The marker will have two sections, one with the
573 the partially merged file. The marker will have two sections, one with the
574 content from one side of the merge, and one with a diff from the base
574 content from one side of the merge, and one with a diff from the base
575 content to the content on the other side. (experimental)"""
575 content to the content on the other side. (experimental)"""
576 if not labels:
576 if not labels:
577 labels = _defaultconflictlabels
577 labels = _defaultconflictlabels
578 if len(labels) < 3:
578 if len(labels) < 3:
579 labels.append(b'base')
579 labels.append(b'base')
580 return _merge(
580 return _merge(
581 repo, mynode, orig, fcd, fco, fca, toolconf, files, labels, b'mergediff'
581 repo, mynode, orig, fcd, fco, fca, toolconf, files, labels, b'mergediff'
582 )
582 )
583
583
584
584
585 def _imergeauto(
585 def _imergeauto(
586 repo,
586 repo,
587 mynode,
587 mynode,
588 orig,
588 orig,
589 fcd,
589 fcd,
590 fco,
590 fco,
591 fca,
591 fca,
592 toolconf,
592 toolconf,
593 files,
593 files,
594 labels=None,
594 labels=None,
595 localorother=None,
595 localorother=None,
596 ):
596 ):
597 """
597 """
598 Generic driver for _imergelocal and _imergeother
598 Generic driver for _imergelocal and _imergeother
599 """
599 """
600 assert localorother is not None
600 assert localorother is not None
601 r = simplemerge.simplemerge(
601 r = simplemerge.simplemerge(
602 repo.ui, fcd, fca, fco, label=labels, localorother=localorother
602 repo.ui, fcd, fca, fco, label=labels, localorother=localorother
603 )
603 )
604 return True, r
604 return True, r
605
605
606
606
607 @internaltool(b'merge-local', mergeonly, precheck=_mergecheck)
607 @internaltool(b'merge-local', mergeonly, precheck=_mergecheck)
608 def _imergelocal(*args, **kwargs):
608 def _imergelocal(*args, **kwargs):
609 """
609 """
610 Like :merge, but resolve all conflicts non-interactively in favor
610 Like :merge, but resolve all conflicts non-interactively in favor
611 of the local `p1()` changes."""
611 of the local `p1()` changes."""
612 success, status = _imergeauto(localorother=b'local', *args, **kwargs)
612 success, status = _imergeauto(localorother=b'local', *args, **kwargs)
613 return success, status, False
613 return success, status, False
614
614
615
615
616 @internaltool(b'merge-other', mergeonly, precheck=_mergecheck)
616 @internaltool(b'merge-other', mergeonly, precheck=_mergecheck)
617 def _imergeother(*args, **kwargs):
617 def _imergeother(*args, **kwargs):
618 """
618 """
619 Like :merge, but resolve all conflicts non-interactively in favor
619 Like :merge, but resolve all conflicts non-interactively in favor
620 of the other `p2()` changes."""
620 of the other `p2()` changes."""
621 success, status = _imergeauto(localorother=b'other', *args, **kwargs)
621 success, status = _imergeauto(localorother=b'other', *args, **kwargs)
622 return success, status, False
622 return success, status, False
623
623
624
624
625 @internaltool(
625 @internaltool(
626 b'tagmerge',
626 b'tagmerge',
627 mergeonly,
627 mergeonly,
628 _(
628 _(
629 b"automatic tag merging of %s failed! "
629 b"automatic tag merging of %s failed! "
630 b"(use 'hg resolve --tool :merge' or another merge "
630 b"(use 'hg resolve --tool :merge' or another merge "
631 b"tool of your choice)\n"
631 b"tool of your choice)\n"
632 ),
632 ),
633 )
633 )
634 def _itagmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
634 def _itagmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
635 """
635 """
636 Uses the internal tag merge algorithm (experimental).
636 Uses the internal tag merge algorithm (experimental).
637 """
637 """
638 success, status = tagmerge.merge(repo, fcd, fco, fca)
638 success, status = tagmerge.merge(repo, fcd, fco, fca)
639 return success, status, False
639 return success, status, False
640
640
641
641
642 @internaltool(b'dump', fullmerge, binary=True, symlink=True)
642 @internaltool(b'dump', fullmerge, binary=True, symlink=True)
643 def _idump(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
643 def _idump(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
644 """
644 """
645 Creates three versions of the files to merge, containing the
645 Creates three versions of the files to merge, containing the
646 contents of local, other and base. These files can then be used to
646 contents of local, other and base. These files can then be used to
647 perform a merge manually. If the file to be merged is named
647 perform a merge manually. If the file to be merged is named
648 ``a.txt``, these files will accordingly be named ``a.txt.local``,
648 ``a.txt``, these files will accordingly be named ``a.txt.local``,
649 ``a.txt.other`` and ``a.txt.base`` and they will be placed in the
649 ``a.txt.other`` and ``a.txt.base`` and they will be placed in the
650 same directory as ``a.txt``.
650 same directory as ``a.txt``.
651
651
652 This implies premerge. Therefore, files aren't dumped, if premerge
652 This implies premerge. Therefore, files aren't dumped, if premerge
653 runs successfully. Use :forcedump to forcibly write files out.
653 runs successfully. Use :forcedump to forcibly write files out.
654 """
654 """
655 a = _workingpath(repo, fcd)
655 a = _workingpath(repo, fcd)
656 fd = fcd.path()
656 fd = fcd.path()
657
657
658 from . import context
658 from . import context
659
659
660 if isinstance(fcd, context.overlayworkingfilectx):
660 if isinstance(fcd, context.overlayworkingfilectx):
661 raise error.InMemoryMergeConflictsError(
661 raise error.InMemoryMergeConflictsError(
662 b'in-memory merge does not support the :dump tool.'
662 b'in-memory merge does not support the :dump tool.'
663 )
663 )
664
664
665 util.writefile(a + b".local", fcd.decodeddata())
665 util.writefile(a + b".local", fcd.decodeddata())
666 repo.wwrite(fd + b".other", fco.data(), fco.flags())
666 repo.wwrite(fd + b".other", fco.data(), fco.flags())
667 repo.wwrite(fd + b".base", fca.data(), fca.flags())
667 repo.wwrite(fd + b".base", fca.data(), fca.flags())
668 return False, 1, False
668 return False, 1, False
669
669
670
670
671 @internaltool(b'forcedump', mergeonly, binary=True, symlink=True)
671 @internaltool(b'forcedump', mergeonly, binary=True, symlink=True)
672 def _forcedump(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
672 def _forcedump(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
673 """
673 """
674 Creates three versions of the files as same as :dump, but omits premerge.
674 Creates three versions of the files as same as :dump, but omits premerge.
675 """
675 """
676 return _idump(
676 return _idump(
677 repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=labels
677 repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=labels
678 )
678 )
679
679
680
680
681 def _xmergeimm(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
681 def _xmergeimm(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
682 # In-memory merge simply raises an exception on all external merge tools,
682 # In-memory merge simply raises an exception on all external merge tools,
683 # for now.
683 # for now.
684 #
684 #
685 # It would be possible to run most tools with temporary files, but this
685 # It would be possible to run most tools with temporary files, but this
686 # raises the question of what to do if the user only partially resolves the
686 # raises the question of what to do if the user only partially resolves the
687 # file -- we can't leave a merge state. (Copy to somewhere in the .hg/
687 # file -- we can't leave a merge state. (Copy to somewhere in the .hg/
688 # directory and tell the user how to get it is my best idea, but it's
688 # directory and tell the user how to get it is my best idea, but it's
689 # clunky.)
689 # clunky.)
690 raise error.InMemoryMergeConflictsError(
690 raise error.InMemoryMergeConflictsError(
691 b'in-memory merge does not support external merge tools'
691 b'in-memory merge does not support external merge tools'
692 )
692 )
693
693
694
694
695 def _describemerge(ui, repo, mynode, fcl, fcb, fco, env, toolpath, args):
695 def _describemerge(ui, repo, mynode, fcl, fcb, fco, env, toolpath, args):
696 tmpl = ui.config(b'command-templates', b'pre-merge-tool-output')
696 tmpl = ui.config(b'command-templates', b'pre-merge-tool-output')
697 if not tmpl:
697 if not tmpl:
698 return
698 return
699
699
700 mappingdict = templateutil.mappingdict
700 mappingdict = templateutil.mappingdict
701 props = {
701 props = {
702 b'ctx': fcl.changectx(),
702 b'ctx': fcl.changectx(),
703 b'node': hex(mynode),
703 b'node': hex(mynode),
704 b'path': fcl.path(),
704 b'path': fcl.path(),
705 b'local': mappingdict(
705 b'local': mappingdict(
706 {
706 {
707 b'ctx': fcl.changectx(),
707 b'ctx': fcl.changectx(),
708 b'fctx': fcl,
708 b'fctx': fcl,
709 b'node': hex(mynode),
709 b'node': hex(mynode),
710 b'name': _(b'local'),
710 b'name': _(b'local'),
711 b'islink': b'l' in fcl.flags(),
711 b'islink': b'l' in fcl.flags(),
712 b'label': env[b'HG_MY_LABEL'],
712 b'label': env[b'HG_MY_LABEL'],
713 }
713 }
714 ),
714 ),
715 b'base': mappingdict(
715 b'base': mappingdict(
716 {
716 {
717 b'ctx': fcb.changectx(),
717 b'ctx': fcb.changectx(),
718 b'fctx': fcb,
718 b'fctx': fcb,
719 b'name': _(b'base'),
719 b'name': _(b'base'),
720 b'islink': b'l' in fcb.flags(),
720 b'islink': b'l' in fcb.flags(),
721 b'label': env[b'HG_BASE_LABEL'],
721 b'label': env[b'HG_BASE_LABEL'],
722 }
722 }
723 ),
723 ),
724 b'other': mappingdict(
724 b'other': mappingdict(
725 {
725 {
726 b'ctx': fco.changectx(),
726 b'ctx': fco.changectx(),
727 b'fctx': fco,
727 b'fctx': fco,
728 b'name': _(b'other'),
728 b'name': _(b'other'),
729 b'islink': b'l' in fco.flags(),
729 b'islink': b'l' in fco.flags(),
730 b'label': env[b'HG_OTHER_LABEL'],
730 b'label': env[b'HG_OTHER_LABEL'],
731 }
731 }
732 ),
732 ),
733 b'toolpath': toolpath,
733 b'toolpath': toolpath,
734 b'toolargs': args,
734 b'toolargs': args,
735 }
735 }
736
736
737 # TODO: make all of this something that can be specified on a per-tool basis
737 # TODO: make all of this something that can be specified on a per-tool basis
738 tmpl = templater.unquotestring(tmpl)
738 tmpl = templater.unquotestring(tmpl)
739
739
740 # Not using cmdutil.rendertemplate here since it causes errors importing
740 # Not using cmdutil.rendertemplate here since it causes errors importing
741 # things for us to import cmdutil.
741 # things for us to import cmdutil.
742 tres = formatter.templateresources(ui, repo)
742 tres = formatter.templateresources(ui, repo)
743 t = formatter.maketemplater(
743 t = formatter.maketemplater(
744 ui, tmpl, defaults=templatekw.keywords, resources=tres
744 ui, tmpl, defaults=templatekw.keywords, resources=tres
745 )
745 )
746 ui.status(t.renderdefault(props))
746 ui.status(t.renderdefault(props))
747
747
748
748
749 def _xmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels):
749 def _xmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels):
750 tool, toolpath, binary, symlink, scriptfn = toolconf
750 tool, toolpath, binary, symlink, scriptfn = toolconf
751 uipathfn = scmutil.getuipathfn(repo)
751 uipathfn = scmutil.getuipathfn(repo)
752 if fcd.isabsent() or fco.isabsent():
752 if fcd.isabsent() or fco.isabsent():
753 repo.ui.warn(
753 repo.ui.warn(
754 _(b'warning: %s cannot merge change/delete conflict for %s\n')
754 _(b'warning: %s cannot merge change/delete conflict for %s\n')
755 % (tool, uipathfn(fcd.path()))
755 % (tool, uipathfn(fcd.path()))
756 )
756 )
757 return False, 1, None
757 return False, 1, None
758 unused, unused, unused, back = files
758 unused, unused, unused, back = files
759 localpath = _workingpath(repo, fcd)
759 localpath = _workingpath(repo, fcd)
760 args = _toolstr(repo.ui, tool, b"args")
760 args = _toolstr(repo.ui, tool, b"args")
761
761
762 with _maketempfiles(
762 with _maketempfiles(
763 repo, fco, fca, repo.wvfs.join(back.path()), b"$output" in args
763 repo, fco, fca, repo.wvfs.join(back.path()), b"$output" in args
764 ) as temppaths:
764 ) as temppaths:
765 basepath, otherpath, localoutputpath = temppaths
765 basepath, otherpath, localoutputpath = temppaths
766 outpath = b""
766 outpath = b""
767 mylabel, otherlabel = labels[:2]
767 mylabel, otherlabel = labels[:2]
768 if len(labels) >= 3:
768 if len(labels) >= 3:
769 baselabel = labels[2]
769 baselabel = labels[2]
770 else:
770 else:
771 baselabel = b'base'
771 baselabel = b'base'
772 env = {
772 env = {
773 b'HG_FILE': fcd.path(),
773 b'HG_FILE': fcd.path(),
774 b'HG_MY_NODE': short(mynode),
774 b'HG_MY_NODE': short(mynode),
775 b'HG_OTHER_NODE': short(fco.changectx().node()),
775 b'HG_OTHER_NODE': short(fco.changectx().node()),
776 b'HG_BASE_NODE': short(fca.changectx().node()),
776 b'HG_BASE_NODE': short(fca.changectx().node()),
777 b'HG_MY_ISLINK': b'l' in fcd.flags(),
777 b'HG_MY_ISLINK': b'l' in fcd.flags(),
778 b'HG_OTHER_ISLINK': b'l' in fco.flags(),
778 b'HG_OTHER_ISLINK': b'l' in fco.flags(),
779 b'HG_BASE_ISLINK': b'l' in fca.flags(),
779 b'HG_BASE_ISLINK': b'l' in fca.flags(),
780 b'HG_MY_LABEL': mylabel,
780 b'HG_MY_LABEL': mylabel,
781 b'HG_OTHER_LABEL': otherlabel,
781 b'HG_OTHER_LABEL': otherlabel,
782 b'HG_BASE_LABEL': baselabel,
782 b'HG_BASE_LABEL': baselabel,
783 }
783 }
784 ui = repo.ui
784 ui = repo.ui
785
785
786 if b"$output" in args:
786 if b"$output" in args:
787 # read input from backup, write to original
787 # read input from backup, write to original
788 outpath = localpath
788 outpath = localpath
789 localpath = localoutputpath
789 localpath = localoutputpath
790 replace = {
790 replace = {
791 b'local': localpath,
791 b'local': localpath,
792 b'base': basepath,
792 b'base': basepath,
793 b'other': otherpath,
793 b'other': otherpath,
794 b'output': outpath,
794 b'output': outpath,
795 b'labellocal': mylabel,
795 b'labellocal': mylabel,
796 b'labelother': otherlabel,
796 b'labelother': otherlabel,
797 b'labelbase': baselabel,
797 b'labelbase': baselabel,
798 }
798 }
799 args = util.interpolate(
799 args = util.interpolate(
800 br'\$',
800 br'\$',
801 replace,
801 replace,
802 args,
802 args,
803 lambda s: procutil.shellquote(util.localpath(s)),
803 lambda s: procutil.shellquote(util.localpath(s)),
804 )
804 )
805 if _toolbool(ui, tool, b"gui"):
805 if _toolbool(ui, tool, b"gui"):
806 repo.ui.status(
806 repo.ui.status(
807 _(b'running merge tool %s for file %s\n')
807 _(b'running merge tool %s for file %s\n')
808 % (tool, uipathfn(fcd.path()))
808 % (tool, uipathfn(fcd.path()))
809 )
809 )
810 if scriptfn is None:
810 if scriptfn is None:
811 cmd = toolpath + b' ' + args
811 cmd = toolpath + b' ' + args
812 repo.ui.debug(b'launching merge tool: %s\n' % cmd)
812 repo.ui.debug(b'launching merge tool: %s\n' % cmd)
813 _describemerge(ui, repo, mynode, fcd, fca, fco, env, toolpath, args)
813 _describemerge(ui, repo, mynode, fcd, fca, fco, env, toolpath, args)
814 r = ui.system(
814 r = ui.system(
815 cmd, cwd=repo.root, environ=env, blockedtag=b'mergetool'
815 cmd, cwd=repo.root, environ=env, blockedtag=b'mergetool'
816 )
816 )
817 else:
817 else:
818 repo.ui.debug(
818 repo.ui.debug(
819 b'launching python merge script: %s:%s\n' % (toolpath, scriptfn)
819 b'launching python merge script: %s:%s\n' % (toolpath, scriptfn)
820 )
820 )
821 r = 0
821 r = 0
822 try:
822 try:
823 # avoid cycle cmdutil->merge->filemerge->extensions->cmdutil
823 # avoid cycle cmdutil->merge->filemerge->extensions->cmdutil
824 from . import extensions
824 from . import extensions
825
825
826 mod = extensions.loadpath(toolpath, b'hgmerge.%s' % tool)
826 mod = extensions.loadpath(toolpath, b'hgmerge.%s' % tool)
827 except Exception:
827 except Exception:
828 raise error.Abort(
828 raise error.Abort(
829 _(b"loading python merge script failed: %s") % toolpath
829 _(b"loading python merge script failed: %s") % toolpath
830 )
830 )
831 mergefn = getattr(mod, scriptfn, None)
831 mergefn = getattr(mod, scriptfn, None)
832 if mergefn is None:
832 if mergefn is None:
833 raise error.Abort(
833 raise error.Abort(
834 _(b"%s does not have function: %s") % (toolpath, scriptfn)
834 _(b"%s does not have function: %s") % (toolpath, scriptfn)
835 )
835 )
836 argslist = procutil.shellsplit(args)
836 argslist = procutil.shellsplit(args)
837 # avoid cycle cmdutil->merge->filemerge->hook->extensions->cmdutil
837 # avoid cycle cmdutil->merge->filemerge->hook->extensions->cmdutil
838 from . import hook
838 from . import hook
839
839
840 ret, raised = hook.pythonhook(
840 ret, raised = hook.pythonhook(
841 ui, repo, b"merge", toolpath, mergefn, {b'args': argslist}, True
841 ui, repo, b"merge", toolpath, mergefn, {b'args': argslist}, True
842 )
842 )
843 if raised:
843 if raised:
844 r = 1
844 r = 1
845 repo.ui.debug(b'merge tool returned: %d\n' % r)
845 repo.ui.debug(b'merge tool returned: %d\n' % r)
846 return True, r, False
846 return True, r, False
847
847
848
848
849 def _formatconflictmarker(ctx, template, label, pad):
849 def _formatconflictmarker(ctx, template, label, pad):
850 """Applies the given template to the ctx, prefixed by the label.
850 """Applies the given template to the ctx, prefixed by the label.
851
851
852 Pad is the minimum width of the label prefix, so that multiple markers
852 Pad is the minimum width of the label prefix, so that multiple markers
853 can have aligned templated parts.
853 can have aligned templated parts.
854 """
854 """
855 if ctx.node() is None:
855 if ctx.node() is None:
856 ctx = ctx.p1()
856 ctx = ctx.p1()
857
857
858 props = {b'ctx': ctx}
858 props = {b'ctx': ctx}
859 templateresult = template.renderdefault(props)
859 templateresult = template.renderdefault(props)
860
860
861 label = (b'%s:' % label).ljust(pad + 1)
861 label = (b'%s:' % label).ljust(pad + 1)
862 mark = b'%s %s' % (label, templateresult)
862 mark = b'%s %s' % (label, templateresult)
863
863
864 if mark:
864 if mark:
865 mark = mark.splitlines()[0] # split for safety
865 mark = mark.splitlines()[0] # split for safety
866
866
867 # 8 for the prefix of conflict marker lines (e.g. '<<<<<<< ')
867 # 8 for the prefix of conflict marker lines (e.g. '<<<<<<< ')
868 return stringutil.ellipsis(mark, 80 - 8)
868 return stringutil.ellipsis(mark, 80 - 8)
869
869
870
870
871 _defaultconflictlabels = [b'local', b'other']
871 _defaultconflictlabels = [b'local', b'other']
872
872
873
873
874 def _formatlabels(repo, fcd, fco, fca, labels, tool=None):
874 def _formatlabels(repo, fcd, fco, fca, labels, tool=None):
875 """Formats the given labels using the conflict marker template.
875 """Formats the given labels using the conflict marker template.
876
876
877 Returns a list of formatted labels.
877 Returns a list of formatted labels.
878 """
878 """
879 cd = fcd.changectx()
879 cd = fcd.changectx()
880 co = fco.changectx()
880 co = fco.changectx()
881 ca = fca.changectx()
881 ca = fca.changectx()
882
882
883 ui = repo.ui
883 ui = repo.ui
884 template = ui.config(b'command-templates', b'mergemarker')
884 template = ui.config(b'command-templates', b'mergemarker')
885 if tool is not None:
885 if tool is not None:
886 template = _toolstr(ui, tool, b'mergemarkertemplate', template)
886 template = _toolstr(ui, tool, b'mergemarkertemplate', template)
887 template = templater.unquotestring(template)
887 template = templater.unquotestring(template)
888 tres = formatter.templateresources(ui, repo)
888 tres = formatter.templateresources(ui, repo)
889 tmpl = formatter.maketemplater(
889 tmpl = formatter.maketemplater(
890 ui, template, defaults=templatekw.keywords, resources=tres
890 ui, template, defaults=templatekw.keywords, resources=tres
891 )
891 )
892
892
893 pad = max(len(l) for l in labels)
893 pad = max(len(l) for l in labels)
894
894
895 newlabels = [
895 newlabels = [
896 _formatconflictmarker(cd, tmpl, labels[0], pad),
896 _formatconflictmarker(cd, tmpl, labels[0], pad),
897 _formatconflictmarker(co, tmpl, labels[1], pad),
897 _formatconflictmarker(co, tmpl, labels[1], pad),
898 ]
898 ]
899 if len(labels) > 2:
899 if len(labels) > 2:
900 newlabels.append(_formatconflictmarker(ca, tmpl, labels[2], pad))
900 newlabels.append(_formatconflictmarker(ca, tmpl, labels[2], pad))
901 return newlabels
901 return newlabels
902
902
903
903
904 def partextras(labels):
904 def partextras(labels):
905 """Return a dictionary of extra labels for use in prompts to the user
905 """Return a dictionary of extra labels for use in prompts to the user
906
906
907 Intended use is in strings of the form "(l)ocal%(l)s".
907 Intended use is in strings of the form "(l)ocal%(l)s".
908 """
908 """
909 if labels is None:
909 if labels is None:
910 return {
910 return {
911 b"l": b"",
911 b"l": b"",
912 b"o": b"",
912 b"o": b"",
913 }
913 }
914
914
915 return {
915 return {
916 b"l": b" [%s]" % labels[0],
916 b"l": b" [%s]" % labels[0],
917 b"o": b" [%s]" % labels[1],
917 b"o": b" [%s]" % labels[1],
918 }
918 }
919
919
920
920
921 def _restorebackup(fcd, back):
921 def _restorebackup(fcd, back):
922 # TODO: Add a workingfilectx.write(otherfilectx) path so we can use
922 # TODO: Add a workingfilectx.write(otherfilectx) path so we can use
923 # util.copy here instead.
923 # util.copy here instead.
924 fcd.write(back.data(), fcd.flags())
924 fcd.write(back.data(), fcd.flags())
925
925
926
926
927 def _makebackup(repo, ui, wctx, fcd, premerge):
927 def _makebackup(repo, ui, wctx, fcd, premerge):
928 """Makes and returns a filectx-like object for ``fcd``'s backup file.
928 """Makes and returns a filectx-like object for ``fcd``'s backup file.
929
929
930 In addition to preserving the user's pre-existing modifications to `fcd`
930 In addition to preserving the user's pre-existing modifications to `fcd`
931 (if any), the backup is used to undo certain premerges, confirm whether a
931 (if any), the backup is used to undo certain premerges, confirm whether a
932 merge changed anything, and determine what line endings the new file should
932 merge changed anything, and determine what line endings the new file should
933 have.
933 have.
934
934
935 Backups only need to be written once (right before the premerge) since their
935 Backups only need to be written once (right before the premerge) since their
936 content doesn't change afterwards.
936 content doesn't change afterwards.
937 """
937 """
938 if fcd.isabsent():
938 if fcd.isabsent():
939 return None
939 return None
940 # TODO: Break this import cycle somehow. (filectx -> ctx -> fileset ->
940 # TODO: Break this import cycle somehow. (filectx -> ctx -> fileset ->
941 # merge -> filemerge). (I suspect the fileset import is the weakest link)
941 # merge -> filemerge). (I suspect the fileset import is the weakest link)
942 from . import context
942 from . import context
943
943
944 back = scmutil.backuppath(ui, repo, fcd.path())
944 back = scmutil.backuppath(ui, repo, fcd.path())
945 inworkingdir = back.startswith(repo.wvfs.base) and not back.startswith(
945 inworkingdir = back.startswith(repo.wvfs.base) and not back.startswith(
946 repo.vfs.base
946 repo.vfs.base
947 )
947 )
948 if isinstance(fcd, context.overlayworkingfilectx) and inworkingdir:
948 if isinstance(fcd, context.overlayworkingfilectx) and inworkingdir:
949 # If the backup file is to be in the working directory, and we're
949 # If the backup file is to be in the working directory, and we're
950 # merging in-memory, we must redirect the backup to the memory context
950 # merging in-memory, we must redirect the backup to the memory context
951 # so we don't disturb the working directory.
951 # so we don't disturb the working directory.
952 relpath = back[len(repo.wvfs.base) + 1 :]
952 relpath = back[len(repo.wvfs.base) + 1 :]
953 if premerge:
953 if premerge:
954 wctx[relpath].write(fcd.data(), fcd.flags())
954 wctx[relpath].write(fcd.data(), fcd.flags())
955 return wctx[relpath]
955 return wctx[relpath]
956 else:
956 else:
957 if premerge:
957 if premerge:
958 # Otherwise, write to wherever path the user specified the backups
958 # Otherwise, write to wherever path the user specified the backups
959 # should go. We still need to switch based on whether the source is
959 # should go. We still need to switch based on whether the source is
960 # in-memory so we can use the fast path of ``util.copy`` if both are
960 # in-memory so we can use the fast path of ``util.copy`` if both are
961 # on disk.
961 # on disk.
962 if isinstance(fcd, context.overlayworkingfilectx):
962 if isinstance(fcd, context.overlayworkingfilectx):
963 util.writefile(back, fcd.data())
963 util.writefile(back, fcd.data())
964 else:
964 else:
965 a = _workingpath(repo, fcd)
965 a = _workingpath(repo, fcd)
966 util.copyfile(a, back)
966 util.copyfile(a, back)
967 # A arbitraryfilectx is returned, so we can run the same functions on
967 # A arbitraryfilectx is returned, so we can run the same functions on
968 # the backup context regardless of where it lives.
968 # the backup context regardless of where it lives.
969 return context.arbitraryfilectx(back, repo=repo)
969 return context.arbitraryfilectx(back, repo=repo)
970
970
971
971
972 @contextlib.contextmanager
972 @contextlib.contextmanager
973 def _maketempfiles(repo, fco, fca, localpath, uselocalpath):
973 def _maketempfiles(repo, fco, fca, localpath, uselocalpath):
974 """Writes out `fco` and `fca` as temporary files, and (if uselocalpath)
974 """Writes out `fco` and `fca` as temporary files, and (if uselocalpath)
975 copies `localpath` to another temporary file, so an external merge tool may
975 copies `localpath` to another temporary file, so an external merge tool may
976 use them.
976 use them.
977 """
977 """
978 tmproot = None
978 tmproot = None
979 tmprootprefix = repo.ui.config(b'experimental', b'mergetempdirprefix')
979 tmprootprefix = repo.ui.config(b'experimental', b'mergetempdirprefix')
980 if tmprootprefix:
980 if tmprootprefix:
981 tmproot = pycompat.mkdtemp(prefix=tmprootprefix)
981 tmproot = pycompat.mkdtemp(prefix=tmprootprefix)
982
982
983 def maketempfrompath(prefix, path):
983 def maketempfrompath(prefix, path):
984 fullbase, ext = os.path.splitext(path)
984 fullbase, ext = os.path.splitext(path)
985 pre = b"%s~%s" % (os.path.basename(fullbase), prefix)
985 pre = b"%s~%s" % (os.path.basename(fullbase), prefix)
986 if tmproot:
986 if tmproot:
987 name = os.path.join(tmproot, pre)
987 name = os.path.join(tmproot, pre)
988 if ext:
988 if ext:
989 name += ext
989 name += ext
990 f = open(name, "wb")
990 f = open(name, "wb")
991 else:
991 else:
992 fd, name = pycompat.mkstemp(prefix=pre + b'.', suffix=ext)
992 fd, name = pycompat.mkstemp(prefix=pre + b'.', suffix=ext)
993 f = os.fdopen(fd, "wb")
993 f = os.fdopen(fd, "wb")
994 return f, name
994 return f, name
995
995
996 def tempfromcontext(prefix, ctx):
996 def tempfromcontext(prefix, ctx):
997 f, name = maketempfrompath(prefix, ctx.path())
997 f, name = maketempfrompath(prefix, ctx.path())
998 data = repo.wwritedata(ctx.path(), ctx.data())
998 data = repo.wwritedata(ctx.path(), ctx.data())
999 f.write(data)
999 f.write(data)
1000 f.close()
1000 f.close()
1001 return name
1001 return name
1002
1002
1003 b = tempfromcontext(b"base", fca)
1003 b = tempfromcontext(b"base", fca)
1004 c = tempfromcontext(b"other", fco)
1004 c = tempfromcontext(b"other", fco)
1005 d = localpath
1005 d = localpath
1006 if uselocalpath:
1006 if uselocalpath:
1007 # We start off with this being the backup filename, so remove the .orig
1007 # We start off with this being the backup filename, so remove the .orig
1008 # to make syntax-highlighting more likely.
1008 # to make syntax-highlighting more likely.
1009 if d.endswith(b'.orig'):
1009 if d.endswith(b'.orig'):
1010 d, _ = os.path.splitext(d)
1010 d, _ = os.path.splitext(d)
1011 f, d = maketempfrompath(b"local", d)
1011 f, d = maketempfrompath(b"local", d)
1012 with open(localpath, b'rb') as src:
1012 with open(localpath, b'rb') as src:
1013 f.write(src.read())
1013 f.write(src.read())
1014 f.close()
1014 f.close()
1015
1015
1016 try:
1016 try:
1017 yield b, c, d
1017 yield b, c, d
1018 finally:
1018 finally:
1019 if tmproot:
1019 if tmproot:
1020 shutil.rmtree(tmproot)
1020 shutil.rmtree(tmproot)
1021 else:
1021 else:
1022 util.unlink(b)
1022 util.unlink(b)
1023 util.unlink(c)
1023 util.unlink(c)
1024 # if not uselocalpath, d is the 'orig'/backup file which we
1024 # if not uselocalpath, d is the 'orig'/backup file which we
1025 # shouldn't delete.
1025 # shouldn't delete.
1026 if d and uselocalpath:
1026 if d and uselocalpath:
1027 util.unlink(d)
1027 util.unlink(d)
1028
1028
1029
1029
1030 def _filemerge(premerge, repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
1030 def _filemerge(premerge, repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
1031 """perform a 3-way merge in the working directory
1031 """perform a 3-way merge in the working directory
1032
1032
1033 premerge = whether this is a premerge
1033 premerge = whether this is a premerge
1034 mynode = parent node before merge
1034 mynode = parent node before merge
1035 orig = original local filename before merge
1035 orig = original local filename before merge
1036 fco = other file context
1036 fco = other file context
1037 fca = ancestor file context
1037 fca = ancestor file context
1038 fcd = local file context for current/destination file
1038 fcd = local file context for current/destination file
1039
1039
1040 Returns whether the merge is complete, the return value of the merge, and
1040 Returns whether the merge is complete, the return value of the merge, and
1041 a boolean indicating whether the file was deleted from disk."""
1041 a boolean indicating whether the file was deleted from disk."""
1042
1042
1043 if not fco.cmp(fcd): # files identical?
1043 if not fco.cmp(fcd): # files identical?
1044 return True, None, False
1044 return True, None, False
1045
1045
1046 ui = repo.ui
1046 ui = repo.ui
1047 fd = fcd.path()
1047 fd = fcd.path()
1048 uipathfn = scmutil.getuipathfn(repo)
1048 uipathfn = scmutil.getuipathfn(repo)
1049 fduipath = uipathfn(fd)
1049 fduipath = uipathfn(fd)
1050 binary = fcd.isbinary() or fco.isbinary() or fca.isbinary()
1050 binary = fcd.isbinary() or fco.isbinary() or fca.isbinary()
1051 symlink = b'l' in fcd.flags() + fco.flags()
1051 symlink = b'l' in fcd.flags() + fco.flags()
1052 changedelete = fcd.isabsent() or fco.isabsent()
1052 changedelete = fcd.isabsent() or fco.isabsent()
1053 tool, toolpath = _picktool(repo, ui, fd, binary, symlink, changedelete)
1053 tool, toolpath = _picktool(repo, ui, fd, binary, symlink, changedelete)
1054 scriptfn = None
1054 scriptfn = None
1055 if tool in internals and tool.startswith(b'internal:'):
1055 if tool in internals and tool.startswith(b'internal:'):
1056 # normalize to new-style names (':merge' etc)
1056 # normalize to new-style names (':merge' etc)
1057 tool = tool[len(b'internal') :]
1057 tool = tool[len(b'internal') :]
1058 if toolpath and toolpath.startswith(b'python:'):
1058 if toolpath and toolpath.startswith(b'python:'):
1059 invalidsyntax = False
1059 invalidsyntax = False
1060 if toolpath.count(b':') >= 2:
1060 if toolpath.count(b':') >= 2:
1061 script, scriptfn = toolpath[7:].rsplit(b':', 1)
1061 script, scriptfn = toolpath[7:].rsplit(b':', 1)
1062 if not scriptfn:
1062 if not scriptfn:
1063 invalidsyntax = True
1063 invalidsyntax = True
1064 # missing :callable can lead to spliting on windows drive letter
1064 # missing :callable can lead to spliting on windows drive letter
1065 if b'\\' in scriptfn or b'/' in scriptfn:
1065 if b'\\' in scriptfn or b'/' in scriptfn:
1066 invalidsyntax = True
1066 invalidsyntax = True
1067 else:
1067 else:
1068 invalidsyntax = True
1068 invalidsyntax = True
1069 if invalidsyntax:
1069 if invalidsyntax:
1070 raise error.Abort(_(b"invalid 'python:' syntax: %s") % toolpath)
1070 raise error.Abort(_(b"invalid 'python:' syntax: %s") % toolpath)
1071 toolpath = script
1071 toolpath = script
1072 ui.debug(
1072 ui.debug(
1073 b"picked tool '%s' for %s (binary %s symlink %s changedelete %s)\n"
1073 b"picked tool '%s' for %s (binary %s symlink %s changedelete %s)\n"
1074 % (
1074 % (
1075 tool,
1075 tool,
1076 fduipath,
1076 fduipath,
1077 pycompat.bytestr(binary),
1077 pycompat.bytestr(binary),
1078 pycompat.bytestr(symlink),
1078 pycompat.bytestr(symlink),
1079 pycompat.bytestr(changedelete),
1079 pycompat.bytestr(changedelete),
1080 )
1080 )
1081 )
1081 )
1082
1082
1083 if tool in internals:
1083 if tool in internals:
1084 func = internals[tool]
1084 func = internals[tool]
1085 mergetype = func.mergetype
1085 mergetype = func.mergetype
1086 onfailure = func.onfailure
1086 onfailure = func.onfailure
1087 precheck = func.precheck
1087 precheck = func.precheck
1088 isexternal = False
1088 isexternal = False
1089 else:
1089 else:
1090 if wctx.isinmemory():
1090 if wctx.isinmemory():
1091 func = _xmergeimm
1091 func = _xmergeimm
1092 else:
1092 else:
1093 func = _xmerge
1093 func = _xmerge
1094 mergetype = fullmerge
1094 mergetype = fullmerge
1095 onfailure = _(b"merging %s failed!\n")
1095 onfailure = _(b"merging %s failed!\n")
1096 precheck = None
1096 precheck = None
1097 isexternal = True
1097 isexternal = True
1098
1098
1099 toolconf = tool, toolpath, binary, symlink, scriptfn
1099 toolconf = tool, toolpath, binary, symlink, scriptfn
1100
1100
1101 if mergetype == nomerge:
1101 if mergetype == nomerge:
1102 r, deleted = func(repo, mynode, orig, fcd, fco, fca, toolconf, labels)
1102 r, deleted = func(repo, mynode, orig, fcd, fco, fca, toolconf, labels)
1103 return True, r, deleted
1103 return True, r, deleted
1104
1104
1105 if premerge:
1105 if premerge:
1106 if orig != fco.path():
1106 if orig != fco.path():
1107 ui.status(
1107 ui.status(
1108 _(b"merging %s and %s to %s\n")
1108 _(b"merging %s and %s to %s\n")
1109 % (uipathfn(orig), uipathfn(fco.path()), fduipath)
1109 % (uipathfn(orig), uipathfn(fco.path()), fduipath)
1110 )
1110 )
1111 else:
1111 else:
1112 ui.status(_(b"merging %s\n") % fduipath)
1112 ui.status(_(b"merging %s\n") % fduipath)
1113
1113
1114 ui.debug(b"my %s other %s ancestor %s\n" % (fcd, fco, fca))
1114 ui.debug(b"my %s other %s ancestor %s\n" % (fcd, fco, fca))
1115
1115
1116 if precheck and not precheck(repo, mynode, orig, fcd, fco, fca, toolconf):
1116 if precheck and not precheck(repo, mynode, orig, fcd, fco, fca, toolconf):
1117 if onfailure:
1117 if onfailure:
1118 if wctx.isinmemory():
1118 if wctx.isinmemory():
1119 raise error.InMemoryMergeConflictsError(
1119 raise error.InMemoryMergeConflictsError(
1120 b'in-memory merge does not support merge conflicts'
1120 b'in-memory merge does not support merge conflicts'
1121 )
1121 )
1122 ui.warn(onfailure % fduipath)
1122 ui.warn(onfailure % fduipath)
1123 return True, 1, False
1123 return True, 1, False
1124
1124
1125 back = _makebackup(repo, ui, wctx, fcd, premerge)
1125 back = _makebackup(repo, ui, wctx, fcd, premerge)
1126 files = (None, None, None, back)
1126 files = (None, None, None, back)
1127 r = 1
1127 r = 1
1128 try:
1128 try:
1129 internalmarkerstyle = ui.config(b'ui', b'mergemarkers')
1129 internalmarkerstyle = ui.config(b'ui', b'mergemarkers')
1130 if isexternal:
1130 if isexternal:
1131 markerstyle = _toolstr(ui, tool, b'mergemarkers')
1131 markerstyle = _toolstr(ui, tool, b'mergemarkers')
1132 else:
1132 else:
1133 markerstyle = internalmarkerstyle
1133 markerstyle = internalmarkerstyle
1134
1134
1135 if not labels:
1135 if not labels:
1136 labels = _defaultconflictlabels
1136 labels = _defaultconflictlabels
1137 formattedlabels = labels
1137 formattedlabels = labels
1138 if markerstyle != b'basic':
1138 if markerstyle != b'basic':
1139 formattedlabels = _formatlabels(
1139 formattedlabels = _formatlabels(
1140 repo, fcd, fco, fca, labels, tool=tool
1140 repo, fcd, fco, fca, labels, tool=tool
1141 )
1141 )
1142
1142
1143 if premerge and mergetype == fullmerge:
1143 if premerge and mergetype == fullmerge:
1144 # conflict markers generated by premerge will use 'detailed'
1144 # conflict markers generated by premerge will use 'detailed'
1145 # settings if either ui.mergemarkers or the tool's mergemarkers
1145 # settings if either ui.mergemarkers or the tool's mergemarkers
1146 # setting is 'detailed'. This way tools can have basic labels in
1146 # setting is 'detailed'. This way tools can have basic labels in
1147 # space-constrained areas of the UI, but still get full information
1147 # space-constrained areas of the UI, but still get full information
1148 # in conflict markers if premerge is 'keep' or 'keep-merge3'.
1148 # in conflict markers if premerge is 'keep' or 'keep-merge3'.
1149 premergelabels = labels
1149 premergelabels = labels
1150 labeltool = None
1150 labeltool = None
1151 if markerstyle != b'basic':
1151 if markerstyle != b'basic':
1152 # respect 'tool's mergemarkertemplate (which defaults to
1152 # respect 'tool's mergemarkertemplate (which defaults to
1153 # command-templates.mergemarker)
1153 # command-templates.mergemarker)
1154 labeltool = tool
1154 labeltool = tool
1155 if internalmarkerstyle != b'basic' or markerstyle != b'basic':
1155 if internalmarkerstyle != b'basic' or markerstyle != b'basic':
1156 premergelabels = _formatlabels(
1156 premergelabels = _formatlabels(
1157 repo, fcd, fco, fca, premergelabels, tool=labeltool
1157 repo, fcd, fco, fca, premergelabels, tool=labeltool
1158 )
1158 )
1159
1159
1160 r = _premerge(
1160 r = _premerge(
1161 repo, fcd, fco, fca, toolconf, files, labels=premergelabels
1161 repo, fcd, fco, fca, toolconf, files, labels=premergelabels
1162 )
1162 )
1163 # complete if premerge successful (r is 0)
1163 # complete if premerge successful (r is 0)
1164 return not r, r, False
1164 return not r, r, False
1165
1165
1166 needcheck, r, deleted = func(
1166 needcheck, r, deleted = func(
1167 repo,
1167 repo,
1168 mynode,
1168 mynode,
1169 orig,
1169 orig,
1170 fcd,
1170 fcd,
1171 fco,
1171 fco,
1172 fca,
1172 fca,
1173 toolconf,
1173 toolconf,
1174 files,
1174 files,
1175 labels=formattedlabels,
1175 labels=formattedlabels,
1176 )
1176 )
1177
1177
1178 if needcheck:
1178 if needcheck:
1179 r = _check(repo, r, ui, tool, fcd, files)
1179 r = _check(repo, r, ui, tool, fcd, files)
1180
1180
1181 if r:
1181 if r:
1182 if onfailure:
1182 if onfailure:
1183 if wctx.isinmemory():
1183 if wctx.isinmemory():
1184 raise error.InMemoryMergeConflictsError(
1184 raise error.InMemoryMergeConflictsError(
1185 b'in-memory merge '
1185 b'in-memory merge '
1186 b'does not support '
1186 b'does not support '
1187 b'merge conflicts'
1187 b'merge conflicts'
1188 )
1188 )
1189 ui.warn(onfailure % fduipath)
1189 ui.warn(onfailure % fduipath)
1190 _onfilemergefailure(ui)
1190 _onfilemergefailure(ui)
1191
1191
1192 return True, r, deleted
1192 return True, r, deleted
1193 finally:
1193 finally:
1194 if not r and back is not None:
1194 if not r and back is not None:
1195 back.remove()
1195 back.remove()
1196
1196
1197
1197
1198 def _haltmerge():
1198 def _haltmerge():
1199 msg = _(b'merge halted after failed merge (see hg resolve)')
1199 msg = _(b'merge halted after failed merge (see hg resolve)')
1200 raise error.InterventionRequired(msg)
1200 raise error.InterventionRequired(msg)
1201
1201
1202
1202
1203 def _onfilemergefailure(ui):
1203 def _onfilemergefailure(ui):
1204 action = ui.config(b'merge', b'on-failure')
1204 action = ui.config(b'merge', b'on-failure')
1205 if action == b'prompt':
1205 if action == b'prompt':
1206 msg = _(b'continue merge operation (yn)?$$ &Yes $$ &No')
1206 msg = _(b'continue merge operation (yn)?$$ &Yes $$ &No')
1207 if ui.promptchoice(msg, 0) == 1:
1207 if ui.promptchoice(msg, 0) == 1:
1208 _haltmerge()
1208 _haltmerge()
1209 if action == b'halt':
1209 if action == b'halt':
1210 _haltmerge()
1210 _haltmerge()
1211 # default action is 'continue', in which case we neither prompt nor halt
1211 # default action is 'continue', in which case we neither prompt nor halt
1212
1212
1213
1213
1214 def hasconflictmarkers(data):
1214 def hasconflictmarkers(data):
1215 # Detect lines starting with a string of 7 identical characters from the
1216 # subset Mercurial uses for conflict markers, followed by either the end of
1217 # line or a space and some text. Note that using [<>=+|-]{7} would detect
1218 # `<><><><><` as a conflict marker, which we don't want.
1215 return bool(
1219 return bool(
1216 re.search(
1220 re.search(
1217 br"^(<<<<<<<.*|=======.*|------- .*|\+\+\+\+\+\+\+ .*|>>>>>>>.*)$",
1221 br"^([<>=+|-])\1{6}( .*)$",
1218 data,
1222 data,
1219 re.MULTILINE,
1223 re.MULTILINE,
1220 )
1224 )
1221 )
1225 )
1222
1226
1223
1227
1224 def _check(repo, r, ui, tool, fcd, files):
1228 def _check(repo, r, ui, tool, fcd, files):
1225 fd = fcd.path()
1229 fd = fcd.path()
1226 uipathfn = scmutil.getuipathfn(repo)
1230 uipathfn = scmutil.getuipathfn(repo)
1227 unused, unused, unused, back = files
1231 unused, unused, unused, back = files
1228
1232
1229 if not r and (
1233 if not r and (
1230 _toolbool(ui, tool, b"checkconflicts")
1234 _toolbool(ui, tool, b"checkconflicts")
1231 or b'conflicts' in _toollist(ui, tool, b"check")
1235 or b'conflicts' in _toollist(ui, tool, b"check")
1232 ):
1236 ):
1233 if hasconflictmarkers(fcd.data()):
1237 if hasconflictmarkers(fcd.data()):
1234 r = 1
1238 r = 1
1235
1239
1236 checked = False
1240 checked = False
1237 if b'prompt' in _toollist(ui, tool, b"check"):
1241 if b'prompt' in _toollist(ui, tool, b"check"):
1238 checked = True
1242 checked = True
1239 if ui.promptchoice(
1243 if ui.promptchoice(
1240 _(b"was merge of '%s' successful (yn)?$$ &Yes $$ &No")
1244 _(b"was merge of '%s' successful (yn)?$$ &Yes $$ &No")
1241 % uipathfn(fd),
1245 % uipathfn(fd),
1242 1,
1246 1,
1243 ):
1247 ):
1244 r = 1
1248 r = 1
1245
1249
1246 if (
1250 if (
1247 not r
1251 not r
1248 and not checked
1252 and not checked
1249 and (
1253 and (
1250 _toolbool(ui, tool, b"checkchanged")
1254 _toolbool(ui, tool, b"checkchanged")
1251 or b'changed' in _toollist(ui, tool, b"check")
1255 or b'changed' in _toollist(ui, tool, b"check")
1252 )
1256 )
1253 ):
1257 ):
1254 if back is not None and not fcd.cmp(back):
1258 if back is not None and not fcd.cmp(back):
1255 if ui.promptchoice(
1259 if ui.promptchoice(
1256 _(
1260 _(
1257 b" output file %s appears unchanged\n"
1261 b" output file %s appears unchanged\n"
1258 b"was merge successful (yn)?"
1262 b"was merge successful (yn)?"
1259 b"$$ &Yes $$ &No"
1263 b"$$ &Yes $$ &No"
1260 )
1264 )
1261 % uipathfn(fd),
1265 % uipathfn(fd),
1262 1,
1266 1,
1263 ):
1267 ):
1264 r = 1
1268 r = 1
1265
1269
1266 if back is not None and _toolbool(ui, tool, b"fixeol"):
1270 if back is not None and _toolbool(ui, tool, b"fixeol"):
1267 _matcheol(_workingpath(repo, fcd), back)
1271 _matcheol(_workingpath(repo, fcd), back)
1268
1272
1269 return r
1273 return r
1270
1274
1271
1275
1272 def _workingpath(repo, ctx):
1276 def _workingpath(repo, ctx):
1273 return repo.wjoin(ctx.path())
1277 return repo.wjoin(ctx.path())
1274
1278
1275
1279
1276 def premerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
1280 def premerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
1277 return _filemerge(
1281 return _filemerge(
1278 True, repo, wctx, mynode, orig, fcd, fco, fca, labels=labels
1282 True, repo, wctx, mynode, orig, fcd, fco, fca, labels=labels
1279 )
1283 )
1280
1284
1281
1285
1282 def filemerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
1286 def filemerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
1283 return _filemerge(
1287 return _filemerge(
1284 False, repo, wctx, mynode, orig, fcd, fco, fca, labels=labels
1288 False, repo, wctx, mynode, orig, fcd, fco, fca, labels=labels
1285 )
1289 )
1286
1290
1287
1291
1288 def loadinternalmerge(ui, extname, registrarobj):
1292 def loadinternalmerge(ui, extname, registrarobj):
1289 """Load internal merge tool from specified registrarobj"""
1293 """Load internal merge tool from specified registrarobj"""
1290 for name, func in pycompat.iteritems(registrarobj._table):
1294 for name, func in pycompat.iteritems(registrarobj._table):
1291 fullname = b':' + name
1295 fullname = b':' + name
1292 internals[fullname] = func
1296 internals[fullname] = func
1293 internals[b'internal:' + name] = func
1297 internals[b'internal:' + name] = func
1294 internalsdoc[fullname] = func
1298 internalsdoc[fullname] = func
1295
1299
1296 capabilities = sorted([k for k, v in func.capabilities.items() if v])
1300 capabilities = sorted([k for k, v in func.capabilities.items() if v])
1297 if capabilities:
1301 if capabilities:
1298 capdesc = b" (actual capabilities: %s)" % b', '.join(
1302 capdesc = b" (actual capabilities: %s)" % b', '.join(
1299 capabilities
1303 capabilities
1300 )
1304 )
1301 func.__doc__ = func.__doc__ + pycompat.sysstr(b"\n\n%s" % capdesc)
1305 func.__doc__ = func.__doc__ + pycompat.sysstr(b"\n\n%s" % capdesc)
1302
1306
1303 # to put i18n comments into hg.pot for automatically generated texts
1307 # to put i18n comments into hg.pot for automatically generated texts
1304
1308
1305 # i18n: "binary" and "symlink" are keywords
1309 # i18n: "binary" and "symlink" are keywords
1306 # i18n: this text is added automatically
1310 # i18n: this text is added automatically
1307 _(b" (actual capabilities: binary, symlink)")
1311 _(b" (actual capabilities: binary, symlink)")
1308 # i18n: "binary" is keyword
1312 # i18n: "binary" is keyword
1309 # i18n: this text is added automatically
1313 # i18n: this text is added automatically
1310 _(b" (actual capabilities: binary)")
1314 _(b" (actual capabilities: binary)")
1311 # i18n: "symlink" is keyword
1315 # i18n: "symlink" is keyword
1312 # i18n: this text is added automatically
1316 # i18n: this text is added automatically
1313 _(b" (actual capabilities: symlink)")
1317 _(b" (actual capabilities: symlink)")
1314
1318
1315
1319
1316 # load built-in merge tools explicitly to setup internalsdoc
1320 # load built-in merge tools explicitly to setup internalsdoc
1317 loadinternalmerge(None, None, internaltool)
1321 loadinternalmerge(None, None, internaltool)
1318
1322
1319 # tell hggettext to extract docstrings from these functions:
1323 # tell hggettext to extract docstrings from these functions:
1320 i18nfunctions = internals.values()
1324 i18nfunctions = internals.values()
General Comments 0
You need to be logged in to leave comments. Login now