[PATCH 2 of 3] graft: support grafting across move/copy (issue4028)

Gábor Stefanik gabor.stefanik at nng.com
Thu Aug 4 11:23:00 EDT 2016


# HG changeset patch
# User Gábor Stefanik <gabor.stefanik at nng.com>
# Date 1470324130 -7200
#      Thu Aug 04 17:22:10 2016 +0200
# Node ID 408f76d8f0b6e7cf473fe4c98abf102220810afa
# Parent  74f67f7f8cf5a21b3107437df65afdcc34575217
graft: support grafting across move/copy (issue4028)

Graft performs a merge with a false common ancestor, which must be taken into
account when tracking copies. Explicitly pass the real common ancestor in this
case, and track copies between the real and false common ancestors in reverse.

With this change, when grafting a commit with a change to a file moved earlier
on the graft's source branch, the change is merged as expected into the original
(unmoved) file, rather than recreating it under its new name.
It should also make it possible to eventually enable cross-branch updates with
merge.

v2: handle the case when target branch also has a rename
v3: address review comments
v4: move ancestry check to mergecopies, split tests to separate commit
v5: split out parameter change, address review comments, re-include tests

diff --git a/mercurial/copies.py b/mercurial/copies.py
--- a/mercurial/copies.py
+++ b/mercurial/copies.py
@@ -8,6 +8,7 @@
 from __future__ import absolute_import
 
 import heapq
+import operator
 
 from . import (
     node,
@@ -321,6 +322,24 @@
     if repo.ui.configbool('experimental', 'disablecopytrace'):
         return {}, {}, {}, {}
 
+    # In certain scenarios (e.g. graft, update or rename), ca can be overridden
+    # We still need to know a real common ancestor in this case
+    # We can't just compute _c1.ancestor(_c2) and compare it to ca, because
+    # there can be multiple common ancestors, e.g. in case of bidmerge.
+    graft = False
+    cta = ca
+    # ca.descendant(wc) and ca.descendant(ca) are False, work around that
+    _c1 = c1 if c1.rev() is None else c1.p1()
+    _c2 = c2 if c2.rev() is None else c2.p1()
+    if ca.rev() is None: # the working copy can never be a common ancestor
+        graft = True
+    elif not (ca == _c1 or ca.descendant(_c1)):
+        graft = True
+    elif not (ca == _c2 or ca.descendant(_c2)):
+        graft = True
+    if graft:
+        cta = _c1.ancestor(_c2)
+
     limit = _findlimit(repo, c1.rev(), c2.rev())
     if limit is None:
         # no common ancestor, no copies
@@ -330,28 +349,44 @@
     m1 = c1.manifest()
     m2 = c2.manifest()
     ma = ca.manifest()
+    mta = cta.manifest()
 
+    # see checkcopies documentation below for these dicts
     copy1, copy2, = {}, {}
+    copyfrom, copyto = {}, {}
     movewithdir1, movewithdir2 = {}, {}
     fullcopy1, fullcopy2 = {}, {}
     diverge = {}
 
     # find interesting file sets from manifests
-    addedinm1 = m1.filesnotin(ma)
-    addedinm2 = m2.filesnotin(ma)
-    u1, u2 = _computenonoverlap(repo, c1, c2, addedinm1, addedinm2)
+    addedinm1 = m1.filesnotin(mta)
+    addedinm2 = m2.filesnotin(mta)
+    u1, u2 = _computenonoverlap(repo, c1, c2, addedinm1, addedinm2, graft)
+    if not graft:
+        unmatched = u1 + u2
+    else: # need to recompute this for directory move handling when grafting
+        unmatched = operator.add(*_computenonoverlap(repo, c1, c2,
+                                 m1.filesnotin(ma), m2.filesnotin(ma), False))
+
     bothnew = sorted(addedinm1 & addedinm2)
 
     for f in u1:
-        checkcopies(c1, f, m1, m2, ca, limit, diverge, copy1, fullcopy1)
+        checkcopies(c1, f, m1, m2, ca, cta, limit, diverge, copy1,
+                    fullcopy1, copyfrom, copyto)
 
     for f in u2:
-        checkcopies(c2, f, m2, m1, ca, limit, diverge, copy2, fullcopy2)
+        checkcopies(c2, f, m2, m1, ca, cta, limit, diverge, copy2,
+                    fullcopy2, copyfrom, copyto)
 
     copy = dict(copy1.items() + copy2.items())
     movewithdir = dict(movewithdir1.items() + movewithdir2.items())
     fullcopy = dict(fullcopy1.items() + fullcopy2.items())
 
+    # combine partial copy paths discovered in the previous step
+    for f in copyfrom:
+        if f in copyto:
+            copy[copyto[f]] = copyfrom[f]
+
     renamedelete = {}
     renamedeleteset = set()
     divergeset = set()
@@ -369,10 +404,13 @@
     if bothnew:
         repo.ui.debug("  unmatched files new in both:\n   %s\n"
                       % "\n   ".join(bothnew))
-    bothdiverge, _copy, _fullcopy = {}, {}, {}
+    bothdiverge = {}
+    _copy, _fullcopy, _copyfrom, _copyto = {}, {}, {}, {} # dummy dicts
     for f in bothnew:
-        checkcopies(c1, f, m1, m2, ca, limit, bothdiverge, _copy, _fullcopy)
-        checkcopies(c2, f, m2, m1, ca, limit, bothdiverge, _copy, _fullcopy)
+        checkcopies(c1, f, m1, m2, ca, cta, limit, bothdiverge, _copy,
+                    _fullcopy, _copyfrom, _copyto)
+        checkcopies(c2, f, m2, m1, ca, cta, limit, bothdiverge, _copy,
+                    _fullcopy, _copyfrom, _copyto)
     for of, fl in bothdiverge.items():
         if len(fl) == 2 and fl[0] == fl[1]:
             copy[fl[0]] = of # not actually divergent, just matching renames
@@ -438,7 +476,7 @@
                       (d, dirmove[d]))
 
     # check unaccounted nonoverlapping files against directory moves
-    for f in u1 + u2:
+    for f in unmatched:
         if f not in fullcopy:
             for d in dirmove:
                 if f.startswith(d):
@@ -452,7 +490,8 @@
 
     return copy, movewithdir, diverge, renamedelete
 
-def checkcopies(ctx, f, m1, m2, ca, limit, diverge, copy, fullcopy):
+def checkcopies(ctx, f, m1, m2, ca, cta, limit, diverge, copy, fullcopy,
+                copyfrom, copyto):
     """
     check possible copies of f from m1 to m2
 
@@ -460,14 +499,19 @@
     f = the filename to check
     m1 = the source manifest
     m2 = the destination manifest
-    ca = the changectx of the common ancestor
+    ca = the changectx of the common ancestor, overridden on graft
+    cta = topological common ancestor for graft-like scenarios
     limit = the rev number to not search beyond
     diverge = record all diverges in this dict
     copy = record all non-divergent copies in this dict
     fullcopy = record all copies in this dict
+    copyfrom = source sides of partially known copy tracks
+    copyto = destination sides of partially known copytracks
     """
 
     ma = ca.manifest()
+    mta = cta.manifest()
+    backwards = f in ma # graft common ancestor already contains the rename
     getfctx = _makegetfctx(ctx)
 
     def _related(f1, f2, limit):
@@ -513,20 +557,32 @@
             continue
         seen.add(of)
 
-        fullcopy[f] = of # remember for dir rename detection
+        # remember for dir rename detection
+        if backwards:
+            fullcopy[of] = f # grafting backwards through renames
+        else:
+            fullcopy[f] = of
         if of not in m2:
             continue # no match, keep looking
         if m2[of] == ma.get(of):
             break # no merge needed, quit early
         c2 = getfctx(of, m2[of])
-        cr = _related(oc, c2, ca.rev())
+        cr = _related(oc, c2, cta.rev())
         if cr and (of == f or of == c2.path()): # non-divergent
-            copy[f] = of
+            if backwards:
+                copy[of] = f
+            else:
+                copy[f] = of
             of = None
             break
 
     if of in ma:
         diverge.setdefault(of, []).append(f)
+    elif cta != ca and of in mta:
+        if backwards:
+            copyfrom[of] = f
+        else:
+            copyto[of] = f
 
 def duplicatecopies(repo, rev, fromrev, skiprev=None):
     '''reproduce copies from fromrev to rev in the dirstate
diff --git a/tests/test-graft.t b/tests/test-graft.t
--- a/tests/test-graft.t
+++ b/tests/test-graft.t
@@ -179,6 +179,11 @@
   committing changelog
   grafting 5:97f8bfe72746 "5"
     searching for copies back to rev 1
+    unmatched files new in both:
+     b
+    all copies found (* = to merge, ! = divergent, % = renamed and deleted):
+     src: 'c' -> dst: 'b' *
+    checking for directory renames
   resolving manifests
    branchmerge: True, force: True, partial: False
    ancestor: 4c60f11aa304, local: 6b9e5368ca4e+, remote: 97f8bfe72746
@@ -193,6 +198,11 @@
   scanning for duplicate grafts
   grafting 4:9c233e8e184d "4"
     searching for copies back to rev 1
+    unmatched files new in both:
+     b
+    all copies found (* = to merge, ! = divergent, % = renamed and deleted):
+     src: 'c' -> dst: 'b' *
+    checking for directory renames
   resolving manifests
    branchmerge: True, force: True, partial: False
    ancestor: 4c60f11aa304, local: 1905859650ec+, remote: 9c233e8e184d
@@ -842,3 +852,24 @@
   |/
   o  0
   
+Graft from behind a move or rename
+
+  $ hg init graftmove
+  $ cd graftmove
+  $ echo c1 > f1
+  $ hg ci -qAm 0
+  $ hg mv f1 f2
+  $ hg ci -qAm 1
+  $ echo c2 > f2
+  $ hg ci -qAm 2
+  $ hg up -q 0
+  $ hg graft -r 2
+  grafting 2:8a20493ece2a "2" (tip)
+  merging f1 and f2 to f1
+  $ hg status --change .
+  M f1
+  $ hg cat f1
+  c2
+  $ hg cat f2
+  f2: no such file in rev ee1f64f8088b
+  [1]


More information about the Mercurial-devel mailing list