##// END OF EJS Templates
filemerge: in ':prompt', use ':fail' tool rather than returning directly...
Siddharth Agarwal -
r27124:9c870e3e default
parent child Browse files
Show More
@@ -1,682 +1,682 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 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 _ifail(repo, mynode, orig, fcd, fco, fca, toolconf)
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 # for change/delete conflicts write out the changed version, then fail
288 # for change/delete conflicts write out the changed version, then fail
289 if fcd.isabsent():
289 if fcd.isabsent():
290 repo.wwrite(fcd.path(), fco.data(), fco.flags())
290 repo.wwrite(fcd.path(), fco.data(), fco.flags())
291 return 1, False
291 return 1, False
292
292
293 def _premerge(repo, fcd, fco, fca, toolconf, files, labels=None):
293 def _premerge(repo, fcd, fco, fca, toolconf, files, labels=None):
294 tool, toolpath, binary, symlink = toolconf
294 tool, toolpath, binary, symlink = toolconf
295 if symlink or fcd.isabsent() or fco.isabsent():
295 if symlink or fcd.isabsent() or fco.isabsent():
296 return 1
296 return 1
297 a, b, c, back = files
297 a, b, c, back = files
298
298
299 ui = repo.ui
299 ui = repo.ui
300
300
301 validkeep = ['keep', 'keep-merge3']
301 validkeep = ['keep', 'keep-merge3']
302
302
303 # do we attempt to simplemerge first?
303 # do we attempt to simplemerge first?
304 try:
304 try:
305 premerge = _toolbool(ui, tool, "premerge", not binary)
305 premerge = _toolbool(ui, tool, "premerge", not binary)
306 except error.ConfigError:
306 except error.ConfigError:
307 premerge = _toolstr(ui, tool, "premerge").lower()
307 premerge = _toolstr(ui, tool, "premerge").lower()
308 if premerge not in validkeep:
308 if premerge not in validkeep:
309 _valid = ', '.join(["'" + v + "'" for v in validkeep])
309 _valid = ', '.join(["'" + v + "'" for v in validkeep])
310 raise error.ConfigError(_("%s.premerge not valid "
310 raise error.ConfigError(_("%s.premerge not valid "
311 "('%s' is neither boolean nor %s)") %
311 "('%s' is neither boolean nor %s)") %
312 (tool, premerge, _valid))
312 (tool, premerge, _valid))
313
313
314 if premerge:
314 if premerge:
315 if premerge == 'keep-merge3':
315 if premerge == 'keep-merge3':
316 if not labels:
316 if not labels:
317 labels = _defaultconflictlabels
317 labels = _defaultconflictlabels
318 if len(labels) < 3:
318 if len(labels) < 3:
319 labels.append('base')
319 labels.append('base')
320 r = simplemerge.simplemerge(ui, a, b, c, quiet=True, label=labels)
320 r = simplemerge.simplemerge(ui, a, b, c, quiet=True, label=labels)
321 if not r:
321 if not r:
322 ui.debug(" premerge successful\n")
322 ui.debug(" premerge successful\n")
323 return 0
323 return 0
324 if premerge not in validkeep:
324 if premerge not in validkeep:
325 util.copyfile(back, a) # restore from backup and try again
325 util.copyfile(back, a) # restore from backup and try again
326 return 1 # continue merging
326 return 1 # continue merging
327
327
328 def _mergecheck(repo, mynode, orig, fcd, fco, fca, toolconf):
328 def _mergecheck(repo, mynode, orig, fcd, fco, fca, toolconf):
329 tool, toolpath, binary, symlink = toolconf
329 tool, toolpath, binary, symlink = toolconf
330 if symlink:
330 if symlink:
331 repo.ui.warn(_('warning: internal %s cannot merge symlinks '
331 repo.ui.warn(_('warning: internal %s cannot merge symlinks '
332 'for %s\n') % (tool, fcd.path()))
332 'for %s\n') % (tool, fcd.path()))
333 return False
333 return False
334 if fcd.isabsent() or fco.isabsent():
334 if fcd.isabsent() or fco.isabsent():
335 repo.ui.warn(_('warning: internal %s cannot merge change/delete '
335 repo.ui.warn(_('warning: internal %s cannot merge change/delete '
336 'conflict for %s\n') % (tool, fcd.path()))
336 'conflict for %s\n') % (tool, fcd.path()))
337 return False
337 return False
338 return True
338 return True
339
339
340 def _merge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels, mode):
340 def _merge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels, mode):
341 """
341 """
342 Uses the internal non-interactive simple merge algorithm for merging
342 Uses the internal non-interactive simple merge algorithm for merging
343 files. It will fail if there are any conflicts and leave markers in
343 files. It will fail if there are any conflicts and leave markers in
344 the partially merged file. Markers will have two sections, one for each side
344 the partially merged file. Markers will have two sections, one for each side
345 of merge, unless mode equals 'union' which suppresses the markers."""
345 of merge, unless mode equals 'union' which suppresses the markers."""
346 a, b, c, back = files
346 a, b, c, back = files
347
347
348 ui = repo.ui
348 ui = repo.ui
349
349
350 r = simplemerge.simplemerge(ui, a, b, c, label=labels, mode=mode)
350 r = simplemerge.simplemerge(ui, a, b, c, label=labels, mode=mode)
351 return True, r, False
351 return True, r, False
352
352
353 @internaltool('union', fullmerge,
353 @internaltool('union', fullmerge,
354 _("warning: conflicts while merging %s! "
354 _("warning: conflicts while merging %s! "
355 "(edit, then use 'hg resolve --mark')\n"),
355 "(edit, then use 'hg resolve --mark')\n"),
356 precheck=_mergecheck)
356 precheck=_mergecheck)
357 def _iunion(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
357 def _iunion(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
358 """
358 """
359 Uses the internal non-interactive simple merge algorithm for merging
359 Uses the internal non-interactive simple merge algorithm for merging
360 files. It will use both left and right sides for conflict regions.
360 files. It will use both left and right sides for conflict regions.
361 No markers are inserted."""
361 No markers are inserted."""
362 return _merge(repo, mynode, orig, fcd, fco, fca, toolconf,
362 return _merge(repo, mynode, orig, fcd, fco, fca, toolconf,
363 files, labels, 'union')
363 files, labels, 'union')
364
364
365 @internaltool('merge', fullmerge,
365 @internaltool('merge', fullmerge,
366 _("warning: conflicts while merging %s! "
366 _("warning: conflicts while merging %s! "
367 "(edit, then use 'hg resolve --mark')\n"),
367 "(edit, then use 'hg resolve --mark')\n"),
368 precheck=_mergecheck)
368 precheck=_mergecheck)
369 def _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
369 def _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
370 """
370 """
371 Uses the internal non-interactive simple merge algorithm for merging
371 Uses the internal non-interactive simple merge algorithm for merging
372 files. It will fail if there are any conflicts and leave markers in
372 files. It will fail if there are any conflicts and leave markers in
373 the partially merged file. Markers will have two sections, one for each side
373 the partially merged file. Markers will have two sections, one for each side
374 of merge."""
374 of merge."""
375 return _merge(repo, mynode, orig, fcd, fco, fca, toolconf,
375 return _merge(repo, mynode, orig, fcd, fco, fca, toolconf,
376 files, labels, 'merge')
376 files, labels, 'merge')
377
377
378 @internaltool('merge3', fullmerge,
378 @internaltool('merge3', fullmerge,
379 _("warning: conflicts while merging %s! "
379 _("warning: conflicts while merging %s! "
380 "(edit, then use 'hg resolve --mark')\n"),
380 "(edit, then use 'hg resolve --mark')\n"),
381 precheck=_mergecheck)
381 precheck=_mergecheck)
382 def _imerge3(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
382 def _imerge3(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
383 """
383 """
384 Uses the internal non-interactive simple merge algorithm for merging
384 Uses the internal non-interactive simple merge algorithm for merging
385 files. It will fail if there are any conflicts and leave markers in
385 files. It will fail if there are any conflicts and leave markers in
386 the partially merged file. Marker will have three sections, one from each
386 the partially merged file. Marker will have three sections, one from each
387 side of the merge and one for the base content."""
387 side of the merge and one for the base content."""
388 if not labels:
388 if not labels:
389 labels = _defaultconflictlabels
389 labels = _defaultconflictlabels
390 if len(labels) < 3:
390 if len(labels) < 3:
391 labels.append('base')
391 labels.append('base')
392 return _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels)
392 return _imerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels)
393
393
394 def _imergeauto(repo, mynode, orig, fcd, fco, fca, toolconf, files,
394 def _imergeauto(repo, mynode, orig, fcd, fco, fca, toolconf, files,
395 labels=None, localorother=None):
395 labels=None, localorother=None):
396 """
396 """
397 Generic driver for _imergelocal and _imergeother
397 Generic driver for _imergelocal and _imergeother
398 """
398 """
399 assert localorother is not None
399 assert localorother is not None
400 tool, toolpath, binary, symlink = toolconf
400 tool, toolpath, binary, symlink = toolconf
401 a, b, c, back = files
401 a, b, c, back = files
402 r = simplemerge.simplemerge(repo.ui, a, b, c, label=labels,
402 r = simplemerge.simplemerge(repo.ui, a, b, c, label=labels,
403 localorother=localorother)
403 localorother=localorother)
404 return True, r
404 return True, r
405
405
406 @internaltool('merge-local', mergeonly, precheck=_mergecheck)
406 @internaltool('merge-local', mergeonly, precheck=_mergecheck)
407 def _imergelocal(*args, **kwargs):
407 def _imergelocal(*args, **kwargs):
408 """
408 """
409 Like :merge, but resolve all conflicts non-interactively in favor
409 Like :merge, but resolve all conflicts non-interactively in favor
410 of the local changes."""
410 of the local changes."""
411 success, status = _imergeauto(localorother='local', *args, **kwargs)
411 success, status = _imergeauto(localorother='local', *args, **kwargs)
412 return success, status, False
412 return success, status, False
413
413
414 @internaltool('merge-other', mergeonly, precheck=_mergecheck)
414 @internaltool('merge-other', mergeonly, precheck=_mergecheck)
415 def _imergeother(*args, **kwargs):
415 def _imergeother(*args, **kwargs):
416 """
416 """
417 Like :merge, but resolve all conflicts non-interactively in favor
417 Like :merge, but resolve all conflicts non-interactively in favor
418 of the other changes."""
418 of the other changes."""
419 success, status = _imergeauto(localorother='other', *args, **kwargs)
419 success, status = _imergeauto(localorother='other', *args, **kwargs)
420 return success, status, False
420 return success, status, False
421
421
422 @internaltool('tagmerge', mergeonly,
422 @internaltool('tagmerge', mergeonly,
423 _("automatic tag merging of %s failed! "
423 _("automatic tag merging of %s failed! "
424 "(use 'hg resolve --tool :merge' or another merge "
424 "(use 'hg resolve --tool :merge' or another merge "
425 "tool of your choice)\n"))
425 "tool of your choice)\n"))
426 def _itagmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
426 def _itagmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
427 """
427 """
428 Uses the internal tag merge algorithm (experimental).
428 Uses the internal tag merge algorithm (experimental).
429 """
429 """
430 success, status = tagmerge.merge(repo, fcd, fco, fca)
430 success, status = tagmerge.merge(repo, fcd, fco, fca)
431 return success, status, False
431 return success, status, False
432
432
433 @internaltool('dump', fullmerge)
433 @internaltool('dump', fullmerge)
434 def _idump(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
434 def _idump(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
435 """
435 """
436 Creates three versions of the files to merge, containing the
436 Creates three versions of the files to merge, containing the
437 contents of local, other and base. These files can then be used to
437 contents of local, other and base. These files can then be used to
438 perform a merge manually. If the file to be merged is named
438 perform a merge manually. If the file to be merged is named
439 ``a.txt``, these files will accordingly be named ``a.txt.local``,
439 ``a.txt``, these files will accordingly be named ``a.txt.local``,
440 ``a.txt.other`` and ``a.txt.base`` and they will be placed in the
440 ``a.txt.other`` and ``a.txt.base`` and they will be placed in the
441 same directory as ``a.txt``."""
441 same directory as ``a.txt``."""
442 a, b, c, back = files
442 a, b, c, back = files
443
443
444 fd = fcd.path()
444 fd = fcd.path()
445
445
446 util.copyfile(a, a + ".local")
446 util.copyfile(a, a + ".local")
447 repo.wwrite(fd + ".other", fco.data(), fco.flags())
447 repo.wwrite(fd + ".other", fco.data(), fco.flags())
448 repo.wwrite(fd + ".base", fca.data(), fca.flags())
448 repo.wwrite(fd + ".base", fca.data(), fca.flags())
449 return False, 1, False
449 return False, 1, False
450
450
451 def _xmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
451 def _xmerge(repo, mynode, orig, fcd, fco, fca, toolconf, files, labels=None):
452 tool, toolpath, binary, symlink = toolconf
452 tool, toolpath, binary, symlink = toolconf
453 if fcd.isabsent() or fco.isabsent():
453 if fcd.isabsent() or fco.isabsent():
454 repo.ui.warn(_('warning: %s cannot merge change/delete conflict '
454 repo.ui.warn(_('warning: %s cannot merge change/delete conflict '
455 'for %s\n') % (tool, fcd.path()))
455 'for %s\n') % (tool, fcd.path()))
456 return False, 1, None
456 return False, 1, None
457 a, b, c, back = files
457 a, b, c, back = files
458 out = ""
458 out = ""
459 env = {'HG_FILE': fcd.path(),
459 env = {'HG_FILE': fcd.path(),
460 'HG_MY_NODE': short(mynode),
460 'HG_MY_NODE': short(mynode),
461 'HG_OTHER_NODE': str(fco.changectx()),
461 'HG_OTHER_NODE': str(fco.changectx()),
462 'HG_BASE_NODE': str(fca.changectx()),
462 'HG_BASE_NODE': str(fca.changectx()),
463 'HG_MY_ISLINK': 'l' in fcd.flags(),
463 'HG_MY_ISLINK': 'l' in fcd.flags(),
464 'HG_OTHER_ISLINK': 'l' in fco.flags(),
464 'HG_OTHER_ISLINK': 'l' in fco.flags(),
465 'HG_BASE_ISLINK': 'l' in fca.flags(),
465 'HG_BASE_ISLINK': 'l' in fca.flags(),
466 }
466 }
467
467
468 ui = repo.ui
468 ui = repo.ui
469
469
470 args = _toolstr(ui, tool, "args", '$local $base $other')
470 args = _toolstr(ui, tool, "args", '$local $base $other')
471 if "$output" in args:
471 if "$output" in args:
472 out, a = a, back # read input from backup, write to original
472 out, a = a, back # read input from backup, write to original
473 replace = {'local': a, 'base': b, 'other': c, 'output': out}
473 replace = {'local': a, 'base': b, 'other': c, 'output': out}
474 args = util.interpolate(r'\$', replace, args,
474 args = util.interpolate(r'\$', replace, args,
475 lambda s: util.shellquote(util.localpath(s)))
475 lambda s: util.shellquote(util.localpath(s)))
476 cmd = toolpath + ' ' + args
476 cmd = toolpath + ' ' + args
477 repo.ui.debug('launching merge tool: %s\n' % cmd)
477 repo.ui.debug('launching merge tool: %s\n' % cmd)
478 r = ui.system(cmd, cwd=repo.root, environ=env)
478 r = ui.system(cmd, cwd=repo.root, environ=env)
479 repo.ui.debug('merge tool returned: %s\n' % r)
479 repo.ui.debug('merge tool returned: %s\n' % r)
480 return True, r, False
480 return True, r, False
481
481
482 def _formatconflictmarker(repo, ctx, template, label, pad):
482 def _formatconflictmarker(repo, ctx, template, label, pad):
483 """Applies the given template to the ctx, prefixed by the label.
483 """Applies the given template to the ctx, prefixed by the label.
484
484
485 Pad is the minimum width of the label prefix, so that multiple markers
485 Pad is the minimum width of the label prefix, so that multiple markers
486 can have aligned templated parts.
486 can have aligned templated parts.
487 """
487 """
488 if ctx.node() is None:
488 if ctx.node() is None:
489 ctx = ctx.p1()
489 ctx = ctx.p1()
490
490
491 props = templatekw.keywords.copy()
491 props = templatekw.keywords.copy()
492 props['templ'] = template
492 props['templ'] = template
493 props['ctx'] = ctx
493 props['ctx'] = ctx
494 props['repo'] = repo
494 props['repo'] = repo
495 templateresult = template('conflictmarker', **props)
495 templateresult = template('conflictmarker', **props)
496
496
497 label = ('%s:' % label).ljust(pad + 1)
497 label = ('%s:' % label).ljust(pad + 1)
498 mark = '%s %s' % (label, templater.stringify(templateresult))
498 mark = '%s %s' % (label, templater.stringify(templateresult))
499
499
500 if mark:
500 if mark:
501 mark = mark.splitlines()[0] # split for safety
501 mark = mark.splitlines()[0] # split for safety
502
502
503 # 8 for the prefix of conflict marker lines (e.g. '<<<<<<< ')
503 # 8 for the prefix of conflict marker lines (e.g. '<<<<<<< ')
504 return util.ellipsis(mark, 80 - 8)
504 return util.ellipsis(mark, 80 - 8)
505
505
506 _defaultconflictmarker = ('{node|short} ' +
506 _defaultconflictmarker = ('{node|short} ' +
507 '{ifeq(tags, "tip", "", "{tags} ")}' +
507 '{ifeq(tags, "tip", "", "{tags} ")}' +
508 '{if(bookmarks, "{bookmarks} ")}' +
508 '{if(bookmarks, "{bookmarks} ")}' +
509 '{ifeq(branch, "default", "", "{branch} ")}' +
509 '{ifeq(branch, "default", "", "{branch} ")}' +
510 '- {author|user}: {desc|firstline}')
510 '- {author|user}: {desc|firstline}')
511
511
512 _defaultconflictlabels = ['local', 'other']
512 _defaultconflictlabels = ['local', 'other']
513
513
514 def _formatlabels(repo, fcd, fco, fca, labels):
514 def _formatlabels(repo, fcd, fco, fca, labels):
515 """Formats the given labels using the conflict marker template.
515 """Formats the given labels using the conflict marker template.
516
516
517 Returns a list of formatted labels.
517 Returns a list of formatted labels.
518 """
518 """
519 cd = fcd.changectx()
519 cd = fcd.changectx()
520 co = fco.changectx()
520 co = fco.changectx()
521 ca = fca.changectx()
521 ca = fca.changectx()
522
522
523 ui = repo.ui
523 ui = repo.ui
524 template = ui.config('ui', 'mergemarkertemplate', _defaultconflictmarker)
524 template = ui.config('ui', 'mergemarkertemplate', _defaultconflictmarker)
525 tmpl = templater.templater(None, cache={'conflictmarker': template})
525 tmpl = templater.templater(None, cache={'conflictmarker': template})
526
526
527 pad = max(len(l) for l in labels)
527 pad = max(len(l) for l in labels)
528
528
529 newlabels = [_formatconflictmarker(repo, cd, tmpl, labels[0], pad),
529 newlabels = [_formatconflictmarker(repo, cd, tmpl, labels[0], pad),
530 _formatconflictmarker(repo, co, tmpl, labels[1], pad)]
530 _formatconflictmarker(repo, co, tmpl, labels[1], pad)]
531 if len(labels) > 2:
531 if len(labels) > 2:
532 newlabels.append(_formatconflictmarker(repo, ca, tmpl, labels[2], pad))
532 newlabels.append(_formatconflictmarker(repo, ca, tmpl, labels[2], pad))
533 return newlabels
533 return newlabels
534
534
535 def _filemerge(premerge, repo, mynode, orig, fcd, fco, fca, labels=None):
535 def _filemerge(premerge, repo, mynode, orig, fcd, fco, fca, labels=None):
536 """perform a 3-way merge in the working directory
536 """perform a 3-way merge in the working directory
537
537
538 premerge = whether this is a premerge
538 premerge = whether this is a premerge
539 mynode = parent node before merge
539 mynode = parent node before merge
540 orig = original local filename before merge
540 orig = original local filename before merge
541 fco = other file context
541 fco = other file context
542 fca = ancestor file context
542 fca = ancestor file context
543 fcd = local file context for current/destination file
543 fcd = local file context for current/destination file
544
544
545 Returns whether the merge is complete, the return value of the merge, and
545 Returns whether the merge is complete, the return value of the merge, and
546 a boolean indicating whether the file was deleted from disk."""
546 a boolean indicating whether the file was deleted from disk."""
547
547
548 def temp(prefix, ctx):
548 def temp(prefix, ctx):
549 pre = "%s~%s." % (os.path.basename(ctx.path()), prefix)
549 pre = "%s~%s." % (os.path.basename(ctx.path()), prefix)
550 (fd, name) = tempfile.mkstemp(prefix=pre)
550 (fd, name) = tempfile.mkstemp(prefix=pre)
551 data = repo.wwritedata(ctx.path(), ctx.data())
551 data = repo.wwritedata(ctx.path(), ctx.data())
552 f = os.fdopen(fd, "wb")
552 f = os.fdopen(fd, "wb")
553 f.write(data)
553 f.write(data)
554 f.close()
554 f.close()
555 return name
555 return name
556
556
557 if not fco.cmp(fcd): # files identical?
557 if not fco.cmp(fcd): # files identical?
558 return True, None, False
558 return True, None, False
559
559
560 ui = repo.ui
560 ui = repo.ui
561 fd = fcd.path()
561 fd = fcd.path()
562 binary = fcd.isbinary() or fco.isbinary() or fca.isbinary()
562 binary = fcd.isbinary() or fco.isbinary() or fca.isbinary()
563 symlink = 'l' in fcd.flags() + fco.flags()
563 symlink = 'l' in fcd.flags() + fco.flags()
564 changedelete = fcd.isabsent() or fco.isabsent()
564 changedelete = fcd.isabsent() or fco.isabsent()
565 tool, toolpath = _picktool(repo, ui, fd, binary, symlink, changedelete)
565 tool, toolpath = _picktool(repo, ui, fd, binary, symlink, changedelete)
566 if tool in internals and tool.startswith('internal:'):
566 if tool in internals and tool.startswith('internal:'):
567 # normalize to new-style names (':merge' etc)
567 # normalize to new-style names (':merge' etc)
568 tool = tool[len('internal'):]
568 tool = tool[len('internal'):]
569 ui.debug("picked tool '%s' for %s (binary %s symlink %s)\n" %
569 ui.debug("picked tool '%s' for %s (binary %s symlink %s)\n" %
570 (tool, fd, binary, symlink))
570 (tool, fd, binary, symlink))
571
571
572 if tool in internals:
572 if tool in internals:
573 func = internals[tool]
573 func = internals[tool]
574 mergetype = func.mergetype
574 mergetype = func.mergetype
575 onfailure = func.onfailure
575 onfailure = func.onfailure
576 precheck = func.precheck
576 precheck = func.precheck
577 else:
577 else:
578 func = _xmerge
578 func = _xmerge
579 mergetype = fullmerge
579 mergetype = fullmerge
580 onfailure = _("merging %s failed!\n")
580 onfailure = _("merging %s failed!\n")
581 precheck = None
581 precheck = None
582
582
583 toolconf = tool, toolpath, binary, symlink
583 toolconf = tool, toolpath, binary, symlink
584
584
585 if mergetype == nomerge:
585 if mergetype == nomerge:
586 r, deleted = func(repo, mynode, orig, fcd, fco, fca, toolconf)
586 r, deleted = func(repo, mynode, orig, fcd, fco, fca, toolconf)
587 return True, r, deleted
587 return True, r, deleted
588
588
589 if premerge:
589 if premerge:
590 if orig != fco.path():
590 if orig != fco.path():
591 ui.status(_("merging %s and %s to %s\n") % (orig, fco.path(), fd))
591 ui.status(_("merging %s and %s to %s\n") % (orig, fco.path(), fd))
592 else:
592 else:
593 ui.status(_("merging %s\n") % fd)
593 ui.status(_("merging %s\n") % fd)
594
594
595 ui.debug("my %s other %s ancestor %s\n" % (fcd, fco, fca))
595 ui.debug("my %s other %s ancestor %s\n" % (fcd, fco, fca))
596
596
597 if precheck and not precheck(repo, mynode, orig, fcd, fco, fca,
597 if precheck and not precheck(repo, mynode, orig, fcd, fco, fca,
598 toolconf):
598 toolconf):
599 if onfailure:
599 if onfailure:
600 ui.warn(onfailure % fd)
600 ui.warn(onfailure % fd)
601 return True, 1, False
601 return True, 1, False
602
602
603 a = repo.wjoin(fd)
603 a = repo.wjoin(fd)
604 b = temp("base", fca)
604 b = temp("base", fca)
605 c = temp("other", fco)
605 c = temp("other", fco)
606 if not fcd.isabsent():
606 if not fcd.isabsent():
607 back = cmdutil.origpath(ui, repo, a)
607 back = cmdutil.origpath(ui, repo, a)
608 if premerge:
608 if premerge:
609 util.copyfile(a, back)
609 util.copyfile(a, back)
610 else:
610 else:
611 back = None
611 back = None
612 files = (a, b, c, back)
612 files = (a, b, c, back)
613
613
614 r = 1
614 r = 1
615 try:
615 try:
616 markerstyle = ui.config('ui', 'mergemarkers', 'basic')
616 markerstyle = ui.config('ui', 'mergemarkers', 'basic')
617 if not labels:
617 if not labels:
618 labels = _defaultconflictlabels
618 labels = _defaultconflictlabels
619 if markerstyle != 'basic':
619 if markerstyle != 'basic':
620 labels = _formatlabels(repo, fcd, fco, fca, labels)
620 labels = _formatlabels(repo, fcd, fco, fca, labels)
621
621
622 if premerge and mergetype == fullmerge:
622 if premerge and mergetype == fullmerge:
623 r = _premerge(repo, fcd, fco, fca, toolconf, files, labels=labels)
623 r = _premerge(repo, fcd, fco, fca, toolconf, files, labels=labels)
624 # complete if premerge successful (r is 0)
624 # complete if premerge successful (r is 0)
625 return not r, r, False
625 return not r, r, False
626
626
627 needcheck, r, deleted = func(repo, mynode, orig, fcd, fco, fca,
627 needcheck, r, deleted = func(repo, mynode, orig, fcd, fco, fca,
628 toolconf, files, labels=labels)
628 toolconf, files, labels=labels)
629
629
630 if needcheck:
630 if needcheck:
631 r = _check(r, ui, tool, fcd, files)
631 r = _check(r, ui, tool, fcd, files)
632
632
633 if r:
633 if r:
634 if onfailure:
634 if onfailure:
635 ui.warn(onfailure % fd)
635 ui.warn(onfailure % fd)
636
636
637 return True, r, deleted
637 return True, r, deleted
638 finally:
638 finally:
639 if not r and back is not None:
639 if not r and back is not None:
640 util.unlink(back)
640 util.unlink(back)
641 util.unlink(b)
641 util.unlink(b)
642 util.unlink(c)
642 util.unlink(c)
643
643
644 def _check(r, ui, tool, fcd, files):
644 def _check(r, ui, tool, fcd, files):
645 fd = fcd.path()
645 fd = fcd.path()
646 a, b, c, back = files
646 a, b, c, back = files
647
647
648 if not r and (_toolbool(ui, tool, "checkconflicts") or
648 if not r and (_toolbool(ui, tool, "checkconflicts") or
649 'conflicts' in _toollist(ui, tool, "check")):
649 'conflicts' in _toollist(ui, tool, "check")):
650 if re.search("^(<<<<<<< .*|=======|>>>>>>> .*)$", fcd.data(),
650 if re.search("^(<<<<<<< .*|=======|>>>>>>> .*)$", fcd.data(),
651 re.MULTILINE):
651 re.MULTILINE):
652 r = 1
652 r = 1
653
653
654 checked = False
654 checked = False
655 if 'prompt' in _toollist(ui, tool, "check"):
655 if 'prompt' in _toollist(ui, tool, "check"):
656 checked = True
656 checked = True
657 if ui.promptchoice(_("was merge of '%s' successful (yn)?"
657 if ui.promptchoice(_("was merge of '%s' successful (yn)?"
658 "$$ &Yes $$ &No") % fd, 1):
658 "$$ &Yes $$ &No") % fd, 1):
659 r = 1
659 r = 1
660
660
661 if not r and not checked and (_toolbool(ui, tool, "checkchanged") or
661 if not r and not checked and (_toolbool(ui, tool, "checkchanged") or
662 'changed' in
662 'changed' in
663 _toollist(ui, tool, "check")):
663 _toollist(ui, tool, "check")):
664 if back is not None and filecmp.cmp(a, back):
664 if back is not None and filecmp.cmp(a, back):
665 if ui.promptchoice(_(" output file %s appears unchanged\n"
665 if ui.promptchoice(_(" output file %s appears unchanged\n"
666 "was merge successful (yn)?"
666 "was merge successful (yn)?"
667 "$$ &Yes $$ &No") % fd, 1):
667 "$$ &Yes $$ &No") % fd, 1):
668 r = 1
668 r = 1
669
669
670 if back is not None and _toolbool(ui, tool, "fixeol"):
670 if back is not None and _toolbool(ui, tool, "fixeol"):
671 _matcheol(a, back)
671 _matcheol(a, back)
672
672
673 return r
673 return r
674
674
675 def premerge(repo, mynode, orig, fcd, fco, fca, labels=None):
675 def premerge(repo, mynode, orig, fcd, fco, fca, labels=None):
676 return _filemerge(True, repo, mynode, orig, fcd, fco, fca, labels=labels)
676 return _filemerge(True, repo, mynode, orig, fcd, fco, fca, labels=labels)
677
677
678 def filemerge(repo, mynode, orig, fcd, fco, fca, labels=None):
678 def filemerge(repo, mynode, orig, fcd, fco, fca, labels=None):
679 return _filemerge(False, repo, mynode, orig, fcd, fco, fca, labels=labels)
679 return _filemerge(False, repo, mynode, orig, fcd, fco, fca, labels=labels)
680
680
681 # tell hggettext to extract docstrings from these functions:
681 # tell hggettext to extract docstrings from these functions:
682 i18nfunctions = internals.values()
682 i18nfunctions = internals.values()
General Comments 0
You need to be logged in to leave comments. Login now