Mercurial > public > mercurial-scm > hg
comparison mercurial/filemerge.py @ 37077:1e30a26a65d0
filemerge: make the 'local' path match the format that 'base' and 'other' use
If we pass a separate '$output' arg to the merge tool, we produce four files:
local, base, other, and output. In this situation, 'output' will be the
original filename, 'base' and 'other' are temporary files, and previously
'local' would be the backup file (so if 'output' was foo.txt, 'local' would be
foo.txt.orig).
This change makes it so that 'local' follows the same pattern as 'base' and
'other' - it will be a temporary file either in the
`experimental.mergetempdirprefix`-controlled directory with a name like
foo~local.txt, or in the normal system-wide temp dir with a name like
foo~local.RaNd0m.txt.
For the cases where the merge tool does not use an '$output' arg, 'local' is
still the destination filename, and 'base' and 'other' are unchanged.
The hope is that this is much easier for people to reason about; rather than
having a tool like Meld pop up with three panes, one of them with the filename
"foo.txt.orig", one with the filename "foo.txt", and one with
"foo~other.StuFf2.txt", we can (when the merge temp dir stuff is enabled) make
it show up as "foo~local.txt", "foo.txt" and "foo~other.txt", respectively.
This also opens the door to future customization, such as getting the
operation-provided labels and a hash prefix into the filenames (so we see
something like "foo~dest.abc123", "foo.txt", and "foo~src.d4e5f6").
Differential Revision: https://phab.mercurial-scm.org/D2889
author | Kyle Lippincott <spectral@google.com> |
---|---|
date | Wed, 21 Mar 2018 12:36:29 -0700 |
parents | e349ad5cbb71 |
children | f0b6fbea00cf |
comparison
equal
deleted
inserted
replaced
37076:66d478064d5f | 37077:1e30a26a65d0 |
---|---|
510 repo.ui.warn(_('warning: %s cannot merge change/delete conflict ' | 510 repo.ui.warn(_('warning: %s cannot merge change/delete conflict ' |
511 'for %s\n') % (tool, fcd.path())) | 511 'for %s\n') % (tool, fcd.path())) |
512 return False, 1, None | 512 return False, 1, None |
513 unused, unused, unused, back = files | 513 unused, unused, unused, back = files |
514 localpath = _workingpath(repo, fcd) | 514 localpath = _workingpath(repo, fcd) |
515 with _maketempfiles(repo, fco, fca) as temppaths: | 515 args = _toolstr(repo.ui, tool, "args") |
516 basepath, otherpath = temppaths | 516 |
517 with _maketempfiles(repo, fco, fca, repo.wvfs.join(back.path()), | |
518 "$output" in args) as temppaths: | |
519 basepath, otherpath, localoutputpath = temppaths | |
517 outpath = "" | 520 outpath = "" |
518 mylabel, otherlabel = labels[:2] | 521 mylabel, otherlabel = labels[:2] |
519 if len(labels) >= 3: | 522 if len(labels) >= 3: |
520 baselabel = labels[2] | 523 baselabel = labels[2] |
521 else: | 524 else: |
531 'HG_OTHER_LABEL': otherlabel, | 534 'HG_OTHER_LABEL': otherlabel, |
532 'HG_BASE_LABEL': baselabel, | 535 'HG_BASE_LABEL': baselabel, |
533 } | 536 } |
534 ui = repo.ui | 537 ui = repo.ui |
535 | 538 |
536 args = _toolstr(ui, tool, "args") | |
537 if "$output" in args: | 539 if "$output" in args: |
538 # read input from backup, write to original | 540 # read input from backup, write to original |
539 outpath = localpath | 541 outpath = localpath |
540 localpath = repo.wvfs.join(back.path()) | 542 localpath = localoutputpath |
541 replace = {'local': localpath, 'base': basepath, 'other': otherpath, | 543 replace = {'local': localpath, 'base': basepath, 'other': otherpath, |
542 'output': outpath, 'labellocal': mylabel, | 544 'output': outpath, 'labellocal': mylabel, |
543 'labelother': otherlabel, 'labelbase': baselabel} | 545 'labelother': otherlabel, 'labelbase': baselabel} |
544 args = util.interpolate(br'\$', replace, args, | 546 args = util.interpolate(br'\$', replace, args, |
545 lambda s: util.shellquote(util.localpath(s))) | 547 lambda s: util.shellquote(util.localpath(s))) |
663 # A arbitraryfilectx is returned, so we can run the same functions on | 665 # A arbitraryfilectx is returned, so we can run the same functions on |
664 # the backup context regardless of where it lives. | 666 # the backup context regardless of where it lives. |
665 return context.arbitraryfilectx(back, repo=repo) | 667 return context.arbitraryfilectx(back, repo=repo) |
666 | 668 |
667 @contextlib.contextmanager | 669 @contextlib.contextmanager |
668 def _maketempfiles(repo, fco, fca): | 670 def _maketempfiles(repo, fco, fca, localpath, uselocalpath): |
669 """Writes out `fco` and `fca` as temporary files, so an external merge | 671 """Writes out `fco` and `fca` as temporary files, and (if uselocalpath) |
670 tool may use them. | 672 copies `localpath` to another temporary file, so an external merge tool may |
673 use them. | |
671 """ | 674 """ |
672 tmproot = None | 675 tmproot = None |
673 tmprootprefix = repo.ui.config('experimental', 'mergetempdirprefix') | 676 tmprootprefix = repo.ui.config('experimental', 'mergetempdirprefix') |
674 if tmprootprefix: | 677 if tmprootprefix: |
675 tmproot = tempfile.mkdtemp(prefix=tmprootprefix) | 678 tmproot = tempfile.mkdtemp(prefix=tmprootprefix) |
676 | 679 |
677 def temp(prefix, ctx): | 680 def maketempfrompath(prefix, path): |
678 fullbase, ext = os.path.splitext(ctx.path()) | 681 fullbase, ext = os.path.splitext(path) |
679 pre = "%s~%s" % (os.path.basename(fullbase), prefix) | 682 pre = "%s~%s" % (os.path.basename(fullbase), prefix) |
680 if tmproot: | 683 if tmproot: |
681 name = os.path.join(tmproot, pre) | 684 name = os.path.join(tmproot, pre) |
682 if ext: | 685 if ext: |
683 name += ext | 686 name += ext |
684 f = open(name, r"wb") | 687 f = open(name, r"wb") |
685 else: | 688 else: |
686 (fd, name) = tempfile.mkstemp(prefix=pre + '.', suffix=ext) | 689 fd, name = tempfile.mkstemp(prefix=pre + '.', suffix=ext) |
687 f = os.fdopen(fd, r"wb") | 690 f = os.fdopen(fd, r"wb") |
691 return f, name | |
692 | |
693 def tempfromcontext(prefix, ctx): | |
694 f, name = maketempfrompath(prefix, ctx.path()) | |
688 data = repo.wwritedata(ctx.path(), ctx.data()) | 695 data = repo.wwritedata(ctx.path(), ctx.data()) |
689 f.write(data) | 696 f.write(data) |
690 f.close() | 697 f.close() |
691 return name | 698 return name |
692 | 699 |
693 b = temp("base", fca) | 700 b = tempfromcontext("base", fca) |
694 c = temp("other", fco) | 701 c = tempfromcontext("other", fco) |
702 d = localpath | |
703 if uselocalpath: | |
704 # We start off with this being the backup filename, so remove the .orig | |
705 # to make syntax-highlighting more likely. | |
706 if d.endswith('.orig'): | |
707 d, _ = os.path.splitext(d) | |
708 f, d = maketempfrompath("local", d) | |
709 with open(localpath, 'rb') as src: | |
710 f.write(src.read()) | |
711 f.close() | |
712 | |
695 try: | 713 try: |
696 yield b, c | 714 yield b, c, d |
697 finally: | 715 finally: |
698 if tmproot: | 716 if tmproot: |
699 shutil.rmtree(tmproot) | 717 shutil.rmtree(tmproot) |
700 else: | 718 else: |
701 util.unlink(b) | 719 util.unlink(b) |
702 util.unlink(c) | 720 util.unlink(c) |
721 # if not uselocalpath, d is the 'orig'/backup file which we | |
722 # shouldn't delete. | |
723 if d and uselocalpath: | |
724 util.unlink(d) | |
703 | 725 |
704 def _filemerge(premerge, repo, wctx, mynode, orig, fcd, fco, fca, labels=None): | 726 def _filemerge(premerge, repo, wctx, mynode, orig, fcd, fco, fca, labels=None): |
705 """perform a 3-way merge in the working directory | 727 """perform a 3-way merge in the working directory |
706 | 728 |
707 premerge = whether this is a premerge | 729 premerge = whether this is a premerge |