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