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