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