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