##// END OF EJS Templates
filemerge: move removal of `.orig` extension on temp file close to context...
Martin von Zweigbergk -
r49634:b53f2f5a default
parent child Browse files
Show More
@@ -1,1247 +1,1246 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, 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
746 localoutputpath = None
747 if b"$output" in args:
747 if b"$output" in args:
748 localoutputpath = backup.path()
748 localoutputpath = backup.path()
749 # Remove the .orig to make syntax-highlighting more likely.
750 if localoutputpath.endswith(b'.orig'):
751 localoutputpath, ext = os.path.splitext(localoutputpath)
749
752
750 with _maketempfiles(
753 with _maketempfiles(
751 fco,
754 fco,
752 fca,
755 fca,
753 localoutputpath,
756 localoutputpath,
754 ) as temppaths:
757 ) as temppaths:
755 basepath, otherpath, localoutputpath = temppaths
758 basepath, otherpath, localoutputpath = temppaths
756 outpath = b""
759 outpath = b""
757
760
758 def format_label(input):
761 def format_label(input):
759 if input.label_detail:
762 if input.label_detail:
760 return b'%s: %s' % (input.label, input.label_detail)
763 return b'%s: %s' % (input.label, input.label_detail)
761 else:
764 else:
762 return input.label
765 return input.label
763
766
764 env = {
767 env = {
765 b'HG_FILE': fcd.path(),
768 b'HG_FILE': fcd.path(),
766 b'HG_MY_NODE': short(mynode),
769 b'HG_MY_NODE': short(mynode),
767 b'HG_OTHER_NODE': short(fco.changectx().node()),
770 b'HG_OTHER_NODE': short(fco.changectx().node()),
768 b'HG_BASE_NODE': short(fca.changectx().node()),
771 b'HG_BASE_NODE': short(fca.changectx().node()),
769 b'HG_MY_ISLINK': b'l' in fcd.flags(),
772 b'HG_MY_ISLINK': b'l' in fcd.flags(),
770 b'HG_OTHER_ISLINK': b'l' in fco.flags(),
773 b'HG_OTHER_ISLINK': b'l' in fco.flags(),
771 b'HG_BASE_ISLINK': b'l' in fca.flags(),
774 b'HG_BASE_ISLINK': b'l' in fca.flags(),
772 b'HG_MY_LABEL': format_label(local),
775 b'HG_MY_LABEL': format_label(local),
773 b'HG_OTHER_LABEL': format_label(other),
776 b'HG_OTHER_LABEL': format_label(other),
774 b'HG_BASE_LABEL': format_label(base),
777 b'HG_BASE_LABEL': format_label(base),
775 }
778 }
776 ui = repo.ui
779 ui = repo.ui
777
780
778 if b"$output" in args:
781 if b"$output" in args:
779 # read input from backup, write to original
782 # read input from backup, write to original
780 outpath = localpath
783 outpath = localpath
781 localpath = localoutputpath
784 localpath = localoutputpath
782 replace = {
785 replace = {
783 b'local': localpath,
786 b'local': localpath,
784 b'base': basepath,
787 b'base': basepath,
785 b'other': otherpath,
788 b'other': otherpath,
786 b'output': outpath,
789 b'output': outpath,
787 b'labellocal': format_label(local),
790 b'labellocal': format_label(local),
788 b'labelother': format_label(other),
791 b'labelother': format_label(other),
789 b'labelbase': format_label(base),
792 b'labelbase': format_label(base),
790 }
793 }
791 args = util.interpolate(
794 args = util.interpolate(
792 br'\$',
795 br'\$',
793 replace,
796 replace,
794 args,
797 args,
795 lambda s: procutil.shellquote(util.localpath(s)),
798 lambda s: procutil.shellquote(util.localpath(s)),
796 )
799 )
797 if _toolbool(ui, tool, b"gui"):
800 if _toolbool(ui, tool, b"gui"):
798 repo.ui.status(
801 repo.ui.status(
799 _(b'running merge tool %s for file %s\n')
802 _(b'running merge tool %s for file %s\n')
800 % (tool, uipathfn(fcd.path()))
803 % (tool, uipathfn(fcd.path()))
801 )
804 )
802 if scriptfn is None:
805 if scriptfn is None:
803 cmd = toolpath + b' ' + args
806 cmd = toolpath + b' ' + args
804 repo.ui.debug(b'launching merge tool: %s\n' % cmd)
807 repo.ui.debug(b'launching merge tool: %s\n' % cmd)
805 _describemerge(ui, repo, mynode, fcd, fca, fco, env, toolpath, args)
808 _describemerge(ui, repo, mynode, fcd, fca, fco, env, toolpath, args)
806 r = ui.system(
809 r = ui.system(
807 cmd, cwd=repo.root, environ=env, blockedtag=b'mergetool'
810 cmd, cwd=repo.root, environ=env, blockedtag=b'mergetool'
808 )
811 )
809 else:
812 else:
810 repo.ui.debug(
813 repo.ui.debug(
811 b'launching python merge script: %s:%s\n' % (toolpath, scriptfn)
814 b'launching python merge script: %s:%s\n' % (toolpath, scriptfn)
812 )
815 )
813 r = 0
816 r = 0
814 try:
817 try:
815 # avoid cycle cmdutil->merge->filemerge->extensions->cmdutil
818 # avoid cycle cmdutil->merge->filemerge->extensions->cmdutil
816 from . import extensions
819 from . import extensions
817
820
818 mod = extensions.loadpath(toolpath, b'hgmerge.%s' % tool)
821 mod = extensions.loadpath(toolpath, b'hgmerge.%s' % tool)
819 except Exception:
822 except Exception:
820 raise error.Abort(
823 raise error.Abort(
821 _(b"loading python merge script failed: %s") % toolpath
824 _(b"loading python merge script failed: %s") % toolpath
822 )
825 )
823 mergefn = getattr(mod, scriptfn, None)
826 mergefn = getattr(mod, scriptfn, None)
824 if mergefn is None:
827 if mergefn is None:
825 raise error.Abort(
828 raise error.Abort(
826 _(b"%s does not have function: %s") % (toolpath, scriptfn)
829 _(b"%s does not have function: %s") % (toolpath, scriptfn)
827 )
830 )
828 argslist = procutil.shellsplit(args)
831 argslist = procutil.shellsplit(args)
829 # avoid cycle cmdutil->merge->filemerge->hook->extensions->cmdutil
832 # avoid cycle cmdutil->merge->filemerge->hook->extensions->cmdutil
830 from . import hook
833 from . import hook
831
834
832 ret, raised = hook.pythonhook(
835 ret, raised = hook.pythonhook(
833 ui, repo, b"merge", toolpath, mergefn, {b'args': argslist}, True
836 ui, repo, b"merge", toolpath, mergefn, {b'args': argslist}, True
834 )
837 )
835 if raised:
838 if raised:
836 r = 1
839 r = 1
837 repo.ui.debug(b'merge tool returned: %d\n' % r)
840 repo.ui.debug(b'merge tool returned: %d\n' % r)
838 return True, r, False
841 return True, r, False
839
842
840
843
841 def _populate_label_detail(input, template):
844 def _populate_label_detail(input, template):
842 """Applies the given template to the ctx and stores it in the input."""
845 """Applies the given template to the ctx and stores it in the input."""
843 ctx = input.fctx.changectx()
846 ctx = input.fctx.changectx()
844 if ctx.node() is None:
847 if ctx.node() is None:
845 ctx = ctx.p1()
848 ctx = ctx.p1()
846
849
847 props = {b'ctx': ctx}
850 props = {b'ctx': ctx}
848 templateresult = template.renderdefault(props)
851 templateresult = template.renderdefault(props)
849 input.label_detail = templateresult.splitlines()[0] # split for safety
852 input.label_detail = templateresult.splitlines()[0] # split for safety
850
853
851
854
852 def _populate_label_details(repo, inputs, tool=None):
855 def _populate_label_details(repo, inputs, tool=None):
853 """Populates the label details using the conflict marker template."""
856 """Populates the label details using the conflict marker template."""
854 ui = repo.ui
857 ui = repo.ui
855 template = ui.config(b'command-templates', b'mergemarker')
858 template = ui.config(b'command-templates', b'mergemarker')
856 if tool is not None:
859 if tool is not None:
857 template = _toolstr(ui, tool, b'mergemarkertemplate', template)
860 template = _toolstr(ui, tool, b'mergemarkertemplate', template)
858 template = templater.unquotestring(template)
861 template = templater.unquotestring(template)
859 tres = formatter.templateresources(ui, repo)
862 tres = formatter.templateresources(ui, repo)
860 tmpl = formatter.maketemplater(
863 tmpl = formatter.maketemplater(
861 ui, template, defaults=templatekw.keywords, resources=tres
864 ui, template, defaults=templatekw.keywords, resources=tres
862 )
865 )
863
866
864 for input in inputs:
867 for input in inputs:
865 _populate_label_detail(input, tmpl)
868 _populate_label_detail(input, tmpl)
866
869
867
870
868 def partextras(labels):
871 def partextras(labels):
869 """Return a dictionary of extra labels for use in prompts to the user
872 """Return a dictionary of extra labels for use in prompts to the user
870
873
871 Intended use is in strings of the form "(l)ocal%(l)s".
874 Intended use is in strings of the form "(l)ocal%(l)s".
872 """
875 """
873 if labels is None:
876 if labels is None:
874 return {
877 return {
875 b"l": b"",
878 b"l": b"",
876 b"o": b"",
879 b"o": b"",
877 }
880 }
878
881
879 return {
882 return {
880 b"l": b" [%s]" % labels[0],
883 b"l": b" [%s]" % labels[0],
881 b"o": b" [%s]" % labels[1],
884 b"o": b" [%s]" % labels[1],
882 }
885 }
883
886
884
887
885 def _makebackup(repo, ui, fcd):
888 def _makebackup(repo, ui, fcd):
886 """Makes and returns a filectx-like object for ``fcd``'s backup file.
889 """Makes and returns a filectx-like object for ``fcd``'s backup file.
887
890
888 In addition to preserving the user's pre-existing modifications to `fcd`
891 In addition to preserving the user's pre-existing modifications to `fcd`
889 (if any), the backup is used to undo certain premerges, confirm whether a
892 (if any), the backup is used to undo certain premerges, confirm whether a
890 merge changed anything, and determine what line endings the new file should
893 merge changed anything, and determine what line endings the new file should
891 have.
894 have.
892
895
893 Backups only need to be written once since their content doesn't change
896 Backups only need to be written once since their content doesn't change
894 afterwards.
897 afterwards.
895 """
898 """
896 if fcd.isabsent():
899 if fcd.isabsent():
897 return None
900 return None
898 # TODO: Break this import cycle somehow. (filectx -> ctx -> fileset ->
901 # TODO: Break this import cycle somehow. (filectx -> ctx -> fileset ->
899 # merge -> filemerge). (I suspect the fileset import is the weakest link)
902 # merge -> filemerge). (I suspect the fileset import is the weakest link)
900 from . import context
903 from . import context
901
904
902 if isinstance(fcd, context.overlayworkingfilectx):
905 if isinstance(fcd, context.overlayworkingfilectx):
903 # If we're merging in-memory, we're free to put the backup anywhere.
906 # If we're merging in-memory, we're free to put the backup anywhere.
904 fd, backup = pycompat.mkstemp(b'hg-merge-backup')
907 fd, backup = pycompat.mkstemp(b'hg-merge-backup')
905 with os.fdopen(fd, 'wb') as f:
908 with os.fdopen(fd, 'wb') as f:
906 f.write(fcd.data())
909 f.write(fcd.data())
907 else:
910 else:
908 backup = scmutil.backuppath(ui, repo, fcd.path())
911 backup = scmutil.backuppath(ui, repo, fcd.path())
909 a = _workingpath(repo, fcd)
912 a = _workingpath(repo, fcd)
910 util.copyfile(a, backup)
913 util.copyfile(a, backup)
911
914
912 return context.arbitraryfilectx(backup, repo=repo)
915 return context.arbitraryfilectx(backup, repo=repo)
913
916
914
917
915 @contextlib.contextmanager
918 @contextlib.contextmanager
916 def _maketempfiles(fco, fca, localpath):
919 def _maketempfiles(fco, fca, localpath):
917 """Writes out `fco` and `fca` as temporary files, and (if localpath is not
920 """Writes out `fco` and `fca` as temporary files, and (if localpath is not
918 None) copies `localpath` to another temporary file, so an external merge
921 None) copies `localpath` to another temporary file, so an external merge
919 tool may use them.
922 tool may use them.
920 """
923 """
921 tmproot = pycompat.mkdtemp(prefix=b'hgmerge-')
924 tmproot = pycompat.mkdtemp(prefix=b'hgmerge-')
922
925
923 def maketempfrompath(prefix, path):
926 def maketempfrompath(prefix, path):
924 fullbase, ext = os.path.splitext(path)
927 fullbase, ext = os.path.splitext(path)
925 pre = b"%s~%s" % (os.path.basename(fullbase), prefix)
928 pre = b"%s~%s" % (os.path.basename(fullbase), prefix)
926 name = os.path.join(tmproot, pre)
929 name = os.path.join(tmproot, pre)
927 if ext:
930 if ext:
928 name += ext
931 name += ext
929 f = open(name, "wb")
932 f = open(name, "wb")
930 return f, name
933 return f, name
931
934
932 def tempfromcontext(prefix, ctx):
935 def tempfromcontext(prefix, ctx):
933 f, name = maketempfrompath(prefix, ctx.path())
936 f, name = maketempfrompath(prefix, ctx.path())
934 data = ctx.decodeddata()
937 data = ctx.decodeddata()
935 f.write(data)
938 f.write(data)
936 f.close()
939 f.close()
937 return name
940 return name
938
941
939 b = tempfromcontext(b"base", fca)
942 b = tempfromcontext(b"base", fca)
940 c = tempfromcontext(b"other", fco)
943 c = tempfromcontext(b"other", fco)
941 d = localpath
944 d = localpath
942 if localpath is not None:
945 if localpath is not None:
943 # We start off with this being the backup filename, so remove the .orig
944 # to make syntax-highlighting more likely.
945 if d.endswith(b'.orig'):
946 d, _ = os.path.splitext(d)
947 f, d = maketempfrompath(b"local", d)
946 f, d = maketempfrompath(b"local", d)
948 with open(localpath, b'rb') as src:
947 with open(localpath, b'rb') as src:
949 f.write(src.read())
948 f.write(src.read())
950 f.close()
949 f.close()
951
950
952 try:
951 try:
953 yield b, c, d
952 yield b, c, d
954 finally:
953 finally:
955 shutil.rmtree(tmproot)
954 shutil.rmtree(tmproot)
956
955
957
956
958 def filemerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
957 def filemerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
959 """perform a 3-way merge in the working directory
958 """perform a 3-way merge in the working directory
960
959
961 mynode = parent node before merge
960 mynode = parent node before merge
962 orig = original local filename before merge
961 orig = original local filename before merge
963 fco = other file context
962 fco = other file context
964 fca = ancestor file context
963 fca = ancestor file context
965 fcd = local file context for current/destination file
964 fcd = local file context for current/destination file
966
965
967 Returns whether the merge is complete, the return value of the merge, and
966 Returns whether the merge is complete, the return value of the merge, and
968 a boolean indicating whether the file was deleted from disk."""
967 a boolean indicating whether the file was deleted from disk."""
969 ui = repo.ui
968 ui = repo.ui
970 fd = fcd.path()
969 fd = fcd.path()
971 uipathfn = scmutil.getuipathfn(repo)
970 uipathfn = scmutil.getuipathfn(repo)
972 fduipath = uipathfn(fd)
971 fduipath = uipathfn(fd)
973 binary = fcd.isbinary() or fco.isbinary() or fca.isbinary()
972 binary = fcd.isbinary() or fco.isbinary() or fca.isbinary()
974 symlink = b'l' in fcd.flags() + fco.flags()
973 symlink = b'l' in fcd.flags() + fco.flags()
975 changedelete = fcd.isabsent() or fco.isabsent()
974 changedelete = fcd.isabsent() or fco.isabsent()
976 tool, toolpath = _picktool(repo, ui, fd, binary, symlink, changedelete)
975 tool, toolpath = _picktool(repo, ui, fd, binary, symlink, changedelete)
977 scriptfn = None
976 scriptfn = None
978 if tool in internals and tool.startswith(b'internal:'):
977 if tool in internals and tool.startswith(b'internal:'):
979 # normalize to new-style names (':merge' etc)
978 # normalize to new-style names (':merge' etc)
980 tool = tool[len(b'internal') :]
979 tool = tool[len(b'internal') :]
981 if toolpath and toolpath.startswith(b'python:'):
980 if toolpath and toolpath.startswith(b'python:'):
982 invalidsyntax = False
981 invalidsyntax = False
983 if toolpath.count(b':') >= 2:
982 if toolpath.count(b':') >= 2:
984 script, scriptfn = toolpath[7:].rsplit(b':', 1)
983 script, scriptfn = toolpath[7:].rsplit(b':', 1)
985 if not scriptfn:
984 if not scriptfn:
986 invalidsyntax = True
985 invalidsyntax = True
987 # missing :callable can lead to spliting on windows drive letter
986 # missing :callable can lead to spliting on windows drive letter
988 if b'\\' in scriptfn or b'/' in scriptfn:
987 if b'\\' in scriptfn or b'/' in scriptfn:
989 invalidsyntax = True
988 invalidsyntax = True
990 else:
989 else:
991 invalidsyntax = True
990 invalidsyntax = True
992 if invalidsyntax:
991 if invalidsyntax:
993 raise error.Abort(_(b"invalid 'python:' syntax: %s") % toolpath)
992 raise error.Abort(_(b"invalid 'python:' syntax: %s") % toolpath)
994 toolpath = script
993 toolpath = script
995 ui.debug(
994 ui.debug(
996 b"picked tool '%s' for %s (binary %s symlink %s changedelete %s)\n"
995 b"picked tool '%s' for %s (binary %s symlink %s changedelete %s)\n"
997 % (
996 % (
998 tool,
997 tool,
999 fduipath,
998 fduipath,
1000 pycompat.bytestr(binary),
999 pycompat.bytestr(binary),
1001 pycompat.bytestr(symlink),
1000 pycompat.bytestr(symlink),
1002 pycompat.bytestr(changedelete),
1001 pycompat.bytestr(changedelete),
1003 )
1002 )
1004 )
1003 )
1005
1004
1006 if tool in internals:
1005 if tool in internals:
1007 func = internals[tool]
1006 func = internals[tool]
1008 mergetype = func.mergetype
1007 mergetype = func.mergetype
1009 onfailure = func.onfailure
1008 onfailure = func.onfailure
1010 precheck = func.precheck
1009 precheck = func.precheck
1011 isexternal = False
1010 isexternal = False
1012 else:
1011 else:
1013 if wctx.isinmemory():
1012 if wctx.isinmemory():
1014 func = _xmergeimm
1013 func = _xmergeimm
1015 else:
1014 else:
1016 func = _xmerge
1015 func = _xmerge
1017 mergetype = fullmerge
1016 mergetype = fullmerge
1018 onfailure = _(b"merging %s failed!\n")
1017 onfailure = _(b"merging %s failed!\n")
1019 precheck = None
1018 precheck = None
1020 isexternal = True
1019 isexternal = True
1021
1020
1022 toolconf = tool, toolpath, binary, symlink, scriptfn
1021 toolconf = tool, toolpath, binary, symlink, scriptfn
1023
1022
1024 if not labels:
1023 if not labels:
1025 labels = [b'local', b'other']
1024 labels = [b'local', b'other']
1026 if len(labels) < 3:
1025 if len(labels) < 3:
1027 labels.append(b'base')
1026 labels.append(b'base')
1028 local = simplemerge.MergeInput(fcd, labels[0])
1027 local = simplemerge.MergeInput(fcd, labels[0])
1029 other = simplemerge.MergeInput(fco, labels[1])
1028 other = simplemerge.MergeInput(fco, labels[1])
1030 base = simplemerge.MergeInput(fca, labels[2])
1029 base = simplemerge.MergeInput(fca, labels[2])
1031 if mergetype == nomerge:
1030 if mergetype == nomerge:
1032 return func(
1031 return func(
1033 repo,
1032 repo,
1034 mynode,
1033 mynode,
1035 local,
1034 local,
1036 other,
1035 other,
1037 base,
1036 base,
1038 toolconf,
1037 toolconf,
1039 )
1038 )
1040
1039
1041 if orig != fco.path():
1040 if orig != fco.path():
1042 ui.status(
1041 ui.status(
1043 _(b"merging %s and %s to %s\n")
1042 _(b"merging %s and %s to %s\n")
1044 % (uipathfn(orig), uipathfn(fco.path()), fduipath)
1043 % (uipathfn(orig), uipathfn(fco.path()), fduipath)
1045 )
1044 )
1046 else:
1045 else:
1047 ui.status(_(b"merging %s\n") % fduipath)
1046 ui.status(_(b"merging %s\n") % fduipath)
1048
1047
1049 ui.debug(b"my %s other %s ancestor %s\n" % (fcd, fco, fca))
1048 ui.debug(b"my %s other %s ancestor %s\n" % (fcd, fco, fca))
1050
1049
1051 if precheck and not precheck(repo, mynode, fcd, fco, fca, toolconf):
1050 if precheck and not precheck(repo, mynode, fcd, fco, fca, toolconf):
1052 if onfailure:
1051 if onfailure:
1053 if wctx.isinmemory():
1052 if wctx.isinmemory():
1054 raise error.InMemoryMergeConflictsError(
1053 raise error.InMemoryMergeConflictsError(
1055 b'in-memory merge does not support merge conflicts'
1054 b'in-memory merge does not support merge conflicts'
1056 )
1055 )
1057 ui.warn(onfailure % fduipath)
1056 ui.warn(onfailure % fduipath)
1058 return 1, False
1057 return 1, False
1059
1058
1060 backup = _makebackup(repo, ui, fcd)
1059 backup = _makebackup(repo, ui, fcd)
1061 r = 1
1060 r = 1
1062 try:
1061 try:
1063 internalmarkerstyle = ui.config(b'ui', b'mergemarkers')
1062 internalmarkerstyle = ui.config(b'ui', b'mergemarkers')
1064 if isexternal:
1063 if isexternal:
1065 markerstyle = _toolstr(ui, tool, b'mergemarkers')
1064 markerstyle = _toolstr(ui, tool, b'mergemarkers')
1066 else:
1065 else:
1067 markerstyle = internalmarkerstyle
1066 markerstyle = internalmarkerstyle
1068
1067
1069 if mergetype == fullmerge:
1068 if mergetype == fullmerge:
1070 # conflict markers generated by premerge will use 'detailed'
1069 # conflict markers generated by premerge will use 'detailed'
1071 # settings if either ui.mergemarkers or the tool's mergemarkers
1070 # settings if either ui.mergemarkers or the tool's mergemarkers
1072 # setting is 'detailed'. This way tools can have basic labels in
1071 # setting is 'detailed'. This way tools can have basic labels in
1073 # space-constrained areas of the UI, but still get full information
1072 # space-constrained areas of the UI, but still get full information
1074 # in conflict markers if premerge is 'keep' or 'keep-merge3'.
1073 # in conflict markers if premerge is 'keep' or 'keep-merge3'.
1075 labeltool = None
1074 labeltool = None
1076 if markerstyle != b'basic':
1075 if markerstyle != b'basic':
1077 # respect 'tool's mergemarkertemplate (which defaults to
1076 # respect 'tool's mergemarkertemplate (which defaults to
1078 # command-templates.mergemarker)
1077 # command-templates.mergemarker)
1079 labeltool = tool
1078 labeltool = tool
1080 if internalmarkerstyle != b'basic' or markerstyle != b'basic':
1079 if internalmarkerstyle != b'basic' or markerstyle != b'basic':
1081 _populate_label_details(
1080 _populate_label_details(
1082 repo, [local, other, base], tool=labeltool
1081 repo, [local, other, base], tool=labeltool
1083 )
1082 )
1084
1083
1085 r = _premerge(
1084 r = _premerge(
1086 repo,
1085 repo,
1087 local,
1086 local,
1088 other,
1087 other,
1089 base,
1088 base,
1090 toolconf,
1089 toolconf,
1091 )
1090 )
1092 # we're done if premerge was successful (r is 0)
1091 # we're done if premerge was successful (r is 0)
1093 if not r:
1092 if not r:
1094 return r, False
1093 return r, False
1095
1094
1096 # Reset to basic labels
1095 # Reset to basic labels
1097 local.label_detail = None
1096 local.label_detail = None
1098 other.label_detail = None
1097 other.label_detail = None
1099 base.label_detail = None
1098 base.label_detail = None
1100
1099
1101 if markerstyle != b'basic':
1100 if markerstyle != b'basic':
1102 _populate_label_details(repo, [local, other, base], tool=tool)
1101 _populate_label_details(repo, [local, other, base], tool=tool)
1103
1102
1104 needcheck, r, deleted = func(
1103 needcheck, r, deleted = func(
1105 repo,
1104 repo,
1106 mynode,
1105 mynode,
1107 local,
1106 local,
1108 other,
1107 other,
1109 base,
1108 base,
1110 toolconf,
1109 toolconf,
1111 backup,
1110 backup,
1112 )
1111 )
1113
1112
1114 if needcheck:
1113 if needcheck:
1115 r = _check(repo, r, ui, tool, fcd, backup)
1114 r = _check(repo, r, ui, tool, fcd, backup)
1116
1115
1117 if r:
1116 if r:
1118 if onfailure:
1117 if onfailure:
1119 if wctx.isinmemory():
1118 if wctx.isinmemory():
1120 raise error.InMemoryMergeConflictsError(
1119 raise error.InMemoryMergeConflictsError(
1121 b'in-memory merge '
1120 b'in-memory merge '
1122 b'does not support '
1121 b'does not support '
1123 b'merge conflicts'
1122 b'merge conflicts'
1124 )
1123 )
1125 ui.warn(onfailure % fduipath)
1124 ui.warn(onfailure % fduipath)
1126 _onfilemergefailure(ui)
1125 _onfilemergefailure(ui)
1127
1126
1128 return r, deleted
1127 return r, deleted
1129 finally:
1128 finally:
1130 if not r and backup is not None:
1129 if not r and backup is not None:
1131 backup.remove()
1130 backup.remove()
1132
1131
1133
1132
1134 def _haltmerge():
1133 def _haltmerge():
1135 msg = _(b'merge halted after failed merge (see hg resolve)')
1134 msg = _(b'merge halted after failed merge (see hg resolve)')
1136 raise error.InterventionRequired(msg)
1135 raise error.InterventionRequired(msg)
1137
1136
1138
1137
1139 def _onfilemergefailure(ui):
1138 def _onfilemergefailure(ui):
1140 action = ui.config(b'merge', b'on-failure')
1139 action = ui.config(b'merge', b'on-failure')
1141 if action == b'prompt':
1140 if action == b'prompt':
1142 msg = _(b'continue merge operation (yn)?$$ &Yes $$ &No')
1141 msg = _(b'continue merge operation (yn)?$$ &Yes $$ &No')
1143 if ui.promptchoice(msg, 0) == 1:
1142 if ui.promptchoice(msg, 0) == 1:
1144 _haltmerge()
1143 _haltmerge()
1145 if action == b'halt':
1144 if action == b'halt':
1146 _haltmerge()
1145 _haltmerge()
1147 # default action is 'continue', in which case we neither prompt nor halt
1146 # default action is 'continue', in which case we neither prompt nor halt
1148
1147
1149
1148
1150 def hasconflictmarkers(data):
1149 def hasconflictmarkers(data):
1151 # Detect lines starting with a string of 7 identical characters from the
1150 # Detect lines starting with a string of 7 identical characters from the
1152 # subset Mercurial uses for conflict markers, followed by either the end of
1151 # subset Mercurial uses for conflict markers, followed by either the end of
1153 # line or a space and some text. Note that using [<>=+|-]{7} would detect
1152 # line or a space and some text. Note that using [<>=+|-]{7} would detect
1154 # `<><><><><` as a conflict marker, which we don't want.
1153 # `<><><><><` as a conflict marker, which we don't want.
1155 return bool(
1154 return bool(
1156 re.search(
1155 re.search(
1157 br"^([<>=+|-])\1{6}( .*)$",
1156 br"^([<>=+|-])\1{6}( .*)$",
1158 data,
1157 data,
1159 re.MULTILINE,
1158 re.MULTILINE,
1160 )
1159 )
1161 )
1160 )
1162
1161
1163
1162
1164 def _check(repo, r, ui, tool, fcd, backup):
1163 def _check(repo, r, ui, tool, fcd, backup):
1165 fd = fcd.path()
1164 fd = fcd.path()
1166 uipathfn = scmutil.getuipathfn(repo)
1165 uipathfn = scmutil.getuipathfn(repo)
1167
1166
1168 if not r and (
1167 if not r and (
1169 _toolbool(ui, tool, b"checkconflicts")
1168 _toolbool(ui, tool, b"checkconflicts")
1170 or b'conflicts' in _toollist(ui, tool, b"check")
1169 or b'conflicts' in _toollist(ui, tool, b"check")
1171 ):
1170 ):
1172 if hasconflictmarkers(fcd.data()):
1171 if hasconflictmarkers(fcd.data()):
1173 r = 1
1172 r = 1
1174
1173
1175 checked = False
1174 checked = False
1176 if b'prompt' in _toollist(ui, tool, b"check"):
1175 if b'prompt' in _toollist(ui, tool, b"check"):
1177 checked = True
1176 checked = True
1178 if ui.promptchoice(
1177 if ui.promptchoice(
1179 _(b"was merge of '%s' successful (yn)?$$ &Yes $$ &No")
1178 _(b"was merge of '%s' successful (yn)?$$ &Yes $$ &No")
1180 % uipathfn(fd),
1179 % uipathfn(fd),
1181 1,
1180 1,
1182 ):
1181 ):
1183 r = 1
1182 r = 1
1184
1183
1185 if (
1184 if (
1186 not r
1185 not r
1187 and not checked
1186 and not checked
1188 and (
1187 and (
1189 _toolbool(ui, tool, b"checkchanged")
1188 _toolbool(ui, tool, b"checkchanged")
1190 or b'changed' in _toollist(ui, tool, b"check")
1189 or b'changed' in _toollist(ui, tool, b"check")
1191 )
1190 )
1192 ):
1191 ):
1193 if backup is not None and not fcd.cmp(backup):
1192 if backup is not None and not fcd.cmp(backup):
1194 if ui.promptchoice(
1193 if ui.promptchoice(
1195 _(
1194 _(
1196 b" output file %s appears unchanged\n"
1195 b" output file %s appears unchanged\n"
1197 b"was merge successful (yn)?"
1196 b"was merge successful (yn)?"
1198 b"$$ &Yes $$ &No"
1197 b"$$ &Yes $$ &No"
1199 )
1198 )
1200 % uipathfn(fd),
1199 % uipathfn(fd),
1201 1,
1200 1,
1202 ):
1201 ):
1203 r = 1
1202 r = 1
1204
1203
1205 if backup is not None and _toolbool(ui, tool, b"fixeol"):
1204 if backup is not None and _toolbool(ui, tool, b"fixeol"):
1206 _matcheol(_workingpath(repo, fcd), backup)
1205 _matcheol(_workingpath(repo, fcd), backup)
1207
1206
1208 return r
1207 return r
1209
1208
1210
1209
1211 def _workingpath(repo, ctx):
1210 def _workingpath(repo, ctx):
1212 return repo.wjoin(ctx.path())
1211 return repo.wjoin(ctx.path())
1213
1212
1214
1213
1215 def loadinternalmerge(ui, extname, registrarobj):
1214 def loadinternalmerge(ui, extname, registrarobj):
1216 """Load internal merge tool from specified registrarobj"""
1215 """Load internal merge tool from specified registrarobj"""
1217 for name, func in pycompat.iteritems(registrarobj._table):
1216 for name, func in pycompat.iteritems(registrarobj._table):
1218 fullname = b':' + name
1217 fullname = b':' + name
1219 internals[fullname] = func
1218 internals[fullname] = func
1220 internals[b'internal:' + name] = func
1219 internals[b'internal:' + name] = func
1221 internalsdoc[fullname] = func
1220 internalsdoc[fullname] = func
1222
1221
1223 capabilities = sorted([k for k, v in func.capabilities.items() if v])
1222 capabilities = sorted([k for k, v in func.capabilities.items() if v])
1224 if capabilities:
1223 if capabilities:
1225 capdesc = b" (actual capabilities: %s)" % b', '.join(
1224 capdesc = b" (actual capabilities: %s)" % b', '.join(
1226 capabilities
1225 capabilities
1227 )
1226 )
1228 func.__doc__ = func.__doc__ + pycompat.sysstr(b"\n\n%s" % capdesc)
1227 func.__doc__ = func.__doc__ + pycompat.sysstr(b"\n\n%s" % capdesc)
1229
1228
1230 # to put i18n comments into hg.pot for automatically generated texts
1229 # to put i18n comments into hg.pot for automatically generated texts
1231
1230
1232 # i18n: "binary" and "symlink" are keywords
1231 # i18n: "binary" and "symlink" are keywords
1233 # i18n: this text is added automatically
1232 # i18n: this text is added automatically
1234 _(b" (actual capabilities: binary, symlink)")
1233 _(b" (actual capabilities: binary, symlink)")
1235 # i18n: "binary" is keyword
1234 # i18n: "binary" is keyword
1236 # i18n: this text is added automatically
1235 # i18n: this text is added automatically
1237 _(b" (actual capabilities: binary)")
1236 _(b" (actual capabilities: binary)")
1238 # i18n: "symlink" is keyword
1237 # i18n: "symlink" is keyword
1239 # i18n: this text is added automatically
1238 # i18n: this text is added automatically
1240 _(b" (actual capabilities: symlink)")
1239 _(b" (actual capabilities: symlink)")
1241
1240
1242
1241
1243 # load built-in merge tools explicitly to setup internalsdoc
1242 # load built-in merge tools explicitly to setup internalsdoc
1244 loadinternalmerge(None, None, internaltool)
1243 loadinternalmerge(None, None, internaltool)
1245
1244
1246 # tell hggettext to extract docstrings from these functions:
1245 # tell hggettext to extract docstrings from these functions:
1247 i18nfunctions = internals.values()
1246 i18nfunctions = internals.values()
General Comments 0
You need to be logged in to leave comments. Login now