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