##// END OF EJS Templates
filemerge: fix backing up an in-memory file to a custom location...
Phil Cohen -
r35720:c0439e11 default
parent child Browse files
Show More
@@ -1,838 +1,845 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
622 Backups only need to be written once (right before the premerge) since their
623 content doesn't change afterwards.
621 """
624 """
622 if fcd.isabsent():
625 if fcd.isabsent():
623 return None
626 return None
624 # TODO: Break this import cycle somehow. (filectx -> ctx -> fileset ->
627 # TODO: Break this import cycle somehow. (filectx -> ctx -> fileset ->
625 # merge -> filemerge). (I suspect the fileset import is the weakest link)
628 # merge -> filemerge). (I suspect the fileset import is the weakest link)
626 from . import context
629 from . import context
627 a = _workingpath(repo, fcd)
630 a = _workingpath(repo, fcd)
628 back = scmutil.origpath(ui, repo, a)
631 back = scmutil.origpath(ui, repo, a)
629 inworkingdir = (back.startswith(repo.wvfs.base) and not
632 inworkingdir = (back.startswith(repo.wvfs.base) and not
630 back.startswith(repo.vfs.base))
633 back.startswith(repo.vfs.base))
631
632 if isinstance(fcd, context.overlayworkingfilectx) and inworkingdir:
634 if isinstance(fcd, context.overlayworkingfilectx) and inworkingdir:
633 # 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
634 # 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
635 # so we don't disturb the working directory.
637 # so we don't disturb the working directory.
636 relpath = back[len(repo.wvfs.base) + 1:]
638 relpath = back[len(repo.wvfs.base) + 1:]
637 wctx[relpath].write(fcd.data(), fcd.flags())
639 wctx[relpath].write(fcd.data(), fcd.flags())
638 return wctx[relpath]
640 return wctx[relpath]
639 else:
641 else:
640 # Otherwise, write to wherever the user specified the backups should go.
642 if premerge:
641 #
643 # 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 # in-memory so we can use the fast path of ``util.copy`` if both are
646 # on disk.
647 if isinstance(fcd, context.overlayworkingfilectx):
648 util.writefile(back, fcd.data())
649 else:
650 util.copyfile(a, back)
642 # A arbitraryfilectx is returned, so we can run the same functions on
651 # A arbitraryfilectx is returned, so we can run the same functions on
643 # the backup context regardless of where it lives.
652 # the backup context regardless of where it lives.
644 if premerge:
645 util.copyfile(a, back)
646 return context.arbitraryfilectx(back, repo=repo)
653 return context.arbitraryfilectx(back, repo=repo)
647
654
648 def _maketempfiles(repo, fco, fca):
655 def _maketempfiles(repo, fco, fca):
649 """Writes out `fco` and `fca` as temporary files, so an external merge
656 """Writes out `fco` and `fca` as temporary files, so an external merge
650 tool may use them.
657 tool may use them.
651 """
658 """
652 def temp(prefix, ctx):
659 def temp(prefix, ctx):
653 fullbase, ext = os.path.splitext(ctx.path())
660 fullbase, ext = os.path.splitext(ctx.path())
654 pre = "%s~%s." % (os.path.basename(fullbase), prefix)
661 pre = "%s~%s." % (os.path.basename(fullbase), prefix)
655 (fd, name) = tempfile.mkstemp(prefix=pre, suffix=ext)
662 (fd, name) = tempfile.mkstemp(prefix=pre, suffix=ext)
656 data = repo.wwritedata(ctx.path(), ctx.data())
663 data = repo.wwritedata(ctx.path(), ctx.data())
657 f = os.fdopen(fd, pycompat.sysstr("wb"))
664 f = os.fdopen(fd, pycompat.sysstr("wb"))
658 f.write(data)
665 f.write(data)
659 f.close()
666 f.close()
660 return name
667 return name
661
668
662 b = temp("base", fca)
669 b = temp("base", fca)
663 c = temp("other", fco)
670 c = temp("other", fco)
664
671
665 return b, c
672 return b, c
666
673
667 def _filemerge(premerge, repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
674 def _filemerge(premerge, repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
668 """perform a 3-way merge in the working directory
675 """perform a 3-way merge in the working directory
669
676
670 premerge = whether this is a premerge
677 premerge = whether this is a premerge
671 mynode = parent node before merge
678 mynode = parent node before merge
672 orig = original local filename before merge
679 orig = original local filename before merge
673 fco = other file context
680 fco = other file context
674 fca = ancestor file context
681 fca = ancestor file context
675 fcd = local file context for current/destination file
682 fcd = local file context for current/destination file
676
683
677 Returns whether the merge is complete, the return value of the merge, and
684 Returns whether the merge is complete, the return value of the merge, and
678 a boolean indicating whether the file was deleted from disk."""
685 a boolean indicating whether the file was deleted from disk."""
679
686
680 if not fco.cmp(fcd): # files identical?
687 if not fco.cmp(fcd): # files identical?
681 return True, None, False
688 return True, None, False
682
689
683 ui = repo.ui
690 ui = repo.ui
684 fd = fcd.path()
691 fd = fcd.path()
685 binary = fcd.isbinary() or fco.isbinary() or fca.isbinary()
692 binary = fcd.isbinary() or fco.isbinary() or fca.isbinary()
686 symlink = 'l' in fcd.flags() + fco.flags()
693 symlink = 'l' in fcd.flags() + fco.flags()
687 changedelete = fcd.isabsent() or fco.isabsent()
694 changedelete = fcd.isabsent() or fco.isabsent()
688 tool, toolpath = _picktool(repo, ui, fd, binary, symlink, changedelete)
695 tool, toolpath = _picktool(repo, ui, fd, binary, symlink, changedelete)
689 if tool in internals and tool.startswith('internal:'):
696 if tool in internals and tool.startswith('internal:'):
690 # normalize to new-style names (':merge' etc)
697 # normalize to new-style names (':merge' etc)
691 tool = tool[len('internal'):]
698 tool = tool[len('internal'):]
692 ui.debug("picked tool '%s' for %s (binary %s symlink %s changedelete %s)\n"
699 ui.debug("picked tool '%s' for %s (binary %s symlink %s changedelete %s)\n"
693 % (tool, fd, pycompat.bytestr(binary), pycompat.bytestr(symlink),
700 % (tool, fd, pycompat.bytestr(binary), pycompat.bytestr(symlink),
694 pycompat.bytestr(changedelete)))
701 pycompat.bytestr(changedelete)))
695
702
696 if tool in internals:
703 if tool in internals:
697 func = internals[tool]
704 func = internals[tool]
698 mergetype = func.mergetype
705 mergetype = func.mergetype
699 onfailure = func.onfailure
706 onfailure = func.onfailure
700 precheck = func.precheck
707 precheck = func.precheck
701 else:
708 else:
702 if wctx.isinmemory():
709 if wctx.isinmemory():
703 func = _xmergeimm
710 func = _xmergeimm
704 else:
711 else:
705 func = _xmerge
712 func = _xmerge
706 mergetype = fullmerge
713 mergetype = fullmerge
707 onfailure = _("merging %s failed!\n")
714 onfailure = _("merging %s failed!\n")
708 precheck = None
715 precheck = None
709
716
710 toolconf = tool, toolpath, binary, symlink
717 toolconf = tool, toolpath, binary, symlink
711
718
712 if mergetype == nomerge:
719 if mergetype == nomerge:
713 r, deleted = func(repo, mynode, orig, fcd, fco, fca, toolconf, labels)
720 r, deleted = func(repo, mynode, orig, fcd, fco, fca, toolconf, labels)
714 return True, r, deleted
721 return True, r, deleted
715
722
716 if premerge:
723 if premerge:
717 if orig != fco.path():
724 if orig != fco.path():
718 ui.status(_("merging %s and %s to %s\n") % (orig, fco.path(), fd))
725 ui.status(_("merging %s and %s to %s\n") % (orig, fco.path(), fd))
719 else:
726 else:
720 ui.status(_("merging %s\n") % fd)
727 ui.status(_("merging %s\n") % fd)
721
728
722 ui.debug("my %s other %s ancestor %s\n" % (fcd, fco, fca))
729 ui.debug("my %s other %s ancestor %s\n" % (fcd, fco, fca))
723
730
724 if precheck and not precheck(repo, mynode, orig, fcd, fco, fca,
731 if precheck and not precheck(repo, mynode, orig, fcd, fco, fca,
725 toolconf):
732 toolconf):
726 if onfailure:
733 if onfailure:
727 if wctx.isinmemory():
734 if wctx.isinmemory():
728 raise error.InMemoryMergeConflictsError('in-memory merge does '
735 raise error.InMemoryMergeConflictsError('in-memory merge does '
729 'not support merge '
736 'not support merge '
730 'conflicts')
737 'conflicts')
731 ui.warn(onfailure % fd)
738 ui.warn(onfailure % fd)
732 return True, 1, False
739 return True, 1, False
733
740
734 back = _makebackup(repo, ui, wctx, fcd, premerge)
741 back = _makebackup(repo, ui, wctx, fcd, premerge)
735 files = (None, None, None, back)
742 files = (None, None, None, back)
736 r = 1
743 r = 1
737 try:
744 try:
738 markerstyle = ui.config('ui', 'mergemarkers')
745 markerstyle = ui.config('ui', 'mergemarkers')
739 if not labels:
746 if not labels:
740 labels = _defaultconflictlabels
747 labels = _defaultconflictlabels
741 if markerstyle != 'basic':
748 if markerstyle != 'basic':
742 labels = _formatlabels(repo, fcd, fco, fca, labels)
749 labels = _formatlabels(repo, fcd, fco, fca, labels)
743
750
744 if premerge and mergetype == fullmerge:
751 if premerge and mergetype == fullmerge:
745 r = _premerge(repo, fcd, fco, fca, toolconf, files, labels=labels)
752 r = _premerge(repo, fcd, fco, fca, toolconf, files, labels=labels)
746 # complete if premerge successful (r is 0)
753 # complete if premerge successful (r is 0)
747 return not r, r, False
754 return not r, r, False
748
755
749 needcheck, r, deleted = func(repo, mynode, orig, fcd, fco, fca,
756 needcheck, r, deleted = func(repo, mynode, orig, fcd, fco, fca,
750 toolconf, files, labels=labels)
757 toolconf, files, labels=labels)
751
758
752 if needcheck:
759 if needcheck:
753 r = _check(repo, r, ui, tool, fcd, files)
760 r = _check(repo, r, ui, tool, fcd, files)
754
761
755 if r:
762 if r:
756 if onfailure:
763 if onfailure:
757 if wctx.isinmemory():
764 if wctx.isinmemory():
758 raise error.InMemoryMergeConflictsError('in-memory merge '
765 raise error.InMemoryMergeConflictsError('in-memory merge '
759 'does not support '
766 'does not support '
760 'merge conflicts')
767 'merge conflicts')
761 ui.warn(onfailure % fd)
768 ui.warn(onfailure % fd)
762 _onfilemergefailure(ui)
769 _onfilemergefailure(ui)
763
770
764 return True, r, deleted
771 return True, r, deleted
765 finally:
772 finally:
766 if not r and back is not None:
773 if not r and back is not None:
767 back.remove()
774 back.remove()
768
775
769 def _haltmerge():
776 def _haltmerge():
770 msg = _('merge halted after failed merge (see hg resolve)')
777 msg = _('merge halted after failed merge (see hg resolve)')
771 raise error.InterventionRequired(msg)
778 raise error.InterventionRequired(msg)
772
779
773 def _onfilemergefailure(ui):
780 def _onfilemergefailure(ui):
774 action = ui.config('merge', 'on-failure')
781 action = ui.config('merge', 'on-failure')
775 if action == 'prompt':
782 if action == 'prompt':
776 msg = _('continue merge operation (yn)?' '$$ &Yes $$ &No')
783 msg = _('continue merge operation (yn)?' '$$ &Yes $$ &No')
777 if ui.promptchoice(msg, 0) == 1:
784 if ui.promptchoice(msg, 0) == 1:
778 _haltmerge()
785 _haltmerge()
779 if action == 'halt':
786 if action == 'halt':
780 _haltmerge()
787 _haltmerge()
781 # default action is 'continue', in which case we neither prompt nor halt
788 # default action is 'continue', in which case we neither prompt nor halt
782
789
783 def _check(repo, r, ui, tool, fcd, files):
790 def _check(repo, r, ui, tool, fcd, files):
784 fd = fcd.path()
791 fd = fcd.path()
785 unused, unused, unused, back = files
792 unused, unused, unused, back = files
786
793
787 if not r and (_toolbool(ui, tool, "checkconflicts") or
794 if not r and (_toolbool(ui, tool, "checkconflicts") or
788 'conflicts' in _toollist(ui, tool, "check")):
795 'conflicts' in _toollist(ui, tool, "check")):
789 if re.search("^(<<<<<<< .*|=======|>>>>>>> .*)$", fcd.data(),
796 if re.search("^(<<<<<<< .*|=======|>>>>>>> .*)$", fcd.data(),
790 re.MULTILINE):
797 re.MULTILINE):
791 r = 1
798 r = 1
792
799
793 checked = False
800 checked = False
794 if 'prompt' in _toollist(ui, tool, "check"):
801 if 'prompt' in _toollist(ui, tool, "check"):
795 checked = True
802 checked = True
796 if ui.promptchoice(_("was merge of '%s' successful (yn)?"
803 if ui.promptchoice(_("was merge of '%s' successful (yn)?"
797 "$$ &Yes $$ &No") % fd, 1):
804 "$$ &Yes $$ &No") % fd, 1):
798 r = 1
805 r = 1
799
806
800 if not r and not checked and (_toolbool(ui, tool, "checkchanged") or
807 if not r and not checked and (_toolbool(ui, tool, "checkchanged") or
801 'changed' in
808 'changed' in
802 _toollist(ui, tool, "check")):
809 _toollist(ui, tool, "check")):
803 if back is not None and not fcd.cmp(back):
810 if back is not None and not fcd.cmp(back):
804 if ui.promptchoice(_(" output file %s appears unchanged\n"
811 if ui.promptchoice(_(" output file %s appears unchanged\n"
805 "was merge successful (yn)?"
812 "was merge successful (yn)?"
806 "$$ &Yes $$ &No") % fd, 1):
813 "$$ &Yes $$ &No") % fd, 1):
807 r = 1
814 r = 1
808
815
809 if back is not None and _toolbool(ui, tool, "fixeol"):
816 if back is not None and _toolbool(ui, tool, "fixeol"):
810 _matcheol(_workingpath(repo, fcd), back)
817 _matcheol(_workingpath(repo, fcd), back)
811
818
812 return r
819 return r
813
820
814 def _workingpath(repo, ctx):
821 def _workingpath(repo, ctx):
815 return repo.wjoin(ctx.path())
822 return repo.wjoin(ctx.path())
816
823
817 def premerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
824 def premerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
818 return _filemerge(True, repo, wctx, mynode, orig, fcd, fco, fca,
825 return _filemerge(True, repo, wctx, mynode, orig, fcd, fco, fca,
819 labels=labels)
826 labels=labels)
820
827
821 def filemerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
828 def filemerge(repo, wctx, mynode, orig, fcd, fco, fca, labels=None):
822 return _filemerge(False, repo, wctx, mynode, orig, fcd, fco, fca,
829 return _filemerge(False, repo, wctx, mynode, orig, fcd, fco, fca,
823 labels=labels)
830 labels=labels)
824
831
825 def loadinternalmerge(ui, extname, registrarobj):
832 def loadinternalmerge(ui, extname, registrarobj):
826 """Load internal merge tool from specified registrarobj
833 """Load internal merge tool from specified registrarobj
827 """
834 """
828 for name, func in registrarobj._table.iteritems():
835 for name, func in registrarobj._table.iteritems():
829 fullname = ':' + name
836 fullname = ':' + name
830 internals[fullname] = func
837 internals[fullname] = func
831 internals['internal:' + name] = func
838 internals['internal:' + name] = func
832 internalsdoc[fullname] = func
839 internalsdoc[fullname] = func
833
840
834 # load built-in merge tools explicitly to setup internalsdoc
841 # load built-in merge tools explicitly to setup internalsdoc
835 loadinternalmerge(None, None, internaltool)
842 loadinternalmerge(None, None, internaltool)
836
843
837 # tell hggettext to extract docstrings from these functions:
844 # tell hggettext to extract docstrings from these functions:
838 i18nfunctions = internals.values()
845 i18nfunctions = internals.values()
General Comments 0
You need to be logged in to leave comments. Login now