copies: follow copies across merge base without source file (issue6163)
authorMartin von Zweigbergk <martinvonz@google.com>
Fri, 28 Jun 2019 12:59:21 -0700
changeset 42595 819712deac69
parent 42594 d013099c551b
child 42596 83666f011679
copies: follow copies across merge base without source file (issue6163) As in the previous patch, consider these two histories: @ 4 'rename x to y' | o 3 'add x again' | o 2 'remove x' | | o 1 'modify x' |/ o 0 'add x' @ 4 'rename x to y' | o 3 'add x again' | | o 2 'modify x' | | | o 1 'add x' |/ o 0 'base' We trace copies from the 'modify x' commit to commit 4 by going via the merge base (commit 0). When tracing file 'y' (_tracefile()) in the first case, we immediately find the rename from 'x'. We check to see if 'x' exists in the merge base, which it does, so we consider it a valid copy. In the second case, 'x' does not exist in the merge base, so it's not considered a valid copy. As a workaround, this patch makes it so we also attempt the check in mergecopies's base commit (commit 1 in the second case). That feels pretty ugly to me, but I don't have any better ideas. Note that we actually also check not only that the filename matches, but also that the file's nodeid matches. I don't know why we do that, but it was like that already before I rewrote mergecopies(). That means that the rebase will still fail in cases like this (again, it already failed before my rewrite): @ 4 'rename x to y' | o 3 'add x again with content X2' | o 2 'remove x' | | o 1 'modify x to content X2' |/ o 1 'modify x to content X1' | o 0 'add x with content X0' Differential Revision: https://phab.mercurial-scm.org/D6604
mercurial/copies.py
tests/test-copies-unrelated.t
--- a/mercurial/copies.py	Tue Jun 25 14:25:03 2019 -0700
+++ b/mercurial/copies.py	Fri Jun 28 12:59:21 2019 -0700
@@ -150,7 +150,7 @@
             t[k] = v
     return t
 
-def _tracefile(fctx, am, limit):
+def _tracefile(fctx, am, basemf, limit):
     """return file context that is the ancestor of fctx present in ancestor
     manifest am, stopping after the first ancestor lower than limit"""
 
@@ -158,6 +158,8 @@
         path = f.path()
         if am.get(path, None) == f.filenode():
             return path
+        if basemf and basemf.get(path, None) == f.filenode():
+            return path
         if not f.isintroducedafter(limit):
             return None
 
@@ -183,7 +185,7 @@
     return (repo.ui.config('experimental', 'copies.read-from') in
             ('changeset-only', 'compatibility'))
 
-def _committedforwardcopies(a, b, match):
+def _committedforwardcopies(a, b, base, match):
     """Like _forwardcopies(), but b.rev() cannot be None (working copy)"""
     # files might have to be traced back to the fctx parent of the last
     # one-side-only changeset, but not further back than that
@@ -201,6 +203,7 @@
     if debug:
         dbg('debug.copies:      search limit: %d\n' % limit)
     am = a.manifest()
+    basemf = None if base is None else base.manifest()
 
     # find where new files came from
     # we currently don't try to find where old files went, too expensive
@@ -232,7 +235,7 @@
 
         if debug:
             start = util.timer()
-        opath = _tracefile(fctx, am, limit)
+        opath = _tracefile(fctx, am, basemf, limit)
         if opath:
             if debug:
                 dbg('debug.copies:          rename of: %s\n' % opath)
@@ -311,17 +314,19 @@
             heapq.heappush(work, (c, parent, newcopies))
     assert False
 
-def _forwardcopies(a, b, match=None):
+def _forwardcopies(a, b, base=None, match=None):
     """find {dst@b: src@a} copy mapping where a is an ancestor of b"""
 
+    if base is None:
+        base = a
     match = a.repo().narrowmatch(match)
     # check for working copy
     if b.rev() is None:
-        cm = _committedforwardcopies(a, b.p1(), match)
+        cm = _committedforwardcopies(a, b.p1(), base, match)
         # combine copies from dirstate if necessary
         copies = _chain(cm, _dirstatecopies(b._repo, match))
     else:
-        copies  = _committedforwardcopies(a, b, match)
+        copies  = _committedforwardcopies(a, b, base, match)
     return copies
 
 def _backwardrenames(a, b, match):
@@ -369,8 +374,11 @@
     else:
         if debug:
             repo.ui.debug('debug.copies: search mode: combined\n')
+        base = None
+        if a.rev() != node.nullrev:
+            base = x
         copies = _chain(_backwardrenames(x, a, match=match),
-                        _forwardcopies(a, y, match=match))
+                        _forwardcopies(a, y, base, match=match))
     _filter(x, y, copies)
     return copies
 
--- a/tests/test-copies-unrelated.t	Tue Jun 25 14:25:03 2019 -0700
+++ b/tests/test-copies-unrelated.t	Fri Jun 28 12:59:21 2019 -0700
@@ -299,22 +299,10 @@
   o  0 base
      a
   $ hg debugpathcopies 1 4
-  x -> y (no-filelog !)
-#if filelog
-BROKEN: This should succeed and merge the changes from x into y
-  $ hg graft -r 2
-  grafting 2:* "modify x" (glob)
-  file 'x' was deleted in local [local] but was modified in other [graft].
-  What do you want to do?
-  use (c)hanged version, leave (d)eleted, or leave (u)nresolved? u
-  abort: unresolved conflicts, can't continue
-  (use 'hg resolve' and 'hg graft --continue')
-  [255]
-#else
+  x -> y
   $ hg graft -r 2
   grafting 2:* "modify x" (glob)
   merging y and x to y
-#endif
   $ hg co -qC 2
   $ hg graft -r 4
   grafting 4:* "rename x to y"* (glob)