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