##// END OF EJS Templates
filemerge: use new function for getting first line of string...
Martin von Zweigbergk -
r49892:db93041e default
parent child Browse files
Show More
@@ -1,1299 +1,1299
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 = templateresult.splitlines()[0] # split for safety
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 # Tuples of (order, name, executable path, args)
1122 # Tuples of (order, name, executable path, args)
1123 tools = []
1123 tools = []
1124 seen = set()
1124 seen = set()
1125 section = b"partial-merge-tools"
1125 section = b"partial-merge-tools"
1126 for k, v in ui.configitems(section):
1126 for k, v in ui.configitems(section):
1127 name = k.split(b'.')[0]
1127 name = k.split(b'.')[0]
1128 if name in seen:
1128 if name in seen:
1129 continue
1129 continue
1130 patterns = ui.configlist(section, b'%s.patterns' % name, [])
1130 patterns = ui.configlist(section, b'%s.patterns' % name, [])
1131 is_match = True
1131 is_match = True
1132 if patterns:
1132 if patterns:
1133 m = match.match(repo.root, b'', patterns)
1133 m = match.match(repo.root, b'', patterns)
1134 is_match = m(local.fctx.path())
1134 is_match = m(local.fctx.path())
1135 if is_match:
1135 if is_match:
1136 order = ui.configint(section, b'%s.order' % name, 0)
1136 order = ui.configint(section, b'%s.order' % name, 0)
1137 executable = ui.config(section, b'%s.executable' % name, name)
1137 executable = ui.config(section, b'%s.executable' % name, name)
1138 args = ui.config(section, b'%s.args' % name)
1138 args = ui.config(section, b'%s.args' % name)
1139 tools.append((order, name, executable, args))
1139 tools.append((order, name, executable, args))
1140
1140
1141 if not tools:
1141 if not tools:
1142 return
1142 return
1143 # Sort in configured order (first in tuple)
1143 # Sort in configured order (first in tuple)
1144 tools.sort()
1144 tools.sort()
1145
1145
1146 files = [
1146 files = [
1147 (b"local", local.fctx.path(), local.text()),
1147 (b"local", local.fctx.path(), local.text()),
1148 (b"base", base.fctx.path(), base.text()),
1148 (b"base", base.fctx.path(), base.text()),
1149 (b"other", other.fctx.path(), other.text()),
1149 (b"other", other.fctx.path(), other.text()),
1150 ]
1150 ]
1151
1151
1152 with _maketempfiles(files) as temppaths:
1152 with _maketempfiles(files) as temppaths:
1153 localpath, basepath, otherpath = temppaths
1153 localpath, basepath, otherpath = temppaths
1154
1154
1155 for order, name, executable, args in tools:
1155 for order, name, executable, args in tools:
1156 cmd = procutil.shellquote(executable)
1156 cmd = procutil.shellquote(executable)
1157 replace = {
1157 replace = {
1158 b'local': localpath,
1158 b'local': localpath,
1159 b'base': basepath,
1159 b'base': basepath,
1160 b'other': otherpath,
1160 b'other': otherpath,
1161 }
1161 }
1162 args = util.interpolate(
1162 args = util.interpolate(
1163 br'\$',
1163 br'\$',
1164 replace,
1164 replace,
1165 args,
1165 args,
1166 lambda s: procutil.shellquote(util.localpath(s)),
1166 lambda s: procutil.shellquote(util.localpath(s)),
1167 )
1167 )
1168
1168
1169 cmd = b'%s %s' % (cmd, args)
1169 cmd = b'%s %s' % (cmd, args)
1170 r = ui.system(cmd, cwd=repo.root, blockedtag=b'partial-mergetool')
1170 r = ui.system(cmd, cwd=repo.root, blockedtag=b'partial-mergetool')
1171 if r:
1171 if r:
1172 raise error.StateError(
1172 raise error.StateError(
1173 b'partial merge tool %s exited with code %d' % (name, r)
1173 b'partial merge tool %s exited with code %d' % (name, r)
1174 )
1174 )
1175 local_text = util.readfile(localpath)
1175 local_text = util.readfile(localpath)
1176 other_text = util.readfile(otherpath)
1176 other_text = util.readfile(otherpath)
1177 if local_text == other_text:
1177 if local_text == other_text:
1178 # No need to run other tools if all conflicts have been resolved
1178 # No need to run other tools if all conflicts have been resolved
1179 break
1179 break
1180
1180
1181 local.set_text(local_text)
1181 local.set_text(local_text)
1182 base.set_text(util.readfile(basepath))
1182 base.set_text(util.readfile(basepath))
1183 other.set_text(other_text)
1183 other.set_text(other_text)
1184
1184
1185
1185
1186 def _haltmerge():
1186 def _haltmerge():
1187 msg = _(b'merge halted after failed merge (see hg resolve)')
1187 msg = _(b'merge halted after failed merge (see hg resolve)')
1188 raise error.InterventionRequired(msg)
1188 raise error.InterventionRequired(msg)
1189
1189
1190
1190
1191 def _onfilemergefailure(ui):
1191 def _onfilemergefailure(ui):
1192 action = ui.config(b'merge', b'on-failure')
1192 action = ui.config(b'merge', b'on-failure')
1193 if action == b'prompt':
1193 if action == b'prompt':
1194 msg = _(b'continue merge operation (yn)?$$ &Yes $$ &No')
1194 msg = _(b'continue merge operation (yn)?$$ &Yes $$ &No')
1195 if ui.promptchoice(msg, 0) == 1:
1195 if ui.promptchoice(msg, 0) == 1:
1196 _haltmerge()
1196 _haltmerge()
1197 if action == b'halt':
1197 if action == b'halt':
1198 _haltmerge()
1198 _haltmerge()
1199 # default action is 'continue', in which case we neither prompt nor halt
1199 # default action is 'continue', in which case we neither prompt nor halt
1200
1200
1201
1201
1202 def hasconflictmarkers(data):
1202 def hasconflictmarkers(data):
1203 # Detect lines starting with a string of 7 identical characters from the
1203 # Detect lines starting with a string of 7 identical characters from the
1204 # subset Mercurial uses for conflict markers, followed by either the end of
1204 # subset Mercurial uses for conflict markers, followed by either the end of
1205 # line or a space and some text. Note that using [<>=+|-]{7} would detect
1205 # line or a space and some text. Note that using [<>=+|-]{7} would detect
1206 # `<><><><><` as a conflict marker, which we don't want.
1206 # `<><><><><` as a conflict marker, which we don't want.
1207 return bool(
1207 return bool(
1208 re.search(
1208 re.search(
1209 br"^([<>=+|-])\1{6}( .*)$",
1209 br"^([<>=+|-])\1{6}( .*)$",
1210 data,
1210 data,
1211 re.MULTILINE,
1211 re.MULTILINE,
1212 )
1212 )
1213 )
1213 )
1214
1214
1215
1215
1216 def _check(repo, r, ui, tool, fcd, backup):
1216 def _check(repo, r, ui, tool, fcd, backup):
1217 fd = fcd.path()
1217 fd = fcd.path()
1218 uipathfn = scmutil.getuipathfn(repo)
1218 uipathfn = scmutil.getuipathfn(repo)
1219
1219
1220 if not r and (
1220 if not r and (
1221 _toolbool(ui, tool, b"checkconflicts")
1221 _toolbool(ui, tool, b"checkconflicts")
1222 or b'conflicts' in _toollist(ui, tool, b"check")
1222 or b'conflicts' in _toollist(ui, tool, b"check")
1223 ):
1223 ):
1224 if hasconflictmarkers(fcd.data()):
1224 if hasconflictmarkers(fcd.data()):
1225 r = 1
1225 r = 1
1226
1226
1227 checked = False
1227 checked = False
1228 if b'prompt' in _toollist(ui, tool, b"check"):
1228 if b'prompt' in _toollist(ui, tool, b"check"):
1229 checked = True
1229 checked = True
1230 if ui.promptchoice(
1230 if ui.promptchoice(
1231 _(b"was merge of '%s' successful (yn)?$$ &Yes $$ &No")
1231 _(b"was merge of '%s' successful (yn)?$$ &Yes $$ &No")
1232 % uipathfn(fd),
1232 % uipathfn(fd),
1233 1,
1233 1,
1234 ):
1234 ):
1235 r = 1
1235 r = 1
1236
1236
1237 if (
1237 if (
1238 not r
1238 not r
1239 and not checked
1239 and not checked
1240 and (
1240 and (
1241 _toolbool(ui, tool, b"checkchanged")
1241 _toolbool(ui, tool, b"checkchanged")
1242 or b'changed' in _toollist(ui, tool, b"check")
1242 or b'changed' in _toollist(ui, tool, b"check")
1243 )
1243 )
1244 ):
1244 ):
1245 if backup is not None and not fcd.cmp(backup):
1245 if backup is not None and not fcd.cmp(backup):
1246 if ui.promptchoice(
1246 if ui.promptchoice(
1247 _(
1247 _(
1248 b" output file %s appears unchanged\n"
1248 b" output file %s appears unchanged\n"
1249 b"was merge successful (yn)?"
1249 b"was merge successful (yn)?"
1250 b"$$ &Yes $$ &No"
1250 b"$$ &Yes $$ &No"
1251 )
1251 )
1252 % uipathfn(fd),
1252 % uipathfn(fd),
1253 1,
1253 1,
1254 ):
1254 ):
1255 r = 1
1255 r = 1
1256
1256
1257 if backup is not None and _toolbool(ui, tool, b"fixeol"):
1257 if backup is not None and _toolbool(ui, tool, b"fixeol"):
1258 _matcheol(_workingpath(repo, fcd), backup)
1258 _matcheol(_workingpath(repo, fcd), backup)
1259
1259
1260 return r
1260 return r
1261
1261
1262
1262
1263 def _workingpath(repo, ctx):
1263 def _workingpath(repo, ctx):
1264 return repo.wjoin(ctx.path())
1264 return repo.wjoin(ctx.path())
1265
1265
1266
1266
1267 def loadinternalmerge(ui, extname, registrarobj):
1267 def loadinternalmerge(ui, extname, registrarobj):
1268 """Load internal merge tool from specified registrarobj"""
1268 """Load internal merge tool from specified registrarobj"""
1269 for name, func in registrarobj._table.items():
1269 for name, func in registrarobj._table.items():
1270 fullname = b':' + name
1270 fullname = b':' + name
1271 internals[fullname] = func
1271 internals[fullname] = func
1272 internals[b'internal:' + name] = func
1272 internals[b'internal:' + name] = func
1273 internalsdoc[fullname] = func
1273 internalsdoc[fullname] = func
1274
1274
1275 capabilities = sorted([k for k, v in func.capabilities.items() if v])
1275 capabilities = sorted([k for k, v in func.capabilities.items() if v])
1276 if capabilities:
1276 if capabilities:
1277 capdesc = b" (actual capabilities: %s)" % b', '.join(
1277 capdesc = b" (actual capabilities: %s)" % b', '.join(
1278 capabilities
1278 capabilities
1279 )
1279 )
1280 func.__doc__ = func.__doc__ + pycompat.sysstr(b"\n\n%s" % capdesc)
1280 func.__doc__ = func.__doc__ + pycompat.sysstr(b"\n\n%s" % capdesc)
1281
1281
1282 # to put i18n comments into hg.pot for automatically generated texts
1282 # to put i18n comments into hg.pot for automatically generated texts
1283
1283
1284 # i18n: "binary" and "symlink" are keywords
1284 # i18n: "binary" and "symlink" are keywords
1285 # i18n: this text is added automatically
1285 # i18n: this text is added automatically
1286 _(b" (actual capabilities: binary, symlink)")
1286 _(b" (actual capabilities: binary, symlink)")
1287 # i18n: "binary" is keyword
1287 # i18n: "binary" is keyword
1288 # i18n: this text is added automatically
1288 # i18n: this text is added automatically
1289 _(b" (actual capabilities: binary)")
1289 _(b" (actual capabilities: binary)")
1290 # i18n: "symlink" is keyword
1290 # i18n: "symlink" is keyword
1291 # i18n: this text is added automatically
1291 # i18n: this text is added automatically
1292 _(b" (actual capabilities: symlink)")
1292 _(b" (actual capabilities: symlink)")
1293
1293
1294
1294
1295 # load built-in merge tools explicitly to setup internalsdoc
1295 # load built-in merge tools explicitly to setup internalsdoc
1296 loadinternalmerge(None, None, internaltool)
1296 loadinternalmerge(None, None, internaltool)
1297
1297
1298 # tell hggettext to extract docstrings from these functions:
1298 # tell hggettext to extract docstrings from these functions:
1299 i18nfunctions = internals.values()
1299 i18nfunctions = internals.values()
General Comments 0
You need to be logged in to leave comments. Login now