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