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