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