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