##// END OF EJS Templates
filemerge: only write in-memory backup during premerge...
Phil Cohen -
r35721:9a50ffd1 default
parent child Browse files
Show More
@@ -1,845 +1,846 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 env = {'HG_FILE': fcd.path(),
516 env = {'HG_FILE': fcd.path(),
517 'HG_MY_NODE': short(mynode),
517 'HG_MY_NODE': short(mynode),
518 'HG_OTHER_NODE': str(fco.changectx()),
518 'HG_OTHER_NODE': str(fco.changectx()),
519 'HG_BASE_NODE': str(fca.changectx()),
519 'HG_BASE_NODE': str(fca.changectx()),
520 'HG_MY_ISLINK': 'l' in fcd.flags(),
520 'HG_MY_ISLINK': 'l' in fcd.flags(),
521 'HG_OTHER_ISLINK': 'l' in fco.flags(),
521 'HG_OTHER_ISLINK': 'l' in fco.flags(),
522 'HG_BASE_ISLINK': 'l' in fca.flags(),
522 'HG_BASE_ISLINK': 'l' in fca.flags(),
523 }
523 }
524 ui = repo.ui
524 ui = repo.ui
525
525
526 args = _toolstr(ui, tool, "args")
526 args = _toolstr(ui, tool, "args")
527 if "$output" in args:
527 if "$output" in args:
528 # read input from backup, write to original
528 # read input from backup, write to original
529 out = a
529 out = a
530 a = repo.wvfs.join(back.path())
530 a = repo.wvfs.join(back.path())
531 replace = {'local': a, 'base': b, 'other': c, 'output': out}
531 replace = {'local': a, 'base': b, 'other': c, 'output': out}
532 args = util.interpolate(r'\$', replace, args,
532 args = util.interpolate(r'\$', replace, args,
533 lambda s: util.shellquote(util.localpath(s)))
533 lambda s: util.shellquote(util.localpath(s)))
534 cmd = toolpath + ' ' + args
534 cmd = toolpath + ' ' + args
535 if _toolbool(ui, tool, "gui"):
535 if _toolbool(ui, tool, "gui"):
536 repo.ui.status(_('running merge tool %s for file %s\n') %
536 repo.ui.status(_('running merge tool %s for file %s\n') %
537 (tool, fcd.path()))
537 (tool, fcd.path()))
538 repo.ui.debug('launching merge tool: %s\n' % cmd)
538 repo.ui.debug('launching merge tool: %s\n' % cmd)
539 r = ui.system(cmd, cwd=repo.root, environ=env, blockedtag='mergetool')
539 r = ui.system(cmd, cwd=repo.root, environ=env, blockedtag='mergetool')
540 repo.ui.debug('merge tool returned: %d\n' % r)
540 repo.ui.debug('merge tool returned: %d\n' % r)
541 return True, r, False
541 return True, r, False
542 finally:
542 finally:
543 util.unlink(b)
543 util.unlink(b)
544 util.unlink(c)
544 util.unlink(c)
545
545
546 def _formatconflictmarker(ctx, template, label, pad):
546 def _formatconflictmarker(ctx, template, label, pad):
547 """Applies the given template to the ctx, prefixed by the label.
547 """Applies the given template to the ctx, prefixed by the label.
548
548
549 Pad is the minimum width of the label prefix, so that multiple markers
549 Pad is the minimum width of the label prefix, so that multiple markers
550 can have aligned templated parts.
550 can have aligned templated parts.
551 """
551 """
552 if ctx.node() is None:
552 if ctx.node() is None:
553 ctx = ctx.p1()
553 ctx = ctx.p1()
554
554
555 props = {'ctx': ctx}
555 props = {'ctx': ctx}
556 templateresult = template.render(props)
556 templateresult = template.render(props)
557
557
558 label = ('%s:' % label).ljust(pad + 1)
558 label = ('%s:' % label).ljust(pad + 1)
559 mark = '%s %s' % (label, templateresult)
559 mark = '%s %s' % (label, templateresult)
560
560
561 if mark:
561 if mark:
562 mark = mark.splitlines()[0] # split for safety
562 mark = mark.splitlines()[0] # split for safety
563
563
564 # 8 for the prefix of conflict marker lines (e.g. '<<<<<<< ')
564 # 8 for the prefix of conflict marker lines (e.g. '<<<<<<< ')
565 return util.ellipsis(mark, 80 - 8)
565 return util.ellipsis(mark, 80 - 8)
566
566
567 _defaultconflictlabels = ['local', 'other']
567 _defaultconflictlabels = ['local', 'other']
568
568
569 def _formatlabels(repo, fcd, fco, fca, labels):
569 def _formatlabels(repo, fcd, fco, fca, labels):
570 """Formats the given labels using the conflict marker template.
570 """Formats the given labels using the conflict marker template.
571
571
572 Returns a list of formatted labels.
572 Returns a list of formatted labels.
573 """
573 """
574 cd = fcd.changectx()
574 cd = fcd.changectx()
575 co = fco.changectx()
575 co = fco.changectx()
576 ca = fca.changectx()
576 ca = fca.changectx()
577
577
578 ui = repo.ui
578 ui = repo.ui
579 template = ui.config('ui', 'mergemarkertemplate')
579 template = ui.config('ui', 'mergemarkertemplate')
580 template = templater.unquotestring(template)
580 template = templater.unquotestring(template)
581 tres = formatter.templateresources(ui, repo)
581 tres = formatter.templateresources(ui, repo)
582 tmpl = formatter.maketemplater(ui, template, defaults=templatekw.keywords,
582 tmpl = formatter.maketemplater(ui, template, defaults=templatekw.keywords,
583 resources=tres)
583 resources=tres)
584
584
585 pad = max(len(l) for l in labels)
585 pad = max(len(l) for l in labels)
586
586
587 newlabels = [_formatconflictmarker(cd, tmpl, labels[0], pad),
587 newlabels = [_formatconflictmarker(cd, tmpl, labels[0], pad),
588 _formatconflictmarker(co, tmpl, labels[1], pad)]
588 _formatconflictmarker(co, tmpl, labels[1], pad)]
589 if len(labels) > 2:
589 if len(labels) > 2:
590 newlabels.append(_formatconflictmarker(ca, tmpl, labels[2], pad))
590 newlabels.append(_formatconflictmarker(ca, tmpl, labels[2], pad))
591 return newlabels
591 return newlabels
592
592
593 def partextras(labels):
593 def partextras(labels):
594 """Return a dictionary of extra labels for use in prompts to the user
594 """Return a dictionary of extra labels for use in prompts to the user
595
595
596 Intended use is in strings of the form "(l)ocal%(l)s".
596 Intended use is in strings of the form "(l)ocal%(l)s".
597 """
597 """
598 if labels is None:
598 if labels is None:
599 return {
599 return {
600 "l": "",
600 "l": "",
601 "o": "",
601 "o": "",
602 }
602 }
603
603
604 return {
604 return {
605 "l": " [%s]" % labels[0],
605 "l": " [%s]" % labels[0],
606 "o": " [%s]" % labels[1],
606 "o": " [%s]" % labels[1],
607 }
607 }
608
608
609 def _restorebackup(fcd, back):
609 def _restorebackup(fcd, back):
610 # TODO: Add a workingfilectx.write(otherfilectx) path so we can use
610 # TODO: Add a workingfilectx.write(otherfilectx) path so we can use
611 # util.copy here instead.
611 # util.copy here instead.
612 fcd.write(back.data(), fcd.flags())
612 fcd.write(back.data(), fcd.flags())
613
613
614 def _makebackup(repo, ui, wctx, fcd, premerge):
614 def _makebackup(repo, ui, wctx, fcd, premerge):
615 """Makes and returns a filectx-like object for ``fcd``'s backup file.
615 """Makes and returns a filectx-like object for ``fcd``'s backup file.
616
616
617 In addition to preserving the user's pre-existing modifications to `fcd`
617 In addition to preserving the user's pre-existing modifications to `fcd`
618 (if any), the backup is used to undo certain premerges, confirm whether a
618 (if any), the backup is used to undo certain premerges, confirm whether a
619 merge changed anything, and determine what line endings the new file should
619 merge changed anything, and determine what line endings the new file should
620 have.
620 have.
621
621
622 Backups only need to be written once (right before the premerge) since their
622 Backups only need to be written once (right before the premerge) since their
623 content doesn't change afterwards.
623 content doesn't change afterwards.
624 """
624 """
625 if fcd.isabsent():
625 if fcd.isabsent():
626 return None
626 return None
627 # TODO: Break this import cycle somehow. (filectx -> ctx -> fileset ->
627 # TODO: Break this import cycle somehow. (filectx -> ctx -> fileset ->
628 # merge -> filemerge). (I suspect the fileset import is the weakest link)
628 # merge -> filemerge). (I suspect the fileset import is the weakest link)
629 from . import context
629 from . import context
630 a = _workingpath(repo, fcd)
630 a = _workingpath(repo, fcd)
631 back = scmutil.origpath(ui, repo, a)
631 back = scmutil.origpath(ui, repo, a)
632 inworkingdir = (back.startswith(repo.wvfs.base) and not
632 inworkingdir = (back.startswith(repo.wvfs.base) and not
633 back.startswith(repo.vfs.base))
633 back.startswith(repo.vfs.base))
634 if isinstance(fcd, context.overlayworkingfilectx) and inworkingdir:
634 if isinstance(fcd, context.overlayworkingfilectx) and inworkingdir:
635 # If the backup file is to be in the working directory, and we're
635 # If the backup file is to be in the working directory, and we're
636 # merging in-memory, we must redirect the backup to the memory context
636 # merging in-memory, we must redirect the backup to the memory context
637 # so we don't disturb the working directory.
637 # so we don't disturb the working directory.
638 relpath = back[len(repo.wvfs.base) + 1:]
638 relpath = back[len(repo.wvfs.base) + 1:]
639 if premerge:
639 wctx[relpath].write(fcd.data(), fcd.flags())
640 wctx[relpath].write(fcd.data(), fcd.flags())
640 return wctx[relpath]
641 return wctx[relpath]
641 else:
642 else:
642 if premerge:
643 if premerge:
643 # Otherwise, write to wherever path the user specified the backups
644 # Otherwise, write to wherever path the user specified the backups
644 # should go. We still need to switch based on whether the source is
645 # should go. We still need to switch based on whether the source is
645 # in-memory so we can use the fast path of ``util.copy`` if both are
646 # in-memory so we can use the fast path of ``util.copy`` if both are
646 # on disk.
647 # on disk.
647 if isinstance(fcd, context.overlayworkingfilectx):
648 if isinstance(fcd, context.overlayworkingfilectx):
648 util.writefile(back, fcd.data())
649 util.writefile(back, fcd.data())
649 else:
650 else:
650 util.copyfile(a, back)
651 util.copyfile(a, back)
651 # A arbitraryfilectx is returned, so we can run the same functions on
652 # A arbitraryfilectx is returned, so we can run the same functions on
652 # the backup context regardless of where it lives.
653 # the backup context regardless of where it lives.
653 return context.arbitraryfilectx(back, repo=repo)
654 return context.arbitraryfilectx(back, repo=repo)
654
655
655 def _maketempfiles(repo, fco, fca):
656 def _maketempfiles(repo, fco, fca):
656 """Writes out `fco` and `fca` as temporary files, so an external merge
657 """Writes out `fco` and `fca` as temporary files, so an external merge
657 tool may use them.
658 tool may use them.
658 """
659 """
659 def temp(prefix, ctx):
660 def temp(prefix, ctx):
660 fullbase, ext = os.path.splitext(ctx.path())
661 fullbase, ext = os.path.splitext(ctx.path())
661 pre = "%s~%s." % (os.path.basename(fullbase), prefix)
662 pre = "%s~%s." % (os.path.basename(fullbase), prefix)
662 (fd, name) = tempfile.mkstemp(prefix=pre, suffix=ext)
663 (fd, name) = tempfile.mkstemp(prefix=pre, suffix=ext)
663 data = repo.wwritedata(ctx.path(), ctx.data())
664 data = repo.wwritedata(ctx.path(), ctx.data())
664 f = os.fdopen(fd, pycompat.sysstr("wb"))
665 f = os.fdopen(fd, pycompat.sysstr("wb"))
665 f.write(data)
666 f.write(data)
666 f.close()
667 f.close()
667 return name
668 return name
668
669
669 b = temp("base", fca)
670 b = temp("base", fca)
670 c = temp("other", fco)
671 c = temp("other", fco)
671
672
672 return b, c
673 return b, c
673
674
674 def _filemerge(premerge, repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
675 def _filemerge(premerge, repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
675 """perform a 3-way merge in the working directory
676 """perform a 3-way merge in the working directory
676
677
677 premerge = whether this is a premerge
678 premerge = whether this is a premerge
678 mynode = parent node before merge
679 mynode = parent node before merge
679 orig = original local filename before merge
680 orig = original local filename before merge
680 fco = other file context
681 fco = other file context
681 fca = ancestor file context
682 fca = ancestor file context
682 fcd = local file context for current/destination file
683 fcd = local file context for current/destination file
683
684
684 Returns whether the merge is complete, the return value of the merge, and
685 Returns whether the merge is complete, the return value of the merge, and
685 a boolean indicating whether the file was deleted from disk."""
686 a boolean indicating whether the file was deleted from disk."""
686
687
687 if not fco.cmp(fcd): # files identical?
688 if not fco.cmp(fcd): # files identical?
688 return True, None, False
689 return True, None, False
689
690
690 ui = repo.ui
691 ui = repo.ui
691 fd = fcd.path()
692 fd = fcd.path()
692 binary = fcd.isbinary() or fco.isbinary() or fca.isbinary()
693 binary = fcd.isbinary() or fco.isbinary() or fca.isbinary()
693 symlink = 'l' in fcd.flags() + fco.flags()
694 symlink = 'l' in fcd.flags() + fco.flags()
694 changedelete = fcd.isabsent() or fco.isabsent()
695 changedelete = fcd.isabsent() or fco.isabsent()
695 tool, toolpath = _picktool(repo, ui, fd, binary, symlink, changedelete)
696 tool, toolpath = _picktool(repo, ui, fd, binary, symlink, changedelete)
696 if tool in internals and tool.startswith('internal:'):
697 if tool in internals and tool.startswith('internal:'):
697 # normalize to new-style names (':merge' etc)
698 # normalize to new-style names (':merge' etc)
698 tool = tool[len('internal'):]
699 tool = tool[len('internal'):]
699 ui.debug("picked tool '%s' for %s (binary %s symlink %s changedelete %s)\n"
700 ui.debug("picked tool '%s' for %s (binary %s symlink %s changedelete %s)\n"
700 % (tool, fd, pycompat.bytestr(binary), pycompat.bytestr(symlink),
701 % (tool, fd, pycompat.bytestr(binary), pycompat.bytestr(symlink),
701 pycompat.bytestr(changedelete)))
702 pycompat.bytestr(changedelete)))
702
703
703 if tool in internals:
704 if tool in internals:
704 func = internals[tool]
705 func = internals[tool]
705 mergetype = func.mergetype
706 mergetype = func.mergetype
706 onfailure = func.onfailure
707 onfailure = func.onfailure
707 precheck = func.precheck
708 precheck = func.precheck
708 else:
709 else:
709 if wctx.isinmemory():
710 if wctx.isinmemory():
710 func = _xmergeimm
711 func = _xmergeimm
711 else:
712 else:
712 func = _xmerge
713 func = _xmerge
713 mergetype = fullmerge
714 mergetype = fullmerge
714 onfailure = _("merging %s failed!\n")
715 onfailure = _("merging %s failed!\n")
715 precheck = None
716 precheck = None
716
717
717 toolconf = tool, toolpath, binary, symlink
718 toolconf = tool, toolpath, binary, symlink
718
719
719 if mergetype == nomerge:
720 if mergetype == nomerge:
720 r, deleted = func(repo, mynode, orig, fcd, fco, fca, toolconf, labels)
721 r, deleted = func(repo, mynode, orig, fcd, fco, fca, toolconf, labels)
721 return True, r, deleted
722 return True, r, deleted
722
723
723 if premerge:
724 if premerge:
724 if orig != fco.path():
725 if orig != fco.path():
725 ui.status(_("merging %s and %s to %s\n") % (orig, fco.path(), fd))
726 ui.status(_("merging %s and %s to %s\n") % (orig, fco.path(), fd))
726 else:
727 else:
727 ui.status(_("merging %s\n") % fd)
728 ui.status(_("merging %s\n") % fd)
728
729
729 ui.debug("my %s other %s ancestor %s\n" % (fcd, fco, fca))
730 ui.debug("my %s other %s ancestor %s\n" % (fcd, fco, fca))
730
731
731 if precheck and not precheck(repo, mynode, orig, fcd, fco, fca,
732 if precheck and not precheck(repo, mynode, orig, fcd, fco, fca,
732 toolconf):
733 toolconf):
733 if onfailure:
734 if onfailure:
734 if wctx.isinmemory():
735 if wctx.isinmemory():
735 raise error.InMemoryMergeConflictsError('in-memory merge does '
736 raise error.InMemoryMergeConflictsError('in-memory merge does '
736 'not support merge '
737 'not support merge '
737 'conflicts')
738 'conflicts')
738 ui.warn(onfailure % fd)
739 ui.warn(onfailure % fd)
739 return True, 1, False
740 return True, 1, False
740
741
741 back = _makebackup(repo, ui, wctx, fcd, premerge)
742 back = _makebackup(repo, ui, wctx, fcd, premerge)
742 files = (None, None, None, back)
743 files = (None, None, None, back)
743 r = 1
744 r = 1
744 try:
745 try:
745 markerstyle = ui.config('ui', 'mergemarkers')
746 markerstyle = ui.config('ui', 'mergemarkers')
746 if not labels:
747 if not labels:
747 labels = _defaultconflictlabels
748 labels = _defaultconflictlabels
748 if markerstyle != 'basic':
749 if markerstyle != 'basic':
749 labels = _formatlabels(repo, fcd, fco, fca, labels)
750 labels = _formatlabels(repo, fcd, fco, fca, labels)
750
751
751 if premerge and mergetype == fullmerge:
752 if premerge and mergetype == fullmerge:
752 r = _premerge(repo, fcd, fco, fca, toolconf, files, labels=labels)
753 r = _premerge(repo, fcd, fco, fca, toolconf, files, labels=labels)
753 # complete if premerge successful (r is 0)
754 # complete if premerge successful (r is 0)
754 return not r, r, False
755 return not r, r, False
755
756
756 needcheck, r, deleted = func(repo, mynode, orig, fcd, fco, fca,
757 needcheck, r, deleted = func(repo, mynode, orig, fcd, fco, fca,
757 toolconf, files, labels=labels)
758 toolconf, files, labels=labels)
758
759
759 if needcheck:
760 if needcheck:
760 r = _check(repo, r, ui, tool, fcd, files)
761 r = _check(repo, r, ui, tool, fcd, files)
761
762
762 if r:
763 if r:
763 if onfailure:
764 if onfailure:
764 if wctx.isinmemory():
765 if wctx.isinmemory():
765 raise error.InMemoryMergeConflictsError('in-memory merge '
766 raise error.InMemoryMergeConflictsError('in-memory merge '
766 'does not support '
767 'does not support '
767 'merge conflicts')
768 'merge conflicts')
768 ui.warn(onfailure % fd)
769 ui.warn(onfailure % fd)
769 _onfilemergefailure(ui)
770 _onfilemergefailure(ui)
770
771
771 return True, r, deleted
772 return True, r, deleted
772 finally:
773 finally:
773 if not r and back is not None:
774 if not r and back is not None:
774 back.remove()
775 back.remove()
775
776
776 def _haltmerge():
777 def _haltmerge():
777 msg = _('merge halted after failed merge (see hg resolve)')
778 msg = _('merge halted after failed merge (see hg resolve)')
778 raise error.InterventionRequired(msg)
779 raise error.InterventionRequired(msg)
779
780
780 def _onfilemergefailure(ui):
781 def _onfilemergefailure(ui):
781 action = ui.config('merge', 'on-failure')
782 action = ui.config('merge', 'on-failure')
782 if action == 'prompt':
783 if action == 'prompt':
783 msg = _('continue merge operation (yn)?' '$$ &Yes $$ &No')
784 msg = _('continue merge operation (yn)?' '$$ &Yes $$ &No')
784 if ui.promptchoice(msg, 0) == 1:
785 if ui.promptchoice(msg, 0) == 1:
785 _haltmerge()
786 _haltmerge()
786 if action == 'halt':
787 if action == 'halt':
787 _haltmerge()
788 _haltmerge()
788 # default action is 'continue', in which case we neither prompt nor halt
789 # default action is 'continue', in which case we neither prompt nor halt
789
790
790 def _check(repo, r, ui, tool, fcd, files):
791 def _check(repo, r, ui, tool, fcd, files):
791 fd = fcd.path()
792 fd = fcd.path()
792 unused, unused, unused, back = files
793 unused, unused, unused, back = files
793
794
794 if not r and (_toolbool(ui, tool, "checkconflicts") or
795 if not r and (_toolbool(ui, tool, "checkconflicts") or
795 'conflicts' in _toollist(ui, tool, "check")):
796 'conflicts' in _toollist(ui, tool, "check")):
796 if re.search("^(<<<<<<< .*|=======|>>>>>>> .*)$", fcd.data(),
797 if re.search("^(<<<<<<< .*|=======|>>>>>>> .*)$", fcd.data(),
797 re.MULTILINE):
798 re.MULTILINE):
798 r = 1
799 r = 1
799
800
800 checked = False
801 checked = False
801 if 'prompt' in _toollist(ui, tool, "check"):
802 if 'prompt' in _toollist(ui, tool, "check"):
802 checked = True
803 checked = True
803 if ui.promptchoice(_("was merge of '%s' successful (yn)?"
804 if ui.promptchoice(_("was merge of '%s' successful (yn)?"
804 "$$ &Yes $$ &No") % fd, 1):
805 "$$ &Yes $$ &No") % fd, 1):
805 r = 1
806 r = 1
806
807
807 if not r and not checked and (_toolbool(ui, tool, "checkchanged") or
808 if not r and not checked and (_toolbool(ui, tool, "checkchanged") or
808 'changed' in
809 'changed' in
809 _toollist(ui, tool, "check")):
810 _toollist(ui, tool, "check")):
810 if back is not None and not fcd.cmp(back):
811 if back is not None and not fcd.cmp(back):
811 if ui.promptchoice(_(" output file %s appears unchanged\n"
812 if ui.promptchoice(_(" output file %s appears unchanged\n"
812 "was merge successful (yn)?"
813 "was merge successful (yn)?"
813 "$$ &Yes $$ &No") % fd, 1):
814 "$$ &Yes $$ &No") % fd, 1):
814 r = 1
815 r = 1
815
816
816 if back is not None and _toolbool(ui, tool, "fixeol"):
817 if back is not None and _toolbool(ui, tool, "fixeol"):
817 _matcheol(_workingpath(repo, fcd), back)
818 _matcheol(_workingpath(repo, fcd), back)
818
819
819 return r
820 return r
820
821
821 def _workingpath(repo, ctx):
822 def _workingpath(repo, ctx):
822 return repo.wjoin(ctx.path())
823 return repo.wjoin(ctx.path())
823
824
824 def premerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
825 def premerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
825 return _filemerge(True, repo, wctx, mynode, orig, fcd, fco, fca,
826 return _filemerge(True, repo, wctx, mynode, orig, fcd, fco, fca,
826 labels=labels)
827 labels=labels)
827
828
828 def filemerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
829 def filemerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
829 return _filemerge(False, repo, wctx, mynode, orig, fcd, fco, fca,
830 return _filemerge(False, repo, wctx, mynode, orig, fcd, fco, fca,
830 labels=labels)
831 labels=labels)
831
832
832 def loadinternalmerge(ui, extname, registrarobj):
833 def loadinternalmerge(ui, extname, registrarobj):
833 """Load internal merge tool from specified registrarobj
834 """Load internal merge tool from specified registrarobj
834 """
835 """
835 for name, func in registrarobj._table.iteritems():
836 for name, func in registrarobj._table.iteritems():
836 fullname = ':' + name
837 fullname = ':' + name
837 internals[fullname] = func
838 internals[fullname] = func
838 internals['internal:' + name] = func
839 internals['internal:' + name] = func
839 internalsdoc[fullname] = func
840 internalsdoc[fullname] = func
840
841
841 # load built-in merge tools explicitly to setup internalsdoc
842 # load built-in merge tools explicitly to setup internalsdoc
842 loadinternalmerge(None, None, internaltool)
843 loadinternalmerge(None, None, internaltool)
843
844
844 # tell hggettext to extract docstrings from these functions:
845 # tell hggettext to extract docstrings from these functions:
845 i18nfunctions = internals.values()
846 i18nfunctions = internals.values()
General Comments 0
You need to be logged in to leave comments. Login now