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