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