##// END OF EJS Templates
merge: allow user to halt merge on merge-tool failures...
Ryan McElroy -
r34798:284fa44f default
parent child Browse files
Show More
@@ -1,817 +1,818 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 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 os
10 import os
11 import re
11 import re
12 import tempfile
12 import tempfile
13
13
14 from .i18n import _
14 from .i18n import _
15 from .node import nullid, short
15 from .node import nullid, short
16
16
17 from . import (
17 from . import (
18 encoding,
18 encoding,
19 error,
19 error,
20 formatter,
20 formatter,
21 match,
21 match,
22 pycompat,
22 pycompat,
23 registrar,
23 registrar,
24 scmutil,
24 scmutil,
25 simplemerge,
25 simplemerge,
26 tagmerge,
26 tagmerge,
27 templatekw,
27 templatekw,
28 templater,
28 templater,
29 util,
29 util,
30 )
30 )
31
31
32 def _toolstr(ui, tool, part, default=""):
32 def _toolstr(ui, tool, part, default=""):
33 return ui.config("merge-tools", tool + "." + part, default)
33 return ui.config("merge-tools", tool + "." + part, default)
34
34
35 def _toolbool(ui, tool, part, default=False):
35 def _toolbool(ui, tool, part, default=False):
36 return ui.configbool("merge-tools", tool + "." + part, default)
36 return ui.configbool("merge-tools", tool + "." + part, default)
37
37
38 def _toollist(ui, tool, part, default=None):
38 def _toollist(ui, tool, part, default=None):
39 if default is None:
39 if default is None:
40 default = []
40 default = []
41 return ui.configlist("merge-tools", tool + "." + part, default)
41 return ui.configlist("merge-tools", tool + "." + part, default)
42
42
43 internals = {}
43 internals = {}
44 # Merge tools to document.
44 # Merge tools to document.
45 internalsdoc = {}
45 internalsdoc = {}
46
46
47 internaltool = registrar.internalmerge()
47 internaltool = registrar.internalmerge()
48
48
49 # internal tool merge types
49 # internal tool merge types
50 nomerge = internaltool.nomerge
50 nomerge = internaltool.nomerge
51 mergeonly = internaltool.mergeonly # just the full merge, no premerge
51 mergeonly = internaltool.mergeonly # just the full merge, no premerge
52 fullmerge = internaltool.fullmerge # both premerge and merge
52 fullmerge = internaltool.fullmerge # both premerge and merge
53
53
54 _localchangedotherdeletedmsg = _(
54 _localchangedotherdeletedmsg = _(
55 "local%(l)s changed %(fd)s which other%(o)s deleted\n"
55 "local%(l)s changed %(fd)s which other%(o)s deleted\n"
56 "use (c)hanged version, (d)elete, or leave (u)nresolved?"
56 "use (c)hanged version, (d)elete, or leave (u)nresolved?"
57 "$$ &Changed $$ &Delete $$ &Unresolved")
57 "$$ &Changed $$ &Delete $$ &Unresolved")
58
58
59 _otherchangedlocaldeletedmsg = _(
59 _otherchangedlocaldeletedmsg = _(
60 "other%(o)s changed %(fd)s which local%(l)s deleted\n"
60 "other%(o)s changed %(fd)s which local%(l)s deleted\n"
61 "use (c)hanged version, leave (d)eleted, or "
61 "use (c)hanged version, leave (d)eleted, or "
62 "leave (u)nresolved?"
62 "leave (u)nresolved?"
63 "$$ &Changed $$ &Deleted $$ &Unresolved")
63 "$$ &Changed $$ &Deleted $$ &Unresolved")
64
64
65 class absentfilectx(object):
65 class absentfilectx(object):
66 """Represents a file that's ostensibly in a context but is actually not
66 """Represents a file that's ostensibly in a context but is actually not
67 present in it.
67 present in it.
68
68
69 This is here because it's very specific to the filemerge code for now --
69 This is here because it's very specific to the filemerge code for now --
70 other code is likely going to break with the values this returns."""
70 other code is likely going to break with the values this returns."""
71 def __init__(self, ctx, f):
71 def __init__(self, ctx, f):
72 self._ctx = ctx
72 self._ctx = ctx
73 self._f = f
73 self._f = f
74
74
75 def path(self):
75 def path(self):
76 return self._f
76 return self._f
77
77
78 def size(self):
78 def size(self):
79 return None
79 return None
80
80
81 def data(self):
81 def data(self):
82 return None
82 return None
83
83
84 def filenode(self):
84 def filenode(self):
85 return nullid
85 return nullid
86
86
87 _customcmp = True
87 _customcmp = True
88 def cmp(self, fctx):
88 def cmp(self, fctx):
89 """compare with other file context
89 """compare with other file context
90
90
91 returns True if different from fctx.
91 returns True if different from fctx.
92 """
92 """
93 return not (fctx.isabsent() and
93 return not (fctx.isabsent() and
94 fctx.ctx() == self.ctx() and
94 fctx.ctx() == self.ctx() and
95 fctx.path() == self.path())
95 fctx.path() == self.path())
96
96
97 def flags(self):
97 def flags(self):
98 return ''
98 return ''
99
99
100 def changectx(self):
100 def changectx(self):
101 return self._ctx
101 return self._ctx
102
102
103 def isbinary(self):
103 def isbinary(self):
104 return False
104 return False
105
105
106 def isabsent(self):
106 def isabsent(self):
107 return True
107 return True
108
108
109 def _findtool(ui, tool):
109 def _findtool(ui, tool):
110 if tool in internals:
110 if tool in internals:
111 return tool
111 return tool
112 return findexternaltool(ui, tool)
112 return findexternaltool(ui, tool)
113
113
114 def findexternaltool(ui, tool):
114 def findexternaltool(ui, tool):
115 for kn in ("regkey", "regkeyalt"):
115 for kn in ("regkey", "regkeyalt"):
116 k = _toolstr(ui, tool, kn)
116 k = _toolstr(ui, tool, kn)
117 if not k:
117 if not k:
118 continue
118 continue
119 p = util.lookupreg(k, _toolstr(ui, tool, "regname"))
119 p = util.lookupreg(k, _toolstr(ui, tool, "regname"))
120 if p:
120 if p:
121 p = util.findexe(p + _toolstr(ui, tool, "regappend"))
121 p = util.findexe(p + _toolstr(ui, tool, "regappend"))
122 if p:
122 if p:
123 return p
123 return p
124 exe = _toolstr(ui, tool, "executable", tool)
124 exe = _toolstr(ui, tool, "executable", tool)
125 return util.findexe(util.expandpath(exe))
125 return util.findexe(util.expandpath(exe))
126
126
127 def _picktool(repo, ui, path, binary, symlink, changedelete):
127 def _picktool(repo, ui, path, binary, symlink, changedelete):
128 def supportscd(tool):
128 def supportscd(tool):
129 return tool in internals and internals[tool].mergetype == nomerge
129 return tool in internals and internals[tool].mergetype == nomerge
130
130
131 def check(tool, pat, symlink, binary, changedelete):
131 def check(tool, pat, symlink, binary, changedelete):
132 tmsg = tool
132 tmsg = tool
133 if pat:
133 if pat:
134 tmsg = _("%s (for pattern %s)") % (tool, pat)
134 tmsg = _("%s (for pattern %s)") % (tool, pat)
135 if not _findtool(ui, tool):
135 if not _findtool(ui, tool):
136 if pat: # explicitly requested tool deserves a warning
136 if pat: # explicitly requested tool deserves a warning
137 ui.warn(_("couldn't find merge tool %s\n") % tmsg)
137 ui.warn(_("couldn't find merge tool %s\n") % tmsg)
138 else: # configured but non-existing tools are more silent
138 else: # configured but non-existing tools are more silent
139 ui.note(_("couldn't find merge tool %s\n") % tmsg)
139 ui.note(_("couldn't find merge tool %s\n") % tmsg)
140 elif symlink and not _toolbool(ui, tool, "symlink"):
140 elif symlink and not _toolbool(ui, tool, "symlink"):
141 ui.warn(_("tool %s can't handle symlinks\n") % tmsg)
141 ui.warn(_("tool %s can't handle symlinks\n") % tmsg)
142 elif binary and not _toolbool(ui, tool, "binary"):
142 elif binary and not _toolbool(ui, tool, "binary"):
143 ui.warn(_("tool %s can't handle binary\n") % tmsg)
143 ui.warn(_("tool %s can't handle binary\n") % tmsg)
144 elif changedelete and not supportscd(tool):
144 elif changedelete and not supportscd(tool):
145 # the nomerge tools are the only tools that support change/delete
145 # the nomerge tools are the only tools that support change/delete
146 # conflicts
146 # conflicts
147 pass
147 pass
148 elif not util.gui() and _toolbool(ui, tool, "gui"):
148 elif not util.gui() and _toolbool(ui, tool, "gui"):
149 ui.warn(_("tool %s requires a GUI\n") % tmsg)
149 ui.warn(_("tool %s requires a GUI\n") % tmsg)
150 else:
150 else:
151 return True
151 return True
152 return False
152 return False
153
153
154 # internal config: ui.forcemerge
154 # internal config: ui.forcemerge
155 # forcemerge comes from command line arguments, highest priority
155 # forcemerge comes from command line arguments, highest priority
156 force = ui.config('ui', 'forcemerge')
156 force = ui.config('ui', 'forcemerge')
157 if force:
157 if force:
158 toolpath = _findtool(ui, force)
158 toolpath = _findtool(ui, force)
159 if changedelete and not supportscd(toolpath):
159 if changedelete and not supportscd(toolpath):
160 return ":prompt", None
160 return ":prompt", None
161 else:
161 else:
162 if toolpath:
162 if toolpath:
163 return (force, util.shellquote(toolpath))
163 return (force, util.shellquote(toolpath))
164 else:
164 else:
165 # mimic HGMERGE if given tool not found
165 # mimic HGMERGE if given tool not found
166 return (force, force)
166 return (force, force)
167
167
168 # HGMERGE takes next precedence
168 # HGMERGE takes next precedence
169 hgmerge = encoding.environ.get("HGMERGE")
169 hgmerge = encoding.environ.get("HGMERGE")
170 if hgmerge:
170 if hgmerge:
171 if changedelete and not supportscd(hgmerge):
171 if changedelete and not supportscd(hgmerge):
172 return ":prompt", None
172 return ":prompt", None
173 else:
173 else:
174 return (hgmerge, hgmerge)
174 return (hgmerge, hgmerge)
175
175
176 # then patterns
176 # then patterns
177 for pat, tool in ui.configitems("merge-patterns"):
177 for pat, tool in ui.configitems("merge-patterns"):
178 mf = match.match(repo.root, '', [pat])
178 mf = match.match(repo.root, '', [pat])
179 if mf(path) and check(tool, pat, symlink, False, changedelete):
179 if mf(path) and check(tool, pat, symlink, False, changedelete):
180 toolpath = _findtool(ui, tool)
180 toolpath = _findtool(ui, tool)
181 return (tool, util.shellquote(toolpath))
181 return (tool, util.shellquote(toolpath))
182
182
183 # then merge tools
183 # then merge tools
184 tools = {}
184 tools = {}
185 disabled = set()
185 disabled = set()
186 for k, v in ui.configitems("merge-tools"):
186 for k, v in ui.configitems("merge-tools"):
187 t = k.split('.')[0]
187 t = k.split('.')[0]
188 if t not in tools:
188 if t not in tools:
189 tools[t] = int(_toolstr(ui, t, "priority", "0"))
189 tools[t] = int(_toolstr(ui, t, "priority", "0"))
190 if _toolbool(ui, t, "disabled", False):
190 if _toolbool(ui, t, "disabled", False):
191 disabled.add(t)
191 disabled.add(t)
192 names = tools.keys()
192 names = tools.keys()
193 tools = sorted([(-p, tool) for tool, p in tools.items()
193 tools = sorted([(-p, tool) for tool, p in tools.items()
194 if tool not in disabled])
194 if tool not in disabled])
195 uimerge = ui.config("ui", "merge")
195 uimerge = ui.config("ui", "merge")
196 if uimerge:
196 if uimerge:
197 # external tools defined in uimerge won't be able to handle
197 # external tools defined in uimerge won't be able to handle
198 # change/delete conflicts
198 # change/delete conflicts
199 if uimerge not in names and not changedelete:
199 if uimerge not in names and not changedelete:
200 return (uimerge, uimerge)
200 return (uimerge, uimerge)
201 tools.insert(0, (None, uimerge)) # highest priority
201 tools.insert(0, (None, uimerge)) # highest priority
202 tools.append((None, "hgmerge")) # the old default, if found
202 tools.append((None, "hgmerge")) # the old default, if found
203 for p, t in tools:
203 for p, t in tools:
204 if check(t, None, symlink, binary, changedelete):
204 if check(t, None, symlink, binary, changedelete):
205 toolpath = _findtool(ui, t)
205 toolpath = _findtool(ui, t)
206 return (t, util.shellquote(toolpath))
206 return (t, util.shellquote(toolpath))
207
207
208 # internal merge or prompt as last resort
208 # internal merge or prompt as last resort
209 if symlink or binary or changedelete:
209 if symlink or binary or changedelete:
210 if not changedelete and len(tools):
210 if not changedelete and len(tools):
211 # any tool is rejected by capability for symlink or binary
211 # any tool is rejected by capability for symlink or binary
212 ui.warn(_("no tool found to merge %s\n") % path)
212 ui.warn(_("no tool found to merge %s\n") % path)
213 return ":prompt", None
213 return ":prompt", None
214 return ":merge", None
214 return ":merge", None
215
215
216 def _eoltype(data):
216 def _eoltype(data):
217 "Guess the EOL type of a file"
217 "Guess the EOL type of a file"
218 if '\0' in data: # binary
218 if '\0' in data: # binary
219 return None
219 return None
220 if '\r\n' in data: # Windows
220 if '\r\n' in data: # Windows
221 return '\r\n'
221 return '\r\n'
222 if '\r' in data: # Old Mac
222 if '\r' in data: # Old Mac
223 return '\r'
223 return '\r'
224 if '\n' in data: # UNIX
224 if '\n' in data: # UNIX
225 return '\n'
225 return '\n'
226 return None # unknown
226 return None # unknown
227
227
228 def _matcheol(file, back):
228 def _matcheol(file, back):
229 "Convert EOL markers in a file to match origfile"
229 "Convert EOL markers in a file to match origfile"
230 tostyle = _eoltype(back.data()) # No repo.wread filters?
230 tostyle = _eoltype(back.data()) # No repo.wread filters?
231 if tostyle:
231 if tostyle:
232 data = util.readfile(file)
232 data = util.readfile(file)
233 style = _eoltype(data)
233 style = _eoltype(data)
234 if style:
234 if style:
235 newdata = data.replace(style, tostyle)
235 newdata = data.replace(style, tostyle)
236 if newdata != data:
236 if newdata != data:
237 util.writefile(file, newdata)
237 util.writefile(file, newdata)
238
238
239 @internaltool('prompt', nomerge)
239 @internaltool('prompt', nomerge)
240 def _iprompt(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
240 def _iprompt(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
241 """Asks the user which of the local `p1()` or the other `p2()` version to
241 """Asks the user which of the local `p1()` or the other `p2()` version to
242 keep as the merged version."""
242 keep as the merged version."""
243 ui = repo.ui
243 ui = repo.ui
244 fd = fcd.path()
244 fd = fcd.path()
245
245
246 prompts = partextras(labels)
246 prompts = partextras(labels)
247 prompts['fd'] = fd
247 prompts['fd'] = fd
248 try:
248 try:
249 if fco.isabsent():
249 if fco.isabsent():
250 index = ui.promptchoice(
250 index = ui.promptchoice(
251 _localchangedotherdeletedmsg % prompts, 2)
251 _localchangedotherdeletedmsg % prompts, 2)
252 choice = ['local', 'other', 'unresolved'][index]
252 choice = ['local', 'other', 'unresolved'][index]
253 elif fcd.isabsent():
253 elif fcd.isabsent():
254 index = ui.promptchoice(
254 index = ui.promptchoice(
255 _otherchangedlocaldeletedmsg % prompts, 2)
255 _otherchangedlocaldeletedmsg % prompts, 2)
256 choice = ['other', 'local', 'unresolved'][index]
256 choice = ['other', 'local', 'unresolved'][index]
257 else:
257 else:
258 index = ui.promptchoice(
258 index = ui.promptchoice(
259 _("keep (l)ocal%(l)s, take (o)ther%(o)s, or leave (u)nresolved"
259 _("keep (l)ocal%(l)s, take (o)ther%(o)s, or leave (u)nresolved"
260 " for %(fd)s?"
260 " for %(fd)s?"
261 "$$ &Local $$ &Other $$ &Unresolved") % prompts, 2)
261 "$$ &Local $$ &Other $$ &Unresolved") % prompts, 2)
262 choice = ['local', 'other', 'unresolved'][index]
262 choice = ['local', 'other', 'unresolved'][index]
263
263
264 if choice == 'other':
264 if choice == 'other':
265 return _iother(repo, mynode, orig, fcd, fco, fca, toolconf,
265 return _iother(repo, mynode, orig, fcd, fco, fca, toolconf,
266 labels)
266 labels)
267 elif choice == 'local':
267 elif choice == 'local':
268 return _ilocal(repo, mynode, orig, fcd, fco, fca, toolconf,
268 return _ilocal(repo, mynode, orig, fcd, fco, fca, toolconf,
269 labels)
269 labels)
270 elif choice == 'unresolved':
270 elif choice == 'unresolved':
271 return _ifail(repo, mynode, orig, fcd, fco, fca, toolconf,
271 return _ifail(repo, mynode, orig, fcd, fco, fca, toolconf,
272 labels)
272 labels)
273 except error.ResponseExpected:
273 except error.ResponseExpected:
274 ui.write("\n")
274 ui.write("\n")
275 return _ifail(repo, mynode, orig, fcd, fco, fca, toolconf,
275 return _ifail(repo, mynode, orig, fcd, fco, fca, toolconf,
276 labels)
276 labels)
277
277
278 @internaltool('local', nomerge)
278 @internaltool('local', nomerge)
279 def _ilocal(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
279 def _ilocal(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
280 """Uses the local `p1()` version of files as the merged version."""
280 """Uses the local `p1()` version of files as the merged version."""
281 return 0, fcd.isabsent()
281 return 0, fcd.isabsent()
282
282
283 @internaltool('other', nomerge)
283 @internaltool('other', nomerge)
284 def _iother(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
284 def _iother(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
285 """Uses the other `p2()` version of files as the merged version."""
285 """Uses the other `p2()` version of files as the merged version."""
286 if fco.isabsent():
286 if fco.isabsent():
287 # local changed, remote deleted -- 'deleted' picked
287 # local changed, remote deleted -- 'deleted' picked
288 _underlyingfctxifabsent(fcd).remove()
288 _underlyingfctxifabsent(fcd).remove()
289 deleted = True
289 deleted = True
290 else:
290 else:
291 _underlyingfctxifabsent(fcd).write(fco.data(), fco.flags())
291 _underlyingfctxifabsent(fcd).write(fco.data(), fco.flags())
292 deleted = False
292 deleted = False
293 return 0, deleted
293 return 0, deleted
294
294
295 @internaltool('fail', nomerge)
295 @internaltool('fail', nomerge)
296 def _ifail(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
296 def _ifail(repo, mynode, orig, fcd, fco, fca, toolconf, labels=None):
297 """
297 """
298 Rather than attempting to merge files that were modified on both
298 Rather than attempting to merge files that were modified on both
299 branches, it marks them as unresolved. The resolve command must be
299 branches, it marks them as unresolved. The resolve command must be
300 used to resolve these conflicts."""
300 used to resolve these conflicts."""
301 # for change/delete conflicts write out the changed version, then fail
301 # for change/delete conflicts write out the changed version, then fail
302 if fcd.isabsent():
302 if fcd.isabsent():
303 _underlyingfctxifabsent(fcd).write(fco.data(), fco.flags())
303 _underlyingfctxifabsent(fcd).write(fco.data(), fco.flags())
304 return 1, False
304 return 1, False
305
305
306 def _underlyingfctxifabsent(filectx):
306 def _underlyingfctxifabsent(filectx):
307 """Sometimes when resolving, our fcd is actually an absentfilectx, but
307 """Sometimes when resolving, our fcd is actually an absentfilectx, but
308 we want to write to it (to do the resolve). This helper returns the
308 we want to write to it (to do the resolve). This helper returns the
309 underyling workingfilectx in that case.
309 underyling workingfilectx in that case.
310 """
310 """
311 if filectx.isabsent():
311 if filectx.isabsent():
312 return filectx.changectx()[filectx.path()]
312 return filectx.changectx()[filectx.path()]
313 else:
313 else:
314 return filectx
314 return filectx
315
315
316 def _premerge(repo, fcd, fco, fca, toolconf, files, labels=None):
316 def _premerge(repo, fcd, fco, fca, toolconf, files, labels=None):
317 tool, toolpath, binary, symlink = toolconf
317 tool, toolpath, binary, symlink = toolconf
318 if symlink or fcd.isabsent() or fco.isabsent():
318 if symlink or fcd.isabsent() or fco.isabsent():
319 return 1
319 return 1
320 unused, unused, unused, back = files
320 unused, unused, unused, back = files
321
321
322 ui = repo.ui
322 ui = repo.ui
323
323
324 validkeep = ['keep', 'keep-merge3']
324 validkeep = ['keep', 'keep-merge3']
325
325
326 # do we attempt to simplemerge first?
326 # do we attempt to simplemerge first?
327 try:
327 try:
328 premerge = _toolbool(ui, tool, "premerge", not binary)
328 premerge = _toolbool(ui, tool, "premerge", not binary)
329 except error.ConfigError:
329 except error.ConfigError:
330 premerge = _toolstr(ui, tool, "premerge").lower()
330 premerge = _toolstr(ui, tool, "premerge").lower()
331 if premerge not in validkeep:
331 if premerge not in validkeep:
332 _valid = ', '.join(["'" + v + "'" for v in validkeep])
332 _valid = ', '.join(["'" + v + "'" for v in validkeep])
333 raise error.ConfigError(_("%s.premerge not valid "
333 raise error.ConfigError(_("%s.premerge not valid "
334 "('%s' is neither boolean nor %s)") %
334 "('%s' is neither boolean nor %s)") %
335 (tool, premerge, _valid))
335 (tool, premerge, _valid))
336
336
337 if premerge:
337 if premerge:
338 if premerge == 'keep-merge3':
338 if premerge == 'keep-merge3':
339 if not labels:
339 if not labels:
340 labels = _defaultconflictlabels
340 labels = _defaultconflictlabels
341 if len(labels) < 3:
341 if len(labels) < 3:
342 labels.append('base')
342 labels.append('base')
343 r = simplemerge.simplemerge(ui, fcd, fca, fco, quiet=True, label=labels)
343 r = simplemerge.simplemerge(ui, fcd, fca, fco, quiet=True, label=labels)
344 if not r:
344 if not r:
345 ui.debug(" premerge successful\n")
345 ui.debug(" premerge successful\n")
346 return 0
346 return 0
347 if premerge not in validkeep:
347 if premerge not in validkeep:
348 # restore from backup and try again
348 # restore from backup and try again
349 _restorebackup(fcd, back)
349 _restorebackup(fcd, back)
350 return 1 # continue merging
350 return 1 # continue merging
351
351
352 def _mergecheck(repo, mynode, orig, fcd, fco, fca, toolconf):
352 def _mergecheck(repo, mynode, orig, fcd, fco, fca, toolconf):
353 tool, toolpath, binary, symlink = toolconf
353 tool, toolpath, binary, symlink = toolconf
354 if symlink:
354 if symlink:
355 repo.ui.warn(_('warning: internal %s cannot merge symlinks '
355 repo.ui.warn(_('warning: internal %s cannot merge symlinks '
356 'for %s\n') % (tool, fcd.path()))
356 'for %s\n') % (tool, fcd.path()))
357 return False
357 return False
358 if fcd.isabsent() or fco.isabsent():
358 if fcd.isabsent() or fco.isabsent():
359 repo.ui.warn(_('warning: internal %s cannot merge change/delete '
359 repo.ui.warn(_('warning: internal %s cannot merge change/delete '
360 'conflict for %s\n') % (tool, fcd.path()))
360 'conflict for %s\n') % (tool, fcd.path()))
361 return False
361 return False
362 return True
362 return True
363
363
364 def _merge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels, mode):
364 def _merge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels, mode):
365 """
365 """
366 Uses the internal non-interactive simple merge algorithm for merging
366 Uses the internal non-interactive simple merge algorithm for merging
367 files. It will fail if there are any conflicts and leave markers in
367 files. It will fail if there are any conflicts and leave markers in
368 the partially merged file. Markers will have two sections, one for each side
368 the partially merged file. Markers will have two sections, one for each side
369 of merge, unless mode equals 'union' which suppresses the markers."""
369 of merge, unless mode equals 'union' which suppresses the markers."""
370 ui = repo.ui
370 ui = repo.ui
371
371
372 r = simplemerge.simplemerge(ui, fcd, fca, fco, label=labels, mode=mode)
372 r = simplemerge.simplemerge(ui, fcd, fca, fco, label=labels, mode=mode)
373 return True, r, False
373 return True, r, False
374
374
375 @internaltool('union', fullmerge,
375 @internaltool('union', fullmerge,
376 _("warning: conflicts while merging %s! "
376 _("warning: conflicts while merging %s! "
377 "(edit, then use 'hg resolve --mark')\n"),
377 "(edit, then use 'hg resolve --mark')\n"),
378 precheck=_mergecheck)
378 precheck=_mergecheck)
379 def _iunion(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
379 def _iunion(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
380 """
380 """
381 Uses the internal non-interactive simple merge algorithm for merging
381 Uses the internal non-interactive simple merge algorithm for merging
382 files. It will use both left and right sides for conflict regions.
382 files. It will use both left and right sides for conflict regions.
383 No markers are inserted."""
383 No markers are inserted."""
384 return _merge(repo, mynode, orig, fcd, fco, fca, toolconf,
384 return _merge(repo, mynode, orig, fcd, fco, fca, toolconf,
385 files, labels, 'union')
385 files, labels, 'union')
386
386
387 @internaltool('merge', fullmerge,
387 @internaltool('merge', fullmerge,
388 _("warning: conflicts while merging %s! "
388 _("warning: conflicts while merging %s! "
389 "(edit, then use 'hg resolve --mark')\n"),
389 "(edit, then use 'hg resolve --mark')\n"),
390 precheck=_mergecheck)
390 precheck=_mergecheck)
391 def _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
391 def _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
392 """
392 """
393 Uses the internal non-interactive simple merge algorithm for merging
393 Uses the internal non-interactive simple merge algorithm for merging
394 files. It will fail if there are any conflicts and leave markers in
394 files. It will fail if there are any conflicts and leave markers in
395 the partially merged file. Markers will have two sections, one for each side
395 the partially merged file. Markers will have two sections, one for each side
396 of merge."""
396 of merge."""
397 return _merge(repo, mynode, orig, fcd, fco, fca, toolconf,
397 return _merge(repo, mynode, orig, fcd, fco, fca, toolconf,
398 files, labels, 'merge')
398 files, labels, 'merge')
399
399
400 @internaltool('merge3', fullmerge,
400 @internaltool('merge3', fullmerge,
401 _("warning: conflicts while merging %s! "
401 _("warning: conflicts while merging %s! "
402 "(edit, then use 'hg resolve --mark')\n"),
402 "(edit, then use 'hg resolve --mark')\n"),
403 precheck=_mergecheck)
403 precheck=_mergecheck)
404 def _imerge3(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
404 def _imerge3(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
405 """
405 """
406 Uses the internal non-interactive simple merge algorithm for merging
406 Uses the internal non-interactive simple merge algorithm for merging
407 files. It will fail if there are any conflicts and leave markers in
407 files. It will fail if there are any conflicts and leave markers in
408 the partially merged file. Marker will have three sections, one from each
408 the partially merged file. Marker will have three sections, one from each
409 side of the merge and one for the base content."""
409 side of the merge and one for the base content."""
410 if not labels:
410 if not labels:
411 labels = _defaultconflictlabels
411 labels = _defaultconflictlabels
412 if len(labels) < 3:
412 if len(labels) < 3:
413 labels.append('base')
413 labels.append('base')
414 return _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels)
414 return _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels)
415
415
416 def _imergeauto(repo, mynode, orig, fcd, fco, fca, toolconf, files,
416 def _imergeauto(repo, mynode, orig, fcd, fco, fca, toolconf, files,
417 labels=None, localorother=None):
417 labels=None, localorother=None):
418 """
418 """
419 Generic driver for _imergelocal and _imergeother
419 Generic driver for _imergelocal and _imergeother
420 """
420 """
421 assert localorother is not None
421 assert localorother is not None
422 tool, toolpath, binary, symlink = toolconf
422 tool, toolpath, binary, symlink = toolconf
423 r = simplemerge.simplemerge(repo.ui, fcd, fca, fco, label=labels,
423 r = simplemerge.simplemerge(repo.ui, fcd, fca, fco, label=labels,
424 localorother=localorother)
424 localorother=localorother)
425 return True, r
425 return True, r
426
426
427 @internaltool('merge-local', mergeonly, precheck=_mergecheck)
427 @internaltool('merge-local', mergeonly, precheck=_mergecheck)
428 def _imergelocal(*args, **kwargs):
428 def _imergelocal(*args, **kwargs):
429 """
429 """
430 Like :merge, but resolve all conflicts non-interactively in favor
430 Like :merge, but resolve all conflicts non-interactively in favor
431 of the local `p1()` changes."""
431 of the local `p1()` changes."""
432 success, status = _imergeauto(localorother='local', *args, **kwargs)
432 success, status = _imergeauto(localorother='local', *args, **kwargs)
433 return success, status, False
433 return success, status, False
434
434
435 @internaltool('merge-other', mergeonly, precheck=_mergecheck)
435 @internaltool('merge-other', mergeonly, precheck=_mergecheck)
436 def _imergeother(*args, **kwargs):
436 def _imergeother(*args, **kwargs):
437 """
437 """
438 Like :merge, but resolve all conflicts non-interactively in favor
438 Like :merge, but resolve all conflicts non-interactively in favor
439 of the other `p2()` changes."""
439 of the other `p2()` changes."""
440 success, status = _imergeauto(localorother='other', *args, **kwargs)
440 success, status = _imergeauto(localorother='other', *args, **kwargs)
441 return success, status, False
441 return success, status, False
442
442
443 @internaltool('tagmerge', mergeonly,
443 @internaltool('tagmerge', mergeonly,
444 _("automatic tag merging of %s failed! "
444 _("automatic tag merging of %s failed! "
445 "(use 'hg resolve --tool :merge' or another merge "
445 "(use 'hg resolve --tool :merge' or another merge "
446 "tool of your choice)\n"))
446 "tool of your choice)\n"))
447 def _itagmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
447 def _itagmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
448 """
448 """
449 Uses the internal tag merge algorithm (experimental).
449 Uses the internal tag merge algorithm (experimental).
450 """
450 """
451 success, status = tagmerge.merge(repo, fcd, fco, fca)
451 success, status = tagmerge.merge(repo, fcd, fco, fca)
452 return success, status, False
452 return success, status, False
453
453
454 @internaltool('dump', fullmerge)
454 @internaltool('dump', fullmerge)
455 def _idump(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
455 def _idump(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
456 """
456 """
457 Creates three versions of the files to merge, containing the
457 Creates three versions of the files to merge, containing the
458 contents of local, other and base. These files can then be used to
458 contents of local, other and base. These files can then be used to
459 perform a merge manually. If the file to be merged is named
459 perform a merge manually. If the file to be merged is named
460 ``a.txt``, these files will accordingly be named ``a.txt.local``,
460 ``a.txt``, these files will accordingly be named ``a.txt.local``,
461 ``a.txt.other`` and ``a.txt.base`` and they will be placed in the
461 ``a.txt.other`` and ``a.txt.base`` and they will be placed in the
462 same directory as ``a.txt``.
462 same directory as ``a.txt``.
463
463
464 This implies permerge. Therefore, files aren't dumped, if premerge
464 This implies permerge. Therefore, files aren't dumped, if premerge
465 runs successfully. Use :forcedump to forcibly write files out.
465 runs successfully. Use :forcedump to forcibly write files out.
466 """
466 """
467 a = _workingpath(repo, fcd)
467 a = _workingpath(repo, fcd)
468 fd = fcd.path()
468 fd = fcd.path()
469
469
470 # Run ``flushall()`` to make any missing folders the following wwrite
470 # Run ``flushall()`` to make any missing folders the following wwrite
471 # calls might be depending on.
471 # calls might be depending on.
472 from . import context
472 from . import context
473 if isinstance(fcd, context.overlayworkingfilectx):
473 if isinstance(fcd, context.overlayworkingfilectx):
474 fcd.ctx().flushall()
474 fcd.ctx().flushall()
475
475
476 util.writefile(a + ".local", fcd.decodeddata())
476 util.writefile(a + ".local", fcd.decodeddata())
477 repo.wwrite(fd + ".other", fco.data(), fco.flags())
477 repo.wwrite(fd + ".other", fco.data(), fco.flags())
478 repo.wwrite(fd + ".base", fca.data(), fca.flags())
478 repo.wwrite(fd + ".base", fca.data(), fca.flags())
479 return False, 1, False
479 return False, 1, False
480
480
481 @internaltool('forcedump', mergeonly)
481 @internaltool('forcedump', mergeonly)
482 def _forcedump(repo, mynode, orig, fcd, fco, fca, toolconf, files,
482 def _forcedump(repo, mynode, orig, fcd, fco, fca, toolconf, files,
483 labels=None):
483 labels=None):
484 """
484 """
485 Creates three versions of the files as same as :dump, but omits premerge.
485 Creates three versions of the files as same as :dump, but omits premerge.
486 """
486 """
487 return _idump(repo, mynode, orig, fcd, fco, fca, toolconf, files,
487 return _idump(repo, mynode, orig, fcd, fco, fca, toolconf, files,
488 labels=labels)
488 labels=labels)
489
489
490 def _xmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
490 def _xmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
491 tool, toolpath, binary, symlink = toolconf
491 tool, toolpath, binary, symlink = toolconf
492 if fcd.isabsent() or fco.isabsent():
492 if fcd.isabsent() or fco.isabsent():
493 repo.ui.warn(_('warning: %s cannot merge change/delete conflict '
493 repo.ui.warn(_('warning: %s cannot merge change/delete conflict '
494 'for %s\n') % (tool, fcd.path()))
494 'for %s\n') % (tool, fcd.path()))
495 return False, 1, None
495 return False, 1, None
496 unused, unused, unused, back = files
496 unused, unused, unused, back = files
497 a = _workingpath(repo, fcd)
497 a = _workingpath(repo, fcd)
498 b, c = _maketempfiles(repo, fco, fca)
498 b, c = _maketempfiles(repo, fco, fca)
499 try:
499 try:
500 out = ""
500 out = ""
501 env = {'HG_FILE': fcd.path(),
501 env = {'HG_FILE': fcd.path(),
502 'HG_MY_NODE': short(mynode),
502 'HG_MY_NODE': short(mynode),
503 'HG_OTHER_NODE': str(fco.changectx()),
503 'HG_OTHER_NODE': str(fco.changectx()),
504 'HG_BASE_NODE': str(fca.changectx()),
504 'HG_BASE_NODE': str(fca.changectx()),
505 'HG_MY_ISLINK': 'l' in fcd.flags(),
505 'HG_MY_ISLINK': 'l' in fcd.flags(),
506 'HG_OTHER_ISLINK': 'l' in fco.flags(),
506 'HG_OTHER_ISLINK': 'l' in fco.flags(),
507 'HG_BASE_ISLINK': 'l' in fca.flags(),
507 'HG_BASE_ISLINK': 'l' in fca.flags(),
508 }
508 }
509 ui = repo.ui
509 ui = repo.ui
510
510
511 args = _toolstr(ui, tool, "args", '$local $base $other')
511 args = _toolstr(ui, tool, "args", '$local $base $other')
512 if "$output" in args:
512 if "$output" in args:
513 # read input from backup, write to original
513 # read input from backup, write to original
514 out = a
514 out = a
515 a = repo.wvfs.join(back.path())
515 a = repo.wvfs.join(back.path())
516 replace = {'local': a, 'base': b, 'other': c, 'output': out}
516 replace = {'local': a, 'base': b, 'other': c, 'output': out}
517 args = util.interpolate(r'\$', replace, args,
517 args = util.interpolate(r'\$', replace, args,
518 lambda s: util.shellquote(util.localpath(s)))
518 lambda s: util.shellquote(util.localpath(s)))
519 cmd = toolpath + ' ' + args
519 cmd = toolpath + ' ' + args
520 if _toolbool(ui, tool, "gui"):
520 if _toolbool(ui, tool, "gui"):
521 repo.ui.status(_('running merge tool %s for file %s\n') %
521 repo.ui.status(_('running merge tool %s for file %s\n') %
522 (tool, fcd.path()))
522 (tool, fcd.path()))
523 repo.ui.debug('launching merge tool: %s\n' % cmd)
523 repo.ui.debug('launching merge tool: %s\n' % cmd)
524 r = ui.system(cmd, cwd=repo.root, environ=env, blockedtag='mergetool')
524 r = ui.system(cmd, cwd=repo.root, environ=env, blockedtag='mergetool')
525 repo.ui.debug('merge tool returned: %d\n' % r)
525 repo.ui.debug('merge tool returned: %d\n' % r)
526 return True, r, False
526 return True, r, False
527 finally:
527 finally:
528 util.unlink(b)
528 util.unlink(b)
529 util.unlink(c)
529 util.unlink(c)
530
530
531 def _formatconflictmarker(repo, ctx, template, label, pad):
531 def _formatconflictmarker(repo, ctx, template, label, pad):
532 """Applies the given template to the ctx, prefixed by the label.
532 """Applies the given template to the ctx, prefixed by the label.
533
533
534 Pad is the minimum width of the label prefix, so that multiple markers
534 Pad is the minimum width of the label prefix, so that multiple markers
535 can have aligned templated parts.
535 can have aligned templated parts.
536 """
536 """
537 if ctx.node() is None:
537 if ctx.node() is None:
538 ctx = ctx.p1()
538 ctx = ctx.p1()
539
539
540 props = templatekw.keywords.copy()
540 props = templatekw.keywords.copy()
541 props['templ'] = template
541 props['templ'] = template
542 props['ctx'] = ctx
542 props['ctx'] = ctx
543 props['repo'] = repo
543 props['repo'] = repo
544 templateresult = template.render(props)
544 templateresult = template.render(props)
545
545
546 label = ('%s:' % label).ljust(pad + 1)
546 label = ('%s:' % label).ljust(pad + 1)
547 mark = '%s %s' % (label, templateresult)
547 mark = '%s %s' % (label, templateresult)
548
548
549 if mark:
549 if mark:
550 mark = mark.splitlines()[0] # split for safety
550 mark = mark.splitlines()[0] # split for safety
551
551
552 # 8 for the prefix of conflict marker lines (e.g. '<<<<<<< ')
552 # 8 for the prefix of conflict marker lines (e.g. '<<<<<<< ')
553 return util.ellipsis(mark, 80 - 8)
553 return util.ellipsis(mark, 80 - 8)
554
554
555 _defaultconflictlabels = ['local', 'other']
555 _defaultconflictlabels = ['local', 'other']
556
556
557 def _formatlabels(repo, fcd, fco, fca, labels):
557 def _formatlabels(repo, fcd, fco, fca, labels):
558 """Formats the given labels using the conflict marker template.
558 """Formats the given labels using the conflict marker template.
559
559
560 Returns a list of formatted labels.
560 Returns a list of formatted labels.
561 """
561 """
562 cd = fcd.changectx()
562 cd = fcd.changectx()
563 co = fco.changectx()
563 co = fco.changectx()
564 ca = fca.changectx()
564 ca = fca.changectx()
565
565
566 ui = repo.ui
566 ui = repo.ui
567 template = ui.config('ui', 'mergemarkertemplate')
567 template = ui.config('ui', 'mergemarkertemplate')
568 template = templater.unquotestring(template)
568 template = templater.unquotestring(template)
569 tmpl = formatter.maketemplater(ui, template)
569 tmpl = formatter.maketemplater(ui, template)
570
570
571 pad = max(len(l) for l in labels)
571 pad = max(len(l) for l in labels)
572
572
573 newlabels = [_formatconflictmarker(repo, cd, tmpl, labels[0], pad),
573 newlabels = [_formatconflictmarker(repo, cd, tmpl, labels[0], pad),
574 _formatconflictmarker(repo, co, tmpl, labels[1], pad)]
574 _formatconflictmarker(repo, co, tmpl, labels[1], pad)]
575 if len(labels) > 2:
575 if len(labels) > 2:
576 newlabels.append(_formatconflictmarker(repo, ca, tmpl, labels[2], pad))
576 newlabels.append(_formatconflictmarker(repo, ca, tmpl, labels[2], pad))
577 return newlabels
577 return newlabels
578
578
579 def partextras(labels):
579 def partextras(labels):
580 """Return a dictionary of extra labels for use in prompts to the user
580 """Return a dictionary of extra labels for use in prompts to the user
581
581
582 Intended use is in strings of the form "(l)ocal%(l)s".
582 Intended use is in strings of the form "(l)ocal%(l)s".
583 """
583 """
584 if labels is None:
584 if labels is None:
585 return {
585 return {
586 "l": "",
586 "l": "",
587 "o": "",
587 "o": "",
588 }
588 }
589
589
590 return {
590 return {
591 "l": " [%s]" % labels[0],
591 "l": " [%s]" % labels[0],
592 "o": " [%s]" % labels[1],
592 "o": " [%s]" % labels[1],
593 }
593 }
594
594
595 def _restorebackup(fcd, back):
595 def _restorebackup(fcd, back):
596 # TODO: Add a workingfilectx.write(otherfilectx) path so we can use
596 # TODO: Add a workingfilectx.write(otherfilectx) path so we can use
597 # util.copy here instead.
597 # util.copy here instead.
598 fcd.write(back.data(), fcd.flags())
598 fcd.write(back.data(), fcd.flags())
599
599
600 def _makebackup(repo, ui, wctx, fcd, premerge):
600 def _makebackup(repo, ui, wctx, fcd, premerge):
601 """Makes and returns a filectx-like object for ``fcd``'s backup file.
601 """Makes and returns a filectx-like object for ``fcd``'s backup file.
602
602
603 In addition to preserving the user's pre-existing modifications to `fcd`
603 In addition to preserving the user's pre-existing modifications to `fcd`
604 (if any), the backup is used to undo certain premerges, confirm whether a
604 (if any), the backup is used to undo certain premerges, confirm whether a
605 merge changed anything, and determine what line endings the new file should
605 merge changed anything, and determine what line endings the new file should
606 have.
606 have.
607 """
607 """
608 if fcd.isabsent():
608 if fcd.isabsent():
609 return None
609 return None
610 # TODO: Break this import cycle somehow. (filectx -> ctx -> fileset ->
610 # TODO: Break this import cycle somehow. (filectx -> ctx -> fileset ->
611 # merge -> filemerge). (I suspect the fileset import is the weakest link)
611 # merge -> filemerge). (I suspect the fileset import is the weakest link)
612 from . import context
612 from . import context
613 a = _workingpath(repo, fcd)
613 a = _workingpath(repo, fcd)
614 back = scmutil.origpath(ui, repo, a)
614 back = scmutil.origpath(ui, repo, a)
615 inworkingdir = (back.startswith(repo.wvfs.base) and not
615 inworkingdir = (back.startswith(repo.wvfs.base) and not
616 back.startswith(repo.vfs.base))
616 back.startswith(repo.vfs.base))
617
617
618 if isinstance(fcd, context.overlayworkingfilectx) and inworkingdir:
618 if isinstance(fcd, context.overlayworkingfilectx) and inworkingdir:
619 # If the backup file is to be in the working directory, and we're
619 # If the backup file is to be in the working directory, and we're
620 # merging in-memory, we must redirect the backup to the memory context
620 # merging in-memory, we must redirect the backup to the memory context
621 # so we don't disturb the working directory.
621 # so we don't disturb the working directory.
622 relpath = back[len(repo.wvfs.base) + 1:]
622 relpath = back[len(repo.wvfs.base) + 1:]
623 wctx[relpath].write(fcd.data(), fcd.flags())
623 wctx[relpath].write(fcd.data(), fcd.flags())
624 return wctx[relpath]
624 return wctx[relpath]
625 else:
625 else:
626 # Otherwise, write to wherever the user specified the backups should go.
626 # Otherwise, write to wherever the user specified the backups should go.
627 #
627 #
628 # A arbitraryfilectx is returned, so we can run the same functions on
628 # A arbitraryfilectx is returned, so we can run the same functions on
629 # the backup context regardless of where it lives.
629 # the backup context regardless of where it lives.
630 if premerge:
630 if premerge:
631 util.copyfile(a, back)
631 util.copyfile(a, back)
632 return context.arbitraryfilectx(back, repo=repo)
632 return context.arbitraryfilectx(back, repo=repo)
633
633
634 def _maketempfiles(repo, fco, fca):
634 def _maketempfiles(repo, fco, fca):
635 """Writes out `fco` and `fca` as temporary files, so an external merge
635 """Writes out `fco` and `fca` as temporary files, so an external merge
636 tool may use them.
636 tool may use them.
637 """
637 """
638 def temp(prefix, ctx):
638 def temp(prefix, ctx):
639 fullbase, ext = os.path.splitext(ctx.path())
639 fullbase, ext = os.path.splitext(ctx.path())
640 pre = "%s~%s." % (os.path.basename(fullbase), prefix)
640 pre = "%s~%s." % (os.path.basename(fullbase), prefix)
641 (fd, name) = tempfile.mkstemp(prefix=pre, suffix=ext)
641 (fd, name) = tempfile.mkstemp(prefix=pre, suffix=ext)
642 data = repo.wwritedata(ctx.path(), ctx.data())
642 data = repo.wwritedata(ctx.path(), ctx.data())
643 f = os.fdopen(fd, pycompat.sysstr("wb"))
643 f = os.fdopen(fd, pycompat.sysstr("wb"))
644 f.write(data)
644 f.write(data)
645 f.close()
645 f.close()
646 return name
646 return name
647
647
648 b = temp("base", fca)
648 b = temp("base", fca)
649 c = temp("other", fco)
649 c = temp("other", fco)
650
650
651 return b, c
651 return b, c
652
652
653 def _filemerge(premerge, repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
653 def _filemerge(premerge, repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
654 """perform a 3-way merge in the working directory
654 """perform a 3-way merge in the working directory
655
655
656 premerge = whether this is a premerge
656 premerge = whether this is a premerge
657 mynode = parent node before merge
657 mynode = parent node before merge
658 orig = original local filename before merge
658 orig = original local filename before merge
659 fco = other file context
659 fco = other file context
660 fca = ancestor file context
660 fca = ancestor file context
661 fcd = local file context for current/destination file
661 fcd = local file context for current/destination file
662
662
663 Returns whether the merge is complete, the return value of the merge, and
663 Returns whether the merge is complete, the return value of the merge, and
664 a boolean indicating whether the file was deleted from disk."""
664 a boolean indicating whether the file was deleted from disk."""
665
665
666 if not fco.cmp(fcd): # files identical?
666 if not fco.cmp(fcd): # files identical?
667 return True, None, False
667 return True, None, False
668
668
669 ui = repo.ui
669 ui = repo.ui
670 fd = fcd.path()
670 fd = fcd.path()
671 binary = fcd.isbinary() or fco.isbinary() or fca.isbinary()
671 binary = fcd.isbinary() or fco.isbinary() or fca.isbinary()
672 symlink = 'l' in fcd.flags() + fco.flags()
672 symlink = 'l' in fcd.flags() + fco.flags()
673 changedelete = fcd.isabsent() or fco.isabsent()
673 changedelete = fcd.isabsent() or fco.isabsent()
674 tool, toolpath = _picktool(repo, ui, fd, binary, symlink, changedelete)
674 tool, toolpath = _picktool(repo, ui, fd, binary, symlink, changedelete)
675 if tool in internals and tool.startswith('internal:'):
675 if tool in internals and tool.startswith('internal:'):
676 # normalize to new-style names (':merge' etc)
676 # normalize to new-style names (':merge' etc)
677 tool = tool[len('internal'):]
677 tool = tool[len('internal'):]
678 ui.debug("picked tool '%s' for %s (binary %s symlink %s changedelete %s)\n"
678 ui.debug("picked tool '%s' for %s (binary %s symlink %s changedelete %s)\n"
679 % (tool, fd, pycompat.bytestr(binary), pycompat.bytestr(symlink),
679 % (tool, fd, pycompat.bytestr(binary), pycompat.bytestr(symlink),
680 pycompat.bytestr(changedelete)))
680 pycompat.bytestr(changedelete)))
681
681
682 if tool in internals:
682 if tool in internals:
683 func = internals[tool]
683 func = internals[tool]
684 mergetype = func.mergetype
684 mergetype = func.mergetype
685 onfailure = func.onfailure
685 onfailure = func.onfailure
686 precheck = func.precheck
686 precheck = func.precheck
687 else:
687 else:
688 func = _xmerge
688 func = _xmerge
689 mergetype = fullmerge
689 mergetype = fullmerge
690 onfailure = _("merging %s failed!\n")
690 onfailure = _("merging %s failed!\n")
691 precheck = None
691 precheck = None
692
692
693 # If using deferred writes, must flush any deferred contents if running
693 # If using deferred writes, must flush any deferred contents if running
694 # an external merge tool since it has arbitrary access to the working
694 # an external merge tool since it has arbitrary access to the working
695 # copy.
695 # copy.
696 wctx.flushall()
696 wctx.flushall()
697
697
698 toolconf = tool, toolpath, binary, symlink
698 toolconf = tool, toolpath, binary, symlink
699
699
700 if mergetype == nomerge:
700 if mergetype == nomerge:
701 r, deleted = func(repo, mynode, orig, fcd, fco, fca, toolconf, labels)
701 r, deleted = func(repo, mynode, orig, fcd, fco, fca, toolconf, labels)
702 return True, r, deleted
702 return True, r, deleted
703
703
704 if premerge:
704 if premerge:
705 if orig != fco.path():
705 if orig != fco.path():
706 ui.status(_("merging %s and %s to %s\n") % (orig, fco.path(), fd))
706 ui.status(_("merging %s and %s to %s\n") % (orig, fco.path(), fd))
707 else:
707 else:
708 ui.status(_("merging %s\n") % fd)
708 ui.status(_("merging %s\n") % fd)
709
709
710 ui.debug("my %s other %s ancestor %s\n" % (fcd, fco, fca))
710 ui.debug("my %s other %s ancestor %s\n" % (fcd, fco, fca))
711
711
712 if precheck and not precheck(repo, mynode, orig, fcd, fco, fca,
712 if precheck and not precheck(repo, mynode, orig, fcd, fco, fca,
713 toolconf):
713 toolconf):
714 if onfailure:
714 if onfailure:
715 ui.warn(onfailure % fd)
715 ui.warn(onfailure % fd)
716 return True, 1, False
716 return True, 1, False
717
717
718 back = _makebackup(repo, ui, wctx, fcd, premerge)
718 back = _makebackup(repo, ui, wctx, fcd, premerge)
719 files = (None, None, None, back)
719 files = (None, None, None, back)
720 r = 1
720 r = 1
721 try:
721 try:
722 markerstyle = ui.config('ui', 'mergemarkers')
722 markerstyle = ui.config('ui', 'mergemarkers')
723 if not labels:
723 if not labels:
724 labels = _defaultconflictlabels
724 labels = _defaultconflictlabels
725 if markerstyle != 'basic':
725 if markerstyle != 'basic':
726 labels = _formatlabels(repo, fcd, fco, fca, labels)
726 labels = _formatlabels(repo, fcd, fco, fca, labels)
727
727
728 if premerge and mergetype == fullmerge:
728 if premerge and mergetype == fullmerge:
729 r = _premerge(repo, fcd, fco, fca, toolconf, files, labels=labels)
729 r = _premerge(repo, fcd, fco, fca, toolconf, files, labels=labels)
730 # complete if premerge successful (r is 0)
730 # complete if premerge successful (r is 0)
731 return not r, r, False
731 return not r, r, False
732
732
733 needcheck, r, deleted = func(repo, mynode, orig, fcd, fco, fca,
733 needcheck, r, deleted = func(repo, mynode, orig, fcd, fco, fca,
734 toolconf, files, labels=labels)
734 toolconf, files, labels=labels)
735
735
736 if needcheck:
736 if needcheck:
737 r = _check(repo, r, ui, tool, fcd, files)
737 r = _check(repo, r, ui, tool, fcd, files)
738
738
739 if r:
739 if r:
740 if onfailure:
740 if onfailure:
741 ui.warn(onfailure % fd)
741 ui.warn(onfailure % fd)
742 _onfilemergefailure(ui)
742
743
743 return True, r, deleted
744 return True, r, deleted
744 finally:
745 finally:
745 if not r and back is not None:
746 if not r and back is not None:
746 back.remove()
747 back.remove()
747
748
748 def _haltmerge():
749 def _haltmerge():
749 msg = _('merge halted after failed merge (see hg resolve)')
750 msg = _('merge halted after failed merge (see hg resolve)')
750 raise error.InterventionRequired(msg)
751 raise error.InterventionRequired(msg)
751
752
752 def _onfilemergefailure(ui):
753 def _onfilemergefailure(ui):
753 action = ui.config('merge', 'on-failure')
754 action = ui.config('merge', 'on-failure')
754 if action == 'prompt':
755 if action == 'prompt':
755 msg = _('continue merge operation (yn)?' '$$ &Yes $$ &No')
756 msg = _('continue merge operation (yn)?' '$$ &Yes $$ &No')
756 if ui.promptchoice(msg, 0) == 1:
757 if ui.promptchoice(msg, 0) == 1:
757 _haltmerge()
758 _haltmerge()
758 if action == 'halt':
759 if action == 'halt':
759 _haltmerge()
760 _haltmerge()
760 # default action is 'continue', in which case we neither prompt nor halt
761 # default action is 'continue', in which case we neither prompt nor halt
761
762
762 def _check(repo, r, ui, tool, fcd, files):
763 def _check(repo, r, ui, tool, fcd, files):
763 fd = fcd.path()
764 fd = fcd.path()
764 unused, unused, unused, back = files
765 unused, unused, unused, back = files
765
766
766 if not r and (_toolbool(ui, tool, "checkconflicts") or
767 if not r and (_toolbool(ui, tool, "checkconflicts") or
767 'conflicts' in _toollist(ui, tool, "check")):
768 'conflicts' in _toollist(ui, tool, "check")):
768 if re.search("^(<<<<<<< .*|=======|>>>>>>> .*)$", fcd.data(),
769 if re.search("^(<<<<<<< .*|=======|>>>>>>> .*)$", fcd.data(),
769 re.MULTILINE):
770 re.MULTILINE):
770 r = 1
771 r = 1
771
772
772 checked = False
773 checked = False
773 if 'prompt' in _toollist(ui, tool, "check"):
774 if 'prompt' in _toollist(ui, tool, "check"):
774 checked = True
775 checked = True
775 if ui.promptchoice(_("was merge of '%s' successful (yn)?"
776 if ui.promptchoice(_("was merge of '%s' successful (yn)?"
776 "$$ &Yes $$ &No") % fd, 1):
777 "$$ &Yes $$ &No") % fd, 1):
777 r = 1
778 r = 1
778
779
779 if not r and not checked and (_toolbool(ui, tool, "checkchanged") or
780 if not r and not checked and (_toolbool(ui, tool, "checkchanged") or
780 'changed' in
781 'changed' in
781 _toollist(ui, tool, "check")):
782 _toollist(ui, tool, "check")):
782 if back is not None and not fcd.cmp(back):
783 if back is not None and not fcd.cmp(back):
783 if ui.promptchoice(_(" output file %s appears unchanged\n"
784 if ui.promptchoice(_(" output file %s appears unchanged\n"
784 "was merge successful (yn)?"
785 "was merge successful (yn)?"
785 "$$ &Yes $$ &No") % fd, 1):
786 "$$ &Yes $$ &No") % fd, 1):
786 r = 1
787 r = 1
787
788
788 if back is not None and _toolbool(ui, tool, "fixeol"):
789 if back is not None and _toolbool(ui, tool, "fixeol"):
789 _matcheol(_workingpath(repo, fcd), back)
790 _matcheol(_workingpath(repo, fcd), back)
790
791
791 return r
792 return r
792
793
793 def _workingpath(repo, ctx):
794 def _workingpath(repo, ctx):
794 return repo.wjoin(ctx.path())
795 return repo.wjoin(ctx.path())
795
796
796 def premerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
797 def premerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
797 return _filemerge(True, repo, wctx, mynode, orig, fcd, fco, fca,
798 return _filemerge(True, repo, wctx, mynode, orig, fcd, fco, fca,
798 labels=labels)
799 labels=labels)
799
800
800 def filemerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
801 def filemerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
801 return _filemerge(False, repo, wctx, mynode, orig, fcd, fco, fca,
802 return _filemerge(False, repo, wctx, mynode, orig, fcd, fco, fca,
802 labels=labels)
803 labels=labels)
803
804
804 def loadinternalmerge(ui, extname, registrarobj):
805 def loadinternalmerge(ui, extname, registrarobj):
805 """Load internal merge tool from specified registrarobj
806 """Load internal merge tool from specified registrarobj
806 """
807 """
807 for name, func in registrarobj._table.iteritems():
808 for name, func in registrarobj._table.iteritems():
808 fullname = ':' + name
809 fullname = ':' + name
809 internals[fullname] = func
810 internals[fullname] = func
810 internals['internal:' + name] = func
811 internals['internal:' + name] = func
811 internalsdoc[fullname] = func
812 internalsdoc[fullname] = func
812
813
813 # load built-in merge tools explicitly to setup internalsdoc
814 # load built-in merge tools explicitly to setup internalsdoc
814 loadinternalmerge(None, None, internaltool)
815 loadinternalmerge(None, None, internaltool)
815
816
816 # tell hggettext to extract docstrings from these functions:
817 # tell hggettext to extract docstrings from these functions:
817 i18nfunctions = internals.values()
818 i18nfunctions = internals.values()
@@ -1,78 +1,79 b''
1 $ cat >> $HGRCPATH <<EOF
1 $ cat >> $HGRCPATH <<EOF
2 > [extensions]
2 > [extensions]
3 > rebase=
3 > rebase=
4 > [phases]
4 > [phases]
5 > publish=False
5 > publish=False
6 > [merge]
6 > [merge]
7 > EOF
7 > EOF
8
8
9 $ hg init repo
9 $ hg init repo
10 $ cd repo
10 $ cd repo
11 $ echo a > a
11 $ echo a > a
12 $ echo b > b
12 $ echo b > b
13 $ hg commit -qAm ab
13 $ hg commit -qAm ab
14 $ echo c >> a
14 $ echo c >> a
15 $ echo c >> b
15 $ echo c >> b
16 $ hg commit -qAm c
16 $ hg commit -qAm c
17 $ hg up -q ".^"
17 $ hg up -q ".^"
18 $ echo d >> a
18 $ echo d >> a
19 $ echo d >> b
19 $ echo d >> b
20 $ hg commit -qAm d
20 $ hg commit -qAm d
21
21
22 Testing on-failure=continue
22 Testing on-failure=continue
23 $ echo on-failure=continue >> $HGRCPATH
23 $ echo on-failure=continue >> $HGRCPATH
24 $ hg rebase -s 1 -d 2 --tool false
24 $ hg rebase -s 1 -d 2 --tool false
25 rebasing 1:1f28a51c3c9b "c"
25 rebasing 1:1f28a51c3c9b "c"
26 merging a
26 merging a
27 merging b
27 merging b
28 merging a failed!
28 merging a failed!
29 merging b failed!
29 merging b failed!
30 unresolved conflicts (see hg resolve, then hg rebase --continue)
30 unresolved conflicts (see hg resolve, then hg rebase --continue)
31 [1]
31 [1]
32
32
33 $ hg resolve --list
33 $ hg resolve --list
34 U a
34 U a
35 U b
35 U b
36
36
37 $ hg rebase --abort
37 $ hg rebase --abort
38 rebase aborted
38 rebase aborted
39
39
40 Testing on-failure=halt
40 Testing on-failure=halt
41 $ echo on-failure=halt >> $HGRCPATH
41 $ echo on-failure=halt >> $HGRCPATH
42 $ hg rebase -s 1 -d 2 --tool false
42 $ hg rebase -s 1 -d 2 --tool false
43 rebasing 1:1f28a51c3c9b "c"
43 rebasing 1:1f28a51c3c9b "c"
44 merging a
44 merging a
45 merging b
45 merging b
46 merging a failed!
46 merging a failed!
47 merging b failed!
47 merge halted after failed merge (see hg resolve)
48 unresolved conflicts (see hg resolve, then hg rebase --continue)
49 [1]
48 [1]
50
49
51 $ hg resolve --list
50 $ hg resolve --list
52 U a
51 U a
53 U b
52 U b
54
53
55 $ hg rebase --abort
54 $ hg rebase --abort
56 rebase aborted
55 rebase aborted
57
56
58 Testing on-failure=prompt
57 Testing on-failure=prompt
59 $ echo on-failure=prompt >> $HGRCPATH
58 $ echo on-failure=prompt >> $HGRCPATH
60 $ cat <<EOS | hg rebase -s 1 -d 2 --tool false --config ui.interactive=1
59 $ cat <<EOS | hg rebase -s 1 -d 2 --tool false --config ui.interactive=1
61 > y
60 > y
62 > n
61 > n
63 > EOS
62 > EOS
64 rebasing 1:1f28a51c3c9b "c"
63 rebasing 1:1f28a51c3c9b "c"
65 merging a
64 merging a
66 merging b
65 merging b
67 merging a failed!
66 merging a failed!
67 continue merge operation (yn)? y
68 merging b failed!
68 merging b failed!
69 unresolved conflicts (see hg resolve, then hg rebase --continue)
69 continue merge operation (yn)? n
70 merge halted after failed merge (see hg resolve)
70 [1]
71 [1]
71
72
72 $ hg resolve --list
73 $ hg resolve --list
73 U a
74 U a
74 U b
75 U b
75
76
76 $ hg rebase --abort
77 $ hg rebase --abort
77 rebase aborted
78 rebase aborted
78
79
General Comments 0
You need to be logged in to leave comments. Login now