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