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