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