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