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