##// END OF EJS Templates
filemerge: don't try using external tools on change/delete conflicts...
Siddharth Agarwal -
r27042:30b919bc default
parent child Browse files
Show More
@@ -1,672 +1,676
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 filecmp
10 import filecmp
11 import os
11 import os
12 import re
12 import re
13 import tempfile
13 import tempfile
14
14
15 from .i18n import _
15 from .i18n import _
16 from .node import nullid, short
16 from .node import nullid, short
17
17
18 from . import (
18 from . import (
19 cmdutil,
19 cmdutil,
20 error,
20 error,
21 match,
21 match,
22 simplemerge,
22 simplemerge,
23 tagmerge,
23 tagmerge,
24 templatekw,
24 templatekw,
25 templater,
25 templater,
26 util,
26 util,
27 )
27 )
28
28
29 def _toolstr(ui, tool, part, default=""):
29 def _toolstr(ui, tool, part, default=""):
30 return ui.config("merge-tools", tool + "." + part, default)
30 return ui.config("merge-tools", tool + "." + part, default)
31
31
32 def _toolbool(ui, tool, part, default=False):
32 def _toolbool(ui, tool, part, default=False):
33 return ui.configbool("merge-tools", tool + "." + part, default)
33 return ui.configbool("merge-tools", tool + "." + part, default)
34
34
35 def _toollist(ui, tool, part, default=[]):
35 def _toollist(ui, tool, part, default=[]):
36 return ui.configlist("merge-tools", tool + "." + part, default)
36 return ui.configlist("merge-tools", tool + "." + part, default)
37
37
38 internals = {}
38 internals = {}
39 # Merge tools to document.
39 # Merge tools to document.
40 internalsdoc = {}
40 internalsdoc = {}
41
41
42 # internal tool merge types
42 # internal tool merge types
43 nomerge = None
43 nomerge = None
44 mergeonly = 'mergeonly' # just the full merge, no premerge
44 mergeonly = 'mergeonly' # just the full merge, no premerge
45 fullmerge = 'fullmerge' # both premerge and merge
45 fullmerge = 'fullmerge' # both premerge and merge
46
46
47 class absentfilectx(object):
47 class absentfilectx(object):
48 """Represents a file that's ostensibly in a context but is actually not
48 """Represents a file that's ostensibly in a context but is actually not
49 present in it.
49 present in it.
50
50
51 This is here because it's very specific to the filemerge code for now --
51 This is here because it's very specific to the filemerge code for now --
52 other code is likely going to break with the values this returns."""
52 other code is likely going to break with the values this returns."""
53 def __init__(self, ctx, f):
53 def __init__(self, ctx, f):
54 self._ctx = ctx
54 self._ctx = ctx
55 self._f = f
55 self._f = f
56
56
57 def path(self):
57 def path(self):
58 return self._f
58 return self._f
59
59
60 def size(self):
60 def size(self):
61 return None
61 return None
62
62
63 def data(self):
63 def data(self):
64 return None
64 return None
65
65
66 def filenode(self):
66 def filenode(self):
67 return nullid
67 return nullid
68
68
69 _customcmp = True
69 _customcmp = True
70 def cmp(self, fctx):
70 def cmp(self, fctx):
71 """compare with other file context
71 """compare with other file context
72
72
73 returns True if different from fctx.
73 returns True if different from fctx.
74 """
74 """
75 return not (fctx.isabsent() and
75 return not (fctx.isabsent() and
76 fctx.ctx() == self.ctx() and
76 fctx.ctx() == self.ctx() and
77 fctx.path() == self.path())
77 fctx.path() == self.path())
78
78
79 def flags(self):
79 def flags(self):
80 return ''
80 return ''
81
81
82 def changectx(self):
82 def changectx(self):
83 return self._ctx
83 return self._ctx
84
84
85 def isbinary(self):
85 def isbinary(self):
86 return False
86 return False
87
87
88 def isabsent(self):
88 def isabsent(self):
89 return True
89 return True
90
90
91 def internaltool(name, mergetype, onfailure=None, precheck=None):
91 def internaltool(name, mergetype, onfailure=None, precheck=None):
92 '''return a decorator for populating internal merge tool table'''
92 '''return a decorator for populating internal merge tool table'''
93 def decorator(func):
93 def decorator(func):
94 fullname = ':' + name
94 fullname = ':' + name
95 func.__doc__ = "``%s``\n" % fullname + func.__doc__.strip()
95 func.__doc__ = "``%s``\n" % fullname + func.__doc__.strip()
96 internals[fullname] = func
96 internals[fullname] = func
97 internals['internal:' + name] = func
97 internals['internal:' + name] = func
98 internalsdoc[fullname] = func
98 internalsdoc[fullname] = func
99 func.mergetype = mergetype
99 func.mergetype = mergetype
100 func.onfailure = onfailure
100 func.onfailure = onfailure
101 func.precheck = precheck
101 func.precheck = precheck
102 return func
102 return func
103 return decorator
103 return decorator
104
104
105 def _findtool(ui, tool):
105 def _findtool(ui, tool):
106 if tool in internals:
106 if tool in internals:
107 return tool
107 return tool
108 return findexternaltool(ui, tool)
108 return findexternaltool(ui, tool)
109
109
110 def findexternaltool(ui, tool):
110 def findexternaltool(ui, tool):
111 for kn in ("regkey", "regkeyalt"):
111 for kn in ("regkey", "regkeyalt"):
112 k = _toolstr(ui, tool, kn)
112 k = _toolstr(ui, tool, kn)
113 if not k:
113 if not k:
114 continue
114 continue
115 p = util.lookupreg(k, _toolstr(ui, tool, "regname"))
115 p = util.lookupreg(k, _toolstr(ui, tool, "regname"))
116 if p:
116 if p:
117 p = util.findexe(p + _toolstr(ui, tool, "regappend"))
117 p = util.findexe(p + _toolstr(ui, tool, "regappend"))
118 if p:
118 if p:
119 return p
119 return p
120 exe = _toolstr(ui, tool, "executable", tool)
120 exe = _toolstr(ui, tool, "executable", tool)
121 return util.findexe(util.expandpath(exe))
121 return util.findexe(util.expandpath(exe))
122
122
123 def _picktool(repo, ui, path, binary, symlink, changedelete):
123 def _picktool(repo, ui, path, binary, symlink, changedelete):
124 def supportscd(tool):
124 def supportscd(tool):
125 return tool in internals and internals[tool].mergetype == nomerge
125 return tool in internals and internals[tool].mergetype == nomerge
126
126
127 def check(tool, pat, symlink, binary, changedelete):
127 def check(tool, pat, symlink, binary, changedelete):
128 tmsg = tool
128 tmsg = tool
129 if pat:
129 if pat:
130 tmsg += " specified for " + pat
130 tmsg += " specified for " + pat
131 if not _findtool(ui, tool):
131 if not _findtool(ui, tool):
132 if pat: # explicitly requested tool deserves a warning
132 if pat: # explicitly requested tool deserves a warning
133 ui.warn(_("couldn't find merge tool %s\n") % tmsg)
133 ui.warn(_("couldn't find merge tool %s\n") % tmsg)
134 else: # configured but non-existing tools are more silent
134 else: # configured but non-existing tools are more silent
135 ui.note(_("couldn't find merge tool %s\n") % tmsg)
135 ui.note(_("couldn't find merge tool %s\n") % tmsg)
136 elif symlink and not _toolbool(ui, tool, "symlink"):
136 elif symlink and not _toolbool(ui, tool, "symlink"):
137 ui.warn(_("tool %s can't handle symlinks\n") % tmsg)
137 ui.warn(_("tool %s can't handle symlinks\n") % tmsg)
138 elif binary and not _toolbool(ui, tool, "binary"):
138 elif binary and not _toolbool(ui, tool, "binary"):
139 ui.warn(_("tool %s can't handle binary\n") % tmsg)
139 ui.warn(_("tool %s can't handle binary\n") % tmsg)
140 elif changedelete and not supportscd(tool):
140 elif changedelete and not supportscd(tool):
141 # the nomerge tools are the only tools that support change/delete
141 # the nomerge tools are the only tools that support change/delete
142 # conflicts
142 # conflicts
143 pass
143 pass
144 elif not util.gui() and _toolbool(ui, tool, "gui"):
144 elif not util.gui() and _toolbool(ui, tool, "gui"):
145 ui.warn(_("tool %s requires a GUI\n") % tmsg)
145 ui.warn(_("tool %s requires a GUI\n") % tmsg)
146 else:
146 else:
147 return True
147 return True
148 return False
148 return False
149
149
150 # internal config: ui.forcemerge
150 # internal config: ui.forcemerge
151 # forcemerge comes from command line arguments, highest priority
151 # forcemerge comes from command line arguments, highest priority
152 force = ui.config('ui', 'forcemerge')
152 force = ui.config('ui', 'forcemerge')
153 if force:
153 if force:
154 toolpath = _findtool(ui, force)
154 toolpath = _findtool(ui, force)
155 if changedelete and not supportscd(toolpath):
155 if changedelete and not supportscd(toolpath):
156 return ":prompt", None
156 return ":prompt", None
157 else:
157 else:
158 if toolpath:
158 if toolpath:
159 return (force, util.shellquote(toolpath))
159 return (force, util.shellquote(toolpath))
160 else:
160 else:
161 # mimic HGMERGE if given tool not found
161 # mimic HGMERGE if given tool not found
162 return (force, force)
162 return (force, force)
163
163
164 # HGMERGE takes next precedence
164 # HGMERGE takes next precedence
165 hgmerge = os.environ.get("HGMERGE")
165 hgmerge = os.environ.get("HGMERGE")
166 if hgmerge:
166 if hgmerge:
167 if changedelete and not supportscd(hgmerge):
167 if changedelete and not supportscd(hgmerge):
168 return ":prompt", None
168 return ":prompt", None
169 else:
169 else:
170 return (hgmerge, hgmerge)
170 return (hgmerge, hgmerge)
171
171
172 # then patterns
172 # then patterns
173 for pat, tool in ui.configitems("merge-patterns"):
173 for pat, tool in ui.configitems("merge-patterns"):
174 mf = match.match(repo.root, '', [pat])
174 mf = match.match(repo.root, '', [pat])
175 if mf(path) and check(tool, pat, symlink, False, changedelete):
175 if mf(path) and check(tool, pat, symlink, False, changedelete):
176 toolpath = _findtool(ui, tool)
176 toolpath = _findtool(ui, tool)
177 return (tool, util.shellquote(toolpath))
177 return (tool, util.shellquote(toolpath))
178
178
179 # then merge tools
179 # then merge tools
180 tools = {}
180 tools = {}
181 disabled = set()
181 disabled = set()
182 for k, v in ui.configitems("merge-tools"):
182 for k, v in ui.configitems("merge-tools"):
183 t = k.split('.')[0]
183 t = k.split('.')[0]
184 if t not in tools:
184 if t not in tools:
185 tools[t] = int(_toolstr(ui, t, "priority", "0"))
185 tools[t] = int(_toolstr(ui, t, "priority", "0"))
186 if _toolbool(ui, t, "disabled", False):
186 if _toolbool(ui, t, "disabled", False):
187 disabled.add(t)
187 disabled.add(t)
188 names = tools.keys()
188 names = tools.keys()
189 tools = sorted([(-p, t) for t, p in tools.items() if t not in disabled])
189 tools = sorted([(-p, t) for t, p in tools.items() if t not in disabled])
190 uimerge = ui.config("ui", "merge")
190 uimerge = ui.config("ui", "merge")
191 if uimerge:
191 if uimerge:
192 # external tools defined in uimerge won't be able to handle
192 # external tools defined in uimerge won't be able to handle
193 # change/delete conflicts
193 # change/delete conflicts
194 if uimerge not in names and not changedelete:
194 if uimerge not in names and not changedelete:
195 return (uimerge, uimerge)
195 return (uimerge, uimerge)
196 tools.insert(0, (None, uimerge)) # highest priority
196 tools.insert(0, (None, uimerge)) # highest priority
197 tools.append((None, "hgmerge")) # the old default, if found
197 tools.append((None, "hgmerge")) # the old default, if found
198 for p, t in tools:
198 for p, t in tools:
199 if check(t, None, symlink, binary, changedelete):
199 if check(t, None, symlink, binary, changedelete):
200 toolpath = _findtool(ui, t)
200 toolpath = _findtool(ui, t)
201 return (t, util.shellquote(toolpath))
201 return (t, util.shellquote(toolpath))
202
202
203 # internal merge or prompt as last resort
203 # internal merge or prompt as last resort
204 if symlink or binary or changedelete:
204 if symlink or binary or changedelete:
205 return ":prompt", None
205 return ":prompt", None
206 return ":merge", None
206 return ":merge", None
207
207
208 def _eoltype(data):
208 def _eoltype(data):
209 "Guess the EOL type of a file"
209 "Guess the EOL type of a file"
210 if '\0' in data: # binary
210 if '\0' in data: # binary
211 return None
211 return None
212 if '\r\n' in data: # Windows
212 if '\r\n' in data: # Windows
213 return '\r\n'
213 return '\r\n'
214 if '\r' in data: # Old Mac
214 if '\r' in data: # Old Mac
215 return '\r'
215 return '\r'
216 if '\n' in data: # UNIX
216 if '\n' in data: # UNIX
217 return '\n'
217 return '\n'
218 return None # unknown
218 return None # unknown
219
219
220 def _matcheol(file, origfile):
220 def _matcheol(file, origfile):
221 "Convert EOL markers in a file to match origfile"
221 "Convert EOL markers in a file to match origfile"
222 tostyle = _eoltype(util.readfile(origfile))
222 tostyle = _eoltype(util.readfile(origfile))
223 if tostyle:
223 if tostyle:
224 data = util.readfile(file)
224 data = util.readfile(file)
225 style = _eoltype(data)
225 style = _eoltype(data)
226 if style:
226 if style:
227 newdata = data.replace(style, tostyle)
227 newdata = data.replace(style, tostyle)
228 if newdata != data:
228 if newdata != data:
229 util.writefile(file, newdata)
229 util.writefile(file, newdata)
230
230
231 @internaltool('prompt', nomerge)
231 @internaltool('prompt', nomerge)
232 def _iprompt(repo, mynode, orig, fcd, fco, fca, toolconf):
232 def _iprompt(repo, mynode, orig, fcd, fco, fca, toolconf):
233 """Asks the user which of the local or the other version to keep as
233 """Asks the user which of the local or the other version to keep as
234 the merged version."""
234 the merged version."""
235 ui = repo.ui
235 ui = repo.ui
236 fd = fcd.path()
236 fd = fcd.path()
237
237
238 try:
238 try:
239 if fco.isabsent():
239 if fco.isabsent():
240 index = ui.promptchoice(
240 index = ui.promptchoice(
241 _("local changed %s which remote deleted\n"
241 _("local changed %s which remote deleted\n"
242 "use (c)hanged version or (d)elete?"
242 "use (c)hanged version or (d)elete?"
243 "$$ &Changed $$ &Delete") % fd, 0)
243 "$$ &Changed $$ &Delete") % fd, 0)
244 choice = ['local', 'other'][index]
244 choice = ['local', 'other'][index]
245 elif fcd.isabsent():
245 elif fcd.isabsent():
246 index = ui.promptchoice(
246 index = ui.promptchoice(
247 _("remote changed %s which local deleted\n"
247 _("remote changed %s which local deleted\n"
248 "use (c)hanged version or leave (d)eleted?"
248 "use (c)hanged version or leave (d)eleted?"
249 "$$ &Changed $$ &Deleted") % fd, 0)
249 "$$ &Changed $$ &Deleted") % fd, 0)
250 choice = ['other', 'local'][index]
250 choice = ['other', 'local'][index]
251 else:
251 else:
252 index = ui.promptchoice(_("no tool found to merge %s\n"
252 index = ui.promptchoice(_("no tool found to merge %s\n"
253 "keep (l)ocal or take (o)ther?"
253 "keep (l)ocal or take (o)ther?"
254 "$$ &Local $$ &Other") % fd, 0)
254 "$$ &Local $$ &Other") % fd, 0)
255 choice = ['local', 'other'][index]
255 choice = ['local', 'other'][index]
256
256
257 if choice == 'other':
257 if choice == 'other':
258 return _iother(repo, mynode, orig, fcd, fco, fca, toolconf)
258 return _iother(repo, mynode, orig, fcd, fco, fca, toolconf)
259 else:
259 else:
260 return _ilocal(repo, mynode, orig, fcd, fco, fca, toolconf)
260 return _ilocal(repo, mynode, orig, fcd, fco, fca, toolconf)
261 except error.ResponseExpected:
261 except error.ResponseExpected:
262 ui.write("\n")
262 ui.write("\n")
263 return 1, False
263 return 1, False
264
264
265 @internaltool('local', nomerge)
265 @internaltool('local', nomerge)
266 def _ilocal(repo, mynode, orig, fcd, fco, fca, toolconf):
266 def _ilocal(repo, mynode, orig, fcd, fco, fca, toolconf):
267 """Uses the local version of files as the merged version."""
267 """Uses the local version of files as the merged version."""
268 return 0, fcd.isabsent()
268 return 0, fcd.isabsent()
269
269
270 @internaltool('other', nomerge)
270 @internaltool('other', nomerge)
271 def _iother(repo, mynode, orig, fcd, fco, fca, toolconf):
271 def _iother(repo, mynode, orig, fcd, fco, fca, toolconf):
272 """Uses the other version of files as the merged version."""
272 """Uses the other version of files as the merged version."""
273 if fco.isabsent():
273 if fco.isabsent():
274 # local changed, remote deleted -- 'deleted' picked
274 # local changed, remote deleted -- 'deleted' picked
275 repo.wvfs.unlinkpath(fcd.path())
275 repo.wvfs.unlinkpath(fcd.path())
276 deleted = True
276 deleted = True
277 else:
277 else:
278 repo.wwrite(fcd.path(), fco.data(), fco.flags())
278 repo.wwrite(fcd.path(), fco.data(), fco.flags())
279 deleted = False
279 deleted = False
280 return 0, deleted
280 return 0, deleted
281
281
282 @internaltool('fail', nomerge)
282 @internaltool('fail', nomerge)
283 def _ifail(repo, mynode, orig, fcd, fco, fca, toolconf):
283 def _ifail(repo, mynode, orig, fcd, fco, fca, toolconf):
284 """
284 """
285 Rather than attempting to merge files that were modified on both
285 Rather than attempting to merge files that were modified on both
286 branches, it marks them as unresolved. The resolve command must be
286 branches, it marks them as unresolved. The resolve command must be
287 used to resolve these conflicts."""
287 used to resolve these conflicts."""
288 return 1, False
288 return 1, False
289
289
290 def _premerge(repo, fcd, fco, fca, toolconf, files, labels=None):
290 def _premerge(repo, fcd, fco, fca, toolconf, files, labels=None):
291 tool, toolpath, binary, symlink = toolconf
291 tool, toolpath, binary, symlink = toolconf
292 if symlink or fcd.isabsent() or fco.isabsent():
292 if symlink or fcd.isabsent() or fco.isabsent():
293 return 1
293 return 1
294 a, b, c, back = files
294 a, b, c, back = files
295
295
296 ui = repo.ui
296 ui = repo.ui
297
297
298 validkeep = ['keep', 'keep-merge3']
298 validkeep = ['keep', 'keep-merge3']
299
299
300 # do we attempt to simplemerge first?
300 # do we attempt to simplemerge first?
301 try:
301 try:
302 premerge = _toolbool(ui, tool, "premerge", not binary)
302 premerge = _toolbool(ui, tool, "premerge", not binary)
303 except error.ConfigError:
303 except error.ConfigError:
304 premerge = _toolstr(ui, tool, "premerge").lower()
304 premerge = _toolstr(ui, tool, "premerge").lower()
305 if premerge not in validkeep:
305 if premerge not in validkeep:
306 _valid = ', '.join(["'" + v + "'" for v in validkeep])
306 _valid = ', '.join(["'" + v + "'" for v in validkeep])
307 raise error.ConfigError(_("%s.premerge not valid "
307 raise error.ConfigError(_("%s.premerge not valid "
308 "('%s' is neither boolean nor %s)") %
308 "('%s' is neither boolean nor %s)") %
309 (tool, premerge, _valid))
309 (tool, premerge, _valid))
310
310
311 if premerge:
311 if premerge:
312 if premerge == 'keep-merge3':
312 if premerge == 'keep-merge3':
313 if not labels:
313 if not labels:
314 labels = _defaultconflictlabels
314 labels = _defaultconflictlabels
315 if len(labels) < 3:
315 if len(labels) < 3:
316 labels.append('base')
316 labels.append('base')
317 r = simplemerge.simplemerge(ui, a, b, c, quiet=True, label=labels)
317 r = simplemerge.simplemerge(ui, a, b, c, quiet=True, label=labels)
318 if not r:
318 if not r:
319 ui.debug(" premerge successful\n")
319 ui.debug(" premerge successful\n")
320 return 0
320 return 0
321 if premerge not in validkeep:
321 if premerge not in validkeep:
322 util.copyfile(back, a) # restore from backup and try again
322 util.copyfile(back, a) # restore from backup and try again
323 return 1 # continue merging
323 return 1 # continue merging
324
324
325 def _mergecheck(repo, mynode, orig, fcd, fco, fca, toolconf):
325 def _mergecheck(repo, mynode, orig, fcd, fco, fca, toolconf):
326 tool, toolpath, binary, symlink = toolconf
326 tool, toolpath, binary, symlink = toolconf
327 if symlink:
327 if symlink:
328 repo.ui.warn(_('warning: internal %s cannot merge symlinks '
328 repo.ui.warn(_('warning: internal %s cannot merge symlinks '
329 'for %s\n') % (tool, fcd.path()))
329 'for %s\n') % (tool, fcd.path()))
330 return False
330 return False
331 if fcd.isabsent() or fco.isabsent():
331 if fcd.isabsent() or fco.isabsent():
332 repo.ui.warn(_('warning: internal %s cannot merge change/delete '
332 repo.ui.warn(_('warning: internal %s cannot merge change/delete '
333 'conflict for %s\n') % (tool, fcd.path()))
333 'conflict for %s\n') % (tool, fcd.path()))
334 return False
334 return False
335 return True
335 return True
336
336
337 def _merge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels, mode):
337 def _merge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels, mode):
338 """
338 """
339 Uses the internal non-interactive simple merge algorithm for merging
339 Uses the internal non-interactive simple merge algorithm for merging
340 files. It will fail if there are any conflicts and leave markers in
340 files. It will fail if there are any conflicts and leave markers in
341 the partially merged file. Markers will have two sections, one for each side
341 the partially merged file. Markers will have two sections, one for each side
342 of merge, unless mode equals 'union' which suppresses the markers."""
342 of merge, unless mode equals 'union' which suppresses the markers."""
343 a, b, c, back = files
343 a, b, c, back = files
344
344
345 ui = repo.ui
345 ui = repo.ui
346
346
347 r = simplemerge.simplemerge(ui, a, b, c, label=labels, mode=mode)
347 r = simplemerge.simplemerge(ui, a, b, c, label=labels, mode=mode)
348 return True, r, False
348 return True, r, False
349
349
350 @internaltool('union', fullmerge,
350 @internaltool('union', fullmerge,
351 _("warning: conflicts while merging %s! "
351 _("warning: conflicts while merging %s! "
352 "(edit, then use 'hg resolve --mark')\n"),
352 "(edit, then use 'hg resolve --mark')\n"),
353 precheck=_mergecheck)
353 precheck=_mergecheck)
354 def _iunion(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
354 def _iunion(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
355 """
355 """
356 Uses the internal non-interactive simple merge algorithm for merging
356 Uses the internal non-interactive simple merge algorithm for merging
357 files. It will use both left and right sides for conflict regions.
357 files. It will use both left and right sides for conflict regions.
358 No markers are inserted."""
358 No markers are inserted."""
359 return _merge(repo, mynode, orig, fcd, fco, fca, toolconf,
359 return _merge(repo, mynode, orig, fcd, fco, fca, toolconf,
360 files, labels, 'union')
360 files, labels, 'union')
361
361
362 @internaltool('merge', fullmerge,
362 @internaltool('merge', fullmerge,
363 _("warning: conflicts while merging %s! "
363 _("warning: conflicts while merging %s! "
364 "(edit, then use 'hg resolve --mark')\n"),
364 "(edit, then use 'hg resolve --mark')\n"),
365 precheck=_mergecheck)
365 precheck=_mergecheck)
366 def _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
366 def _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
367 """
367 """
368 Uses the internal non-interactive simple merge algorithm for merging
368 Uses the internal non-interactive simple merge algorithm for merging
369 files. It will fail if there are any conflicts and leave markers in
369 files. It will fail if there are any conflicts and leave markers in
370 the partially merged file. Markers will have two sections, one for each side
370 the partially merged file. Markers will have two sections, one for each side
371 of merge."""
371 of merge."""
372 return _merge(repo, mynode, orig, fcd, fco, fca, toolconf,
372 return _merge(repo, mynode, orig, fcd, fco, fca, toolconf,
373 files, labels, 'merge')
373 files, labels, 'merge')
374
374
375 @internaltool('merge3', fullmerge,
375 @internaltool('merge3', 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 _imerge3(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
379 def _imerge3(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 fail if there are any conflicts and leave markers in
382 files. It will fail if there are any conflicts and leave markers in
383 the partially merged file. Marker will have three sections, one from each
383 the partially merged file. Marker will have three sections, one from each
384 side of the merge and one for the base content."""
384 side of the merge and one for the base content."""
385 if not labels:
385 if not labels:
386 labels = _defaultconflictlabels
386 labels = _defaultconflictlabels
387 if len(labels) < 3:
387 if len(labels) < 3:
388 labels.append('base')
388 labels.append('base')
389 return _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels)
389 return _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels)
390
390
391 def _imergeauto(repo, mynode, orig, fcd, fco, fca, toolconf, files,
391 def _imergeauto(repo, mynode, orig, fcd, fco, fca, toolconf, files,
392 labels=None, localorother=None):
392 labels=None, localorother=None):
393 """
393 """
394 Generic driver for _imergelocal and _imergeother
394 Generic driver for _imergelocal and _imergeother
395 """
395 """
396 assert localorother is not None
396 assert localorother is not None
397 tool, toolpath, binary, symlink = toolconf
397 tool, toolpath, binary, symlink = toolconf
398 a, b, c, back = files
398 a, b, c, back = files
399 r = simplemerge.simplemerge(repo.ui, a, b, c, label=labels,
399 r = simplemerge.simplemerge(repo.ui, a, b, c, label=labels,
400 localorother=localorother)
400 localorother=localorother)
401 return True, r
401 return True, r
402
402
403 @internaltool('merge-local', mergeonly, precheck=_mergecheck)
403 @internaltool('merge-local', mergeonly, precheck=_mergecheck)
404 def _imergelocal(*args, **kwargs):
404 def _imergelocal(*args, **kwargs):
405 """
405 """
406 Like :merge, but resolve all conflicts non-interactively in favor
406 Like :merge, but resolve all conflicts non-interactively in favor
407 of the local changes."""
407 of the local changes."""
408 success, status = _imergeauto(localorother='local', *args, **kwargs)
408 success, status = _imergeauto(localorother='local', *args, **kwargs)
409 return success, status, False
409 return success, status, False
410
410
411 @internaltool('merge-other', mergeonly, precheck=_mergecheck)
411 @internaltool('merge-other', mergeonly, precheck=_mergecheck)
412 def _imergeother(*args, **kwargs):
412 def _imergeother(*args, **kwargs):
413 """
413 """
414 Like :merge, but resolve all conflicts non-interactively in favor
414 Like :merge, but resolve all conflicts non-interactively in favor
415 of the other changes."""
415 of the other changes."""
416 success, status = _imergeauto(localorother='other', *args, **kwargs)
416 success, status = _imergeauto(localorother='other', *args, **kwargs)
417 return success, status, False
417 return success, status, False
418
418
419 @internaltool('tagmerge', mergeonly,
419 @internaltool('tagmerge', mergeonly,
420 _("automatic tag merging of %s failed! "
420 _("automatic tag merging of %s failed! "
421 "(use 'hg resolve --tool :merge' or another merge "
421 "(use 'hg resolve --tool :merge' or another merge "
422 "tool of your choice)\n"))
422 "tool of your choice)\n"))
423 def _itagmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
423 def _itagmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
424 """
424 """
425 Uses the internal tag merge algorithm (experimental).
425 Uses the internal tag merge algorithm (experimental).
426 """
426 """
427 success, status = tagmerge.merge(repo, fcd, fco, fca)
427 success, status = tagmerge.merge(repo, fcd, fco, fca)
428 return success, status, False
428 return success, status, False
429
429
430 @internaltool('dump', fullmerge)
430 @internaltool('dump', fullmerge)
431 def _idump(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
431 def _idump(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
432 """
432 """
433 Creates three versions of the files to merge, containing the
433 Creates three versions of the files to merge, containing the
434 contents of local, other and base. These files can then be used to
434 contents of local, other and base. These files can then be used to
435 perform a merge manually. If the file to be merged is named
435 perform a merge manually. If the file to be merged is named
436 ``a.txt``, these files will accordingly be named ``a.txt.local``,
436 ``a.txt``, these files will accordingly be named ``a.txt.local``,
437 ``a.txt.other`` and ``a.txt.base`` and they will be placed in the
437 ``a.txt.other`` and ``a.txt.base`` and they will be placed in the
438 same directory as ``a.txt``."""
438 same directory as ``a.txt``."""
439 a, b, c, back = files
439 a, b, c, back = files
440
440
441 fd = fcd.path()
441 fd = fcd.path()
442
442
443 util.copyfile(a, a + ".local")
443 util.copyfile(a, a + ".local")
444 repo.wwrite(fd + ".other", fco.data(), fco.flags())
444 repo.wwrite(fd + ".other", fco.data(), fco.flags())
445 repo.wwrite(fd + ".base", fca.data(), fca.flags())
445 repo.wwrite(fd + ".base", fca.data(), fca.flags())
446 return False, 1, False
446 return False, 1, False
447
447
448 def _xmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
448 def _xmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
449 tool, toolpath, binary, symlink = toolconf
449 tool, toolpath, binary, symlink = toolconf
450 if fcd.isabsent() or fco.isabsent():
451 repo.ui.warn(_('warning: %s cannot merge change/delete conflict '
452 'for %s\n') % (tool, fcd.path()))
453 return False, 1, None
450 a, b, c, back = files
454 a, b, c, back = files
451 out = ""
455 out = ""
452 env = {'HG_FILE': fcd.path(),
456 env = {'HG_FILE': fcd.path(),
453 'HG_MY_NODE': short(mynode),
457 'HG_MY_NODE': short(mynode),
454 'HG_OTHER_NODE': str(fco.changectx()),
458 'HG_OTHER_NODE': str(fco.changectx()),
455 'HG_BASE_NODE': str(fca.changectx()),
459 'HG_BASE_NODE': str(fca.changectx()),
456 'HG_MY_ISLINK': 'l' in fcd.flags(),
460 'HG_MY_ISLINK': 'l' in fcd.flags(),
457 'HG_OTHER_ISLINK': 'l' in fco.flags(),
461 'HG_OTHER_ISLINK': 'l' in fco.flags(),
458 'HG_BASE_ISLINK': 'l' in fca.flags(),
462 'HG_BASE_ISLINK': 'l' in fca.flags(),
459 }
463 }
460
464
461 ui = repo.ui
465 ui = repo.ui
462
466
463 args = _toolstr(ui, tool, "args", '$local $base $other')
467 args = _toolstr(ui, tool, "args", '$local $base $other')
464 if "$output" in args:
468 if "$output" in args:
465 out, a = a, back # read input from backup, write to original
469 out, a = a, back # read input from backup, write to original
466 replace = {'local': a, 'base': b, 'other': c, 'output': out}
470 replace = {'local': a, 'base': b, 'other': c, 'output': out}
467 args = util.interpolate(r'\$', replace, args,
471 args = util.interpolate(r'\$', replace, args,
468 lambda s: util.shellquote(util.localpath(s)))
472 lambda s: util.shellquote(util.localpath(s)))
469 cmd = toolpath + ' ' + args
473 cmd = toolpath + ' ' + args
470 repo.ui.debug('launching merge tool: %s\n' % cmd)
474 repo.ui.debug('launching merge tool: %s\n' % cmd)
471 r = ui.system(cmd, cwd=repo.root, environ=env)
475 r = ui.system(cmd, cwd=repo.root, environ=env)
472 repo.ui.debug('merge tool returned: %s\n' % r)
476 repo.ui.debug('merge tool returned: %s\n' % r)
473 return True, r, False
477 return True, r, False
474
478
475 def _formatconflictmarker(repo, ctx, template, label, pad):
479 def _formatconflictmarker(repo, ctx, template, label, pad):
476 """Applies the given template to the ctx, prefixed by the label.
480 """Applies the given template to the ctx, prefixed by the label.
477
481
478 Pad is the minimum width of the label prefix, so that multiple markers
482 Pad is the minimum width of the label prefix, so that multiple markers
479 can have aligned templated parts.
483 can have aligned templated parts.
480 """
484 """
481 if ctx.node() is None:
485 if ctx.node() is None:
482 ctx = ctx.p1()
486 ctx = ctx.p1()
483
487
484 props = templatekw.keywords.copy()
488 props = templatekw.keywords.copy()
485 props['templ'] = template
489 props['templ'] = template
486 props['ctx'] = ctx
490 props['ctx'] = ctx
487 props['repo'] = repo
491 props['repo'] = repo
488 templateresult = template('conflictmarker', **props)
492 templateresult = template('conflictmarker', **props)
489
493
490 label = ('%s:' % label).ljust(pad + 1)
494 label = ('%s:' % label).ljust(pad + 1)
491 mark = '%s %s' % (label, templater.stringify(templateresult))
495 mark = '%s %s' % (label, templater.stringify(templateresult))
492
496
493 if mark:
497 if mark:
494 mark = mark.splitlines()[0] # split for safety
498 mark = mark.splitlines()[0] # split for safety
495
499
496 # 8 for the prefix of conflict marker lines (e.g. '<<<<<<< ')
500 # 8 for the prefix of conflict marker lines (e.g. '<<<<<<< ')
497 return util.ellipsis(mark, 80 - 8)
501 return util.ellipsis(mark, 80 - 8)
498
502
499 _defaultconflictmarker = ('{node|short} ' +
503 _defaultconflictmarker = ('{node|short} ' +
500 '{ifeq(tags, "tip", "", "{tags} ")}' +
504 '{ifeq(tags, "tip", "", "{tags} ")}' +
501 '{if(bookmarks, "{bookmarks} ")}' +
505 '{if(bookmarks, "{bookmarks} ")}' +
502 '{ifeq(branch, "default", "", "{branch} ")}' +
506 '{ifeq(branch, "default", "", "{branch} ")}' +
503 '- {author|user}: {desc|firstline}')
507 '- {author|user}: {desc|firstline}')
504
508
505 _defaultconflictlabels = ['local', 'other']
509 _defaultconflictlabels = ['local', 'other']
506
510
507 def _formatlabels(repo, fcd, fco, fca, labels):
511 def _formatlabels(repo, fcd, fco, fca, labels):
508 """Formats the given labels using the conflict marker template.
512 """Formats the given labels using the conflict marker template.
509
513
510 Returns a list of formatted labels.
514 Returns a list of formatted labels.
511 """
515 """
512 cd = fcd.changectx()
516 cd = fcd.changectx()
513 co = fco.changectx()
517 co = fco.changectx()
514 ca = fca.changectx()
518 ca = fca.changectx()
515
519
516 ui = repo.ui
520 ui = repo.ui
517 template = ui.config('ui', 'mergemarkertemplate', _defaultconflictmarker)
521 template = ui.config('ui', 'mergemarkertemplate', _defaultconflictmarker)
518 tmpl = templater.templater(None, cache={'conflictmarker': template})
522 tmpl = templater.templater(None, cache={'conflictmarker': template})
519
523
520 pad = max(len(l) for l in labels)
524 pad = max(len(l) for l in labels)
521
525
522 newlabels = [_formatconflictmarker(repo, cd, tmpl, labels[0], pad),
526 newlabels = [_formatconflictmarker(repo, cd, tmpl, labels[0], pad),
523 _formatconflictmarker(repo, co, tmpl, labels[1], pad)]
527 _formatconflictmarker(repo, co, tmpl, labels[1], pad)]
524 if len(labels) > 2:
528 if len(labels) > 2:
525 newlabels.append(_formatconflictmarker(repo, ca, tmpl, labels[2], pad))
529 newlabels.append(_formatconflictmarker(repo, ca, tmpl, labels[2], pad))
526 return newlabels
530 return newlabels
527
531
528 def _filemerge(premerge, repo, mynode, orig, fcd, fco, fca, labels=None):
532 def _filemerge(premerge, repo, mynode, orig, fcd, fco, fca, labels=None):
529 """perform a 3-way merge in the working directory
533 """perform a 3-way merge in the working directory
530
534
531 premerge = whether this is a premerge
535 premerge = whether this is a premerge
532 mynode = parent node before merge
536 mynode = parent node before merge
533 orig = original local filename before merge
537 orig = original local filename before merge
534 fco = other file context
538 fco = other file context
535 fca = ancestor file context
539 fca = ancestor file context
536 fcd = local file context for current/destination file
540 fcd = local file context for current/destination file
537
541
538 Returns whether the merge is complete, the return value of the merge, and
542 Returns whether the merge is complete, the return value of the merge, and
539 a boolean indicating whether the file was deleted from disk."""
543 a boolean indicating whether the file was deleted from disk."""
540
544
541 def temp(prefix, ctx):
545 def temp(prefix, ctx):
542 pre = "%s~%s." % (os.path.basename(ctx.path()), prefix)
546 pre = "%s~%s." % (os.path.basename(ctx.path()), prefix)
543 (fd, name) = tempfile.mkstemp(prefix=pre)
547 (fd, name) = tempfile.mkstemp(prefix=pre)
544 data = repo.wwritedata(ctx.path(), ctx.data())
548 data = repo.wwritedata(ctx.path(), ctx.data())
545 f = os.fdopen(fd, "wb")
549 f = os.fdopen(fd, "wb")
546 f.write(data)
550 f.write(data)
547 f.close()
551 f.close()
548 return name
552 return name
549
553
550 if not fco.cmp(fcd): # files identical?
554 if not fco.cmp(fcd): # files identical?
551 return True, None, False
555 return True, None, False
552
556
553 ui = repo.ui
557 ui = repo.ui
554 fd = fcd.path()
558 fd = fcd.path()
555 binary = fcd.isbinary() or fco.isbinary() or fca.isbinary()
559 binary = fcd.isbinary() or fco.isbinary() or fca.isbinary()
556 symlink = 'l' in fcd.flags() + fco.flags()
560 symlink = 'l' in fcd.flags() + fco.flags()
557 changedelete = fcd.isabsent() or fco.isabsent()
561 changedelete = fcd.isabsent() or fco.isabsent()
558 tool, toolpath = _picktool(repo, ui, fd, binary, symlink, changedelete)
562 tool, toolpath = _picktool(repo, ui, fd, binary, symlink, changedelete)
559 if tool in internals and tool.startswith('internal:'):
563 if tool in internals and tool.startswith('internal:'):
560 # normalize to new-style names (':merge' etc)
564 # normalize to new-style names (':merge' etc)
561 tool = tool[len('internal'):]
565 tool = tool[len('internal'):]
562 ui.debug("picked tool '%s' for %s (binary %s symlink %s)\n" %
566 ui.debug("picked tool '%s' for %s (binary %s symlink %s)\n" %
563 (tool, fd, binary, symlink))
567 (tool, fd, binary, symlink))
564
568
565 if tool in internals:
569 if tool in internals:
566 func = internals[tool]
570 func = internals[tool]
567 mergetype = func.mergetype
571 mergetype = func.mergetype
568 onfailure = func.onfailure
572 onfailure = func.onfailure
569 precheck = func.precheck
573 precheck = func.precheck
570 else:
574 else:
571 func = _xmerge
575 func = _xmerge
572 mergetype = fullmerge
576 mergetype = fullmerge
573 onfailure = _("merging %s failed!\n")
577 onfailure = _("merging %s failed!\n")
574 precheck = None
578 precheck = None
575
579
576 toolconf = tool, toolpath, binary, symlink
580 toolconf = tool, toolpath, binary, symlink
577
581
578 if mergetype == nomerge:
582 if mergetype == nomerge:
579 r, deleted = func(repo, mynode, orig, fcd, fco, fca, toolconf)
583 r, deleted = func(repo, mynode, orig, fcd, fco, fca, toolconf)
580 return True, r, deleted
584 return True, r, deleted
581
585
582 if premerge:
586 if premerge:
583 if orig != fco.path():
587 if orig != fco.path():
584 ui.status(_("merging %s and %s to %s\n") % (orig, fco.path(), fd))
588 ui.status(_("merging %s and %s to %s\n") % (orig, fco.path(), fd))
585 else:
589 else:
586 ui.status(_("merging %s\n") % fd)
590 ui.status(_("merging %s\n") % fd)
587
591
588 ui.debug("my %s other %s ancestor %s\n" % (fcd, fco, fca))
592 ui.debug("my %s other %s ancestor %s\n" % (fcd, fco, fca))
589
593
590 if precheck and not precheck(repo, mynode, orig, fcd, fco, fca,
594 if precheck and not precheck(repo, mynode, orig, fcd, fco, fca,
591 toolconf):
595 toolconf):
592 if onfailure:
596 if onfailure:
593 ui.warn(onfailure % fd)
597 ui.warn(onfailure % fd)
594 return True, 1, False
598 return True, 1, False
595
599
596 a = repo.wjoin(fd)
600 a = repo.wjoin(fd)
597 b = temp("base", fca)
601 b = temp("base", fca)
598 c = temp("other", fco)
602 c = temp("other", fco)
599 back = cmdutil.origpath(ui, repo, a)
603 back = cmdutil.origpath(ui, repo, a)
600 if premerge:
604 if premerge:
601 util.copyfile(a, back)
605 util.copyfile(a, back)
602 files = (a, b, c, back)
606 files = (a, b, c, back)
603
607
604 r = 1
608 r = 1
605 try:
609 try:
606 markerstyle = ui.config('ui', 'mergemarkers', 'basic')
610 markerstyle = ui.config('ui', 'mergemarkers', 'basic')
607 if not labels:
611 if not labels:
608 labels = _defaultconflictlabels
612 labels = _defaultconflictlabels
609 if markerstyle != 'basic':
613 if markerstyle != 'basic':
610 labels = _formatlabels(repo, fcd, fco, fca, labels)
614 labels = _formatlabels(repo, fcd, fco, fca, labels)
611
615
612 if premerge and mergetype == fullmerge:
616 if premerge and mergetype == fullmerge:
613 r = _premerge(repo, fcd, fco, fca, toolconf, files, labels=labels)
617 r = _premerge(repo, fcd, fco, fca, toolconf, files, labels=labels)
614 # complete if premerge successful (r is 0)
618 # complete if premerge successful (r is 0)
615 return not r, r, False
619 return not r, r, False
616
620
617 needcheck, r, deleted = func(repo, mynode, orig, fcd, fco, fca,
621 needcheck, r, deleted = func(repo, mynode, orig, fcd, fco, fca,
618 toolconf, files, labels=labels)
622 toolconf, files, labels=labels)
619
623
620 if needcheck:
624 if needcheck:
621 r = _check(r, ui, tool, fcd, files)
625 r = _check(r, ui, tool, fcd, files)
622
626
623 if r:
627 if r:
624 if onfailure:
628 if onfailure:
625 ui.warn(onfailure % fd)
629 ui.warn(onfailure % fd)
626
630
627 return True, r, deleted
631 return True, r, deleted
628 finally:
632 finally:
629 if not r:
633 if not r:
630 util.unlink(back)
634 util.unlink(back)
631 util.unlink(b)
635 util.unlink(b)
632 util.unlink(c)
636 util.unlink(c)
633
637
634 def _check(r, ui, tool, fcd, files):
638 def _check(r, ui, tool, fcd, files):
635 fd = fcd.path()
639 fd = fcd.path()
636 a, b, c, back = files
640 a, b, c, back = files
637
641
638 if not r and (_toolbool(ui, tool, "checkconflicts") or
642 if not r and (_toolbool(ui, tool, "checkconflicts") or
639 'conflicts' in _toollist(ui, tool, "check")):
643 'conflicts' in _toollist(ui, tool, "check")):
640 if re.search("^(<<<<<<< .*|=======|>>>>>>> .*)$", fcd.data(),
644 if re.search("^(<<<<<<< .*|=======|>>>>>>> .*)$", fcd.data(),
641 re.MULTILINE):
645 re.MULTILINE):
642 r = 1
646 r = 1
643
647
644 checked = False
648 checked = False
645 if 'prompt' in _toollist(ui, tool, "check"):
649 if 'prompt' in _toollist(ui, tool, "check"):
646 checked = True
650 checked = True
647 if ui.promptchoice(_("was merge of '%s' successful (yn)?"
651 if ui.promptchoice(_("was merge of '%s' successful (yn)?"
648 "$$ &Yes $$ &No") % fd, 1):
652 "$$ &Yes $$ &No") % fd, 1):
649 r = 1
653 r = 1
650
654
651 if not r and not checked and (_toolbool(ui, tool, "checkchanged") or
655 if not r and not checked and (_toolbool(ui, tool, "checkchanged") or
652 'changed' in
656 'changed' in
653 _toollist(ui, tool, "check")):
657 _toollist(ui, tool, "check")):
654 if filecmp.cmp(a, back):
658 if filecmp.cmp(a, back):
655 if ui.promptchoice(_(" output file %s appears unchanged\n"
659 if ui.promptchoice(_(" output file %s appears unchanged\n"
656 "was merge successful (yn)?"
660 "was merge successful (yn)?"
657 "$$ &Yes $$ &No") % fd, 1):
661 "$$ &Yes $$ &No") % fd, 1):
658 r = 1
662 r = 1
659
663
660 if _toolbool(ui, tool, "fixeol"):
664 if _toolbool(ui, tool, "fixeol"):
661 _matcheol(a, back)
665 _matcheol(a, back)
662
666
663 return r
667 return r
664
668
665 def premerge(repo, mynode, orig, fcd, fco, fca, labels=None):
669 def premerge(repo, mynode, orig, fcd, fco, fca, labels=None):
666 return _filemerge(True, repo, mynode, orig, fcd, fco, fca, labels=labels)
670 return _filemerge(True, repo, mynode, orig, fcd, fco, fca, labels=labels)
667
671
668 def filemerge(repo, mynode, orig, fcd, fco, fca, labels=None):
672 def filemerge(repo, mynode, orig, fcd, fco, fca, labels=None):
669 return _filemerge(False, repo, mynode, orig, fcd, fco, fca, labels=labels)
673 return _filemerge(False, repo, mynode, orig, fcd, fco, fca, labels=labels)
670
674
671 # tell hggettext to extract docstrings from these functions:
675 # tell hggettext to extract docstrings from these functions:
672 i18nfunctions = internals.values()
676 i18nfunctions = internals.values()
General Comments 0
You need to be logged in to leave comments. Login now