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