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