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