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