# HG changeset patch # User Martin von Zweigbergk # Date 1561751961 25200 # Node ID 819712deac699a76c9100498cc98beb44ab2bd2c # Parent d013099c551b92b3b7585b116ba18652f1b4f2c0 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 diff -r d013099c551b -r 819712deac69 mercurial/copies.py --- 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 diff -r d013099c551b -r 819712deac69 tests/test-copies-unrelated.t --- 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)