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