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