[PATCH 1 of 2 v7] graft: support grafting across move/copy (issue4028)
Gábor STEFANIK
Gabor.STEFANIK at nng.com
Fri Aug 26 11:12:53 EDT 2016
>
--------------------------------------------------------------------------
This message, including its attachments, is confidential. For more information please read NNG's email policy here:
http://www.nng.com/emailpolicy/
By responding to this email you accept the email policy.
-----Original Message-----
> From: Mercurial-devel [mailto:mercurial-devel-bounces at mercurial-scm.org]
> On Behalf Of Gábor Stefanik
> Sent: Friday, August 26, 2016 1:01 AM
> To: mercurial-devel at mercurial-scm.org
> Subject: [PATCH 1 of 2 v7] graft: support grafting across move/copy
> (issue4028)
>
> # HG changeset patch
> # User Gábor Stefanik <gabor.stefanik at nng.com> # Date 1472156862 -7200
> # Thu Aug 25 22:27:42 2016 +0200
> # Node ID a887e7516d5b37c0b55202fa1ec9db6909a03ce0
> # Parent b1809f5d7630a3fff0fa715bbd30dba0f07672a8
> 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
> v6: fix reversed logic
> v7: partial rewrite to correctly handle divergent renames, address review
> comments, adjust more tests, and polish up some edge cases
>
> diff --git a/mercurial/copies.py b/mercurial/copies.py
> --- a/mercurial/copies.py
> +++ b/mercurial/copies.py
> @@ -321,6 +321,20 @@
> if repo.ui.configbool('experimental', 'disablecopytrace'):
> return {}, {}, {}, {}
>
> + # In certain scenarios (e.g. graft, update or rebase), 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.
> + cta = ca
> + # ca.descendant(wc) and ca.descendant(ca) are False, work around that
> + _c1 = c1.p1() if c1.rev() is None else c1
> + _c2 = c2.p1() if c2.rev() is None else c2
> + dirty_c1 = not (ca == _c1 or ca.descendant(_c1))
> + dirty_c2 = not (ca == _c2 or ca.descendant(_c2))
> + graft = dirty_c1 or dirty_c2
> + if graft:
> + cta = _c1.ancestor(_c2)
> +
> limit = _findlimit(repo, c1.rev(), c2.rev())
> if limit is None:
> # no common ancestor, no copies @@ -330,28 +344,54 @@
> m1 = c1.manifest()
> m2 = c2.manifest()
> ma = ca.manifest()
> + mta = cta.manifest()
>
> - copy1, copy2, = {}, {}
> + # see checkcopies documentation below for these dicts
> + copy1, copy2 = {}, {}
> + incomplete1, incomplete2 = {}, {}
> movewithdir1, movewithdir2 = {}, {}
> fullcopy1, fullcopy2 = {}, {}
> - diverge = {}
> + diverge, incompletediverge = {}, {}
>
> # find interesting file sets from manifests
> + if graft:
> + repo.ui.debug(" computing unmatched files in rotated DAG\n")
> addedinm1 = m1.filesnotin(ma)
> addedinm2 = m2.filesnotin(ma)
> - u1, u2 = _computenonoverlap(repo, c1, c2, addedinm1, addedinm2)
> + _u1, _u2 = _computenonoverlap(repo, c1, c2, addedinm1, addedinm2)
> + if not graft:
> + u1, u2 = _u1, _u2
> + else: # need to recompute this for directory move handling when grafting
> + repo.ui.debug(" computing unmatched files in unrotated DAG\n")
> + u1, u2 = _computenonoverlap(repo, c1, c2, m1.filesnotin(mta),
> + m2.filesnotin(mta))
> +
> 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, dirty_c1, limit, diverge, copy1,
> + fullcopy1, incomplete1, incompletediverge)
>
> for f in u2:
> - checkcopies(c2, f, m2, m1, ca, limit, diverge, copy2, fullcopy2)
> + checkcopies(c2, f, m2, m1, ca, cta, dirty_c2, limit, diverge, copy2,
> + fullcopy2, incomplete2, incompletediverge)
>
> 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
> + copyfrom, copyto = incomplete1, incomplete2
> + if dirty_c1:
> + copyfrom, copyto = incomplete2, incomplete1
> + for f in copyfrom:
> + copy[copyto[f]] = copyfrom[f]
PATCHSET V7 RECALLED! This can fail if a file was renamed between cta and ca, and deleted between ca and c1. Fix coming soon.
> + del copyto[f]
> + for f in incompletediverge:
> + ic = incompletediverge[f]
> + assert f not in diverge
> + diverge[f] = [copyto[ic[0]], ic[1]]
> +
> renamedelete = {}
> renamedeleteset = set()
> divergeset = set()
> @@ -369,10 +409,25 @@
> if bothnew:
> repo.ui.debug(" unmatched files new in both:\n %s\n"
> % "\n ".join(bothnew))
> - bothdiverge, _copy, _fullcopy = {}, {}, {}
> + bothdiverge = {}
> + _copy, _fullcopy = {}, {} # dummy dicts
> + _incomplete1, _incomplete2, _diverge = {}, {}, {}
> 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, dirty_c1, limit, bothdiverge,
> + _copy, _fullcopy, _incomplete1, _diverge)
> + checkcopies(c2, f, m2, m1, ca, cta, dirty_c2, limit, bothdiverge,
> + _copy, _fullcopy, _incomplete2, _diverge)
> + _incomplete = _incomplete2
> + if dirty_c1:
> + _incomplete = _incomplete1
> + assert _incomplete2 == {}
> + else:
> + assert _incomplete1 == {}
> + for f in _diverge:
> + ic = _diverge[f]
> + assert f not in bothdiverge
> + bothdiverge[f] = [_incomplete.get(ic[0], ic[0]), ic[1]]
> +
> 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 +493,7 @@
> (d, dirmove[d]))
>
> # check unaccounted nonoverlapping files against directory moves
> - for f in u1 + u2:
> + for f in _u1 + _u2:
> if f not in fullcopy:
> for d in dirmove:
> if f.startswith(d):
> @@ -452,7 +507,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, remote_ca, limit, diverge, copy,
> + fullcopy, incomplete, incompletediverge):
> """
> check possible copies of f from m1 to m2
>
> @@ -460,14 +516,20 @@
> 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
> + remote_ca = True if ca is outside cta::m1, False otherwise
> 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
> + incomplete = record non-divergent partial copies here
> + incompletediverge = record divergent partial copies here
> """
>
> ma = ca.manifest()
> + mta = cta.manifest()
> + backwards = ca != cta and not remote_ca and f in ma
> getfctx = _makegetfctx(ctx)
>
> def _related(f1, f2, limit):
> @@ -502,31 +564,57 @@
> return False
>
> of = None
> - seen = set([f])
> - for oc in getfctx(f, m1[f]).ancestors():
> + seen = {f: [getfctx(f, m1[f])]}
> + for oc in seen[f][0].ancestors():
> ocr = oc.linkrev()
> of = oc.path()
> if of in seen:
> + seen[of].append(oc)
> # check limit late - grab last rename before
> if ocr < limit:
> break
> continue
> - seen.add(of)
> + seen[of] = [oc]
>
> - 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
> + return # 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
> - of = None
> - break
> + if backwards:
> + copy[of] = f
> + elif of in ma:
> + copy[f] = of
> + elif remote_ca: # special case: a <- b <- a -> b "ping-pong" rename
> + copy[of] = f
> + del fullcopy[f]
> + fullcopy[of] = f
> + else: # divergence w.r.t. graft CA on one side of topological CA
> + for sf in seen:
> + if sf in ma and getfctx(sf, ma[sf]) in seen[sf]:
> + assert sf not in diverge
> + diverge[sf] = [f, of]
> + break
> + return
>
> - if of in ma:
> - diverge.setdefault(of, []).append(f)
> + if of in mta:
> + if backwards or remote_ca:
> + incomplete[of] = f
> + else:
> + for sf in seen:
> + if sf in ma and getfctx(sf, ma[sf]) in seen[sf]:
> + if cta == ca:
> + diverge.setdefault(sf, []).append(f)
> + else:
> + incompletediverge[sf] = [of, f]
> + return
>
> 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,13 @@
> committing changelog
> grafting 5:97f8bfe72746 "5"
> searching for copies back to rev 1
> + computing unmatched files in rotated DAG
> + computing unmatched files in unrotated DAG
> + unmatched files in other:
> + c
> + 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 +200,13 @@
> scanning for duplicate grafts
> grafting 4:9c233e8e184d "4"
> searching for copies back to rev 1
> + computing unmatched files in rotated DAG
> + computing unmatched files in unrotated DAG
> + unmatched files in other:
> + c
> + 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 @@ -
> 427,8 +441,8 @@
> $ hg graft 3 --log -u foo
> grafting 3:4c60f11aa304 "3"
> warning: can't find ancestor for 'c' copied from 'b'!
> - $ hg log --template '{rev} {parents} {desc}\n' -r tip
> - 14 1:5d205f8b35b6 3
> + $ hg log --template '{rev}:{node|short} {parents} {desc}\n' -r tip
> + 14:0c921c65ef1e 1:5d205f8b35b6 3
> (grafted from 4c60f11aa304a54ae1c199feb94e7fc771e51ed8)
>
> Resolve conflicted graft
> @@ -620,7 +634,7 @@
> date: Thu Jan 01 00:00:00 1970 +0000
> summary: 2
>
> - changeset: 14:f64defefacee
> + changeset: 14:0c921c65ef1e
> parent: 1:5d205f8b35b6
> user: foo
> date: Thu Jan 01 00:00:00 1970 +0000
> @@ -842,3 +856,290 @@
> |/
> o 0
>
> +Graft from behind a move or rename
> +NOTE: This is affected by bug 5343, and will need updating when it's
> +fixed
> +
> +f50 tests a "ping-pong" rename case, where a file is renamed to the
> +same name on both branches, then the rename is backed out on one
> +branch, and the backout is grafted to the other branch. This creates a
> +challenging b <- a <- b -> a rename sequence in the graft target,
> +topological CA, graft CA and graft source, respectively. Since rename
> +detection will run on the target side for such a sequence (as for
> +technical reasons, we split the source and target sides not at the
> +graft CA, but at the topological CA), it will pick up a false rename,
> +and cause a spurious merge conflict. This false rename is always
> +exactly the reverse of the true rename that would be detected on the
> source side, so deal with it by detecting this condition and reversing as
> necessary.
> +
> + $ hg init ../graftmove
> + $ cd ../graftmove
> + $ echo c10 > f10
> + $ echo c20 > f20
> + $ echo c30 > f30
> + $ echo c40 > f40
> + $ echo c50 > f50
> + $ hg ci -qAm 0
> + $ hg mv f10 f11
> + $ hg mv f30 f31
> + $ hg mv f50 f51
> + $ hg ci -qAm 1
> + $ echo c12 > f11
> + $ hg mv f20 f22
> + $ hg mv f51 f50
> + $ hg ci -qAm 2
> + $ hg mv f31 f33
> + $ echo c43 > f40
> + $ hg ci -qAm 3
> + $ hg up -q 0
> + $ hg graft -r 2
> + grafting 2:52f80b3a6c3e "2"
> + merging f10 and f11 to f10
> + warning: can't find ancestor for 'f50' copied from 'f51'!
> + $ hg status --change .
> + M f10
> + A f22
> + R f20
> + $ hg cat f10
> + c12
> + $ hg cat f11
> + f11: no such file in rev 54f7d8995b01 [1] $ hg graft -r 3 grafting
> + 3:0f6524134ac0 "3"
> + note: possible conflict - f31 was renamed multiple times to:
> + f33
> + f30
> + warning: can't find ancestor for 'f33' copied from 'f31'!
> + $ hg up -q 0
> + $ hg mv f10 f16
> + $ echo c26 > f20
> + $ hg mv f30 f36
> + $ hg mv f40 f46
> + $ hg mv f50 f51
> + $ hg ci -qAm 6
> + $ hg graft -r 2
> + grafting 2:52f80b3a6c3e "2"
> + merging f16 and f11 to f16
> + merging f20 and f22 to f22
> + $ hg graft -r 3
> + grafting 3:0f6524134ac0 "3"
> + note: possible conflict - f31 was renamed multiple times to:
> + f36
> + f33
> + merging f46 and f40 to f46
> + warning: can't find ancestor for 'f33' copied from 'f31'!
> + $ hg log -CGv --patch --git
> + @ changeset: 8:db9925d9208e
> + | tag: tip
> + | user: test
> + | date: Thu Jan 01 00:00:00 1970 +0000
> + | files: f33 f46
> + | description:
> + | 3
> + |
> + |
> + | diff --git a/f33 b/f33
> + | new file mode 100644
> + | --- /dev/null
> + | +++ b/f33
> + | @@ -0,0 +1,1 @@
> + | +c30
> + | diff --git a/f46 b/f46
> + | --- a/f46
> + | +++ b/f46
> + | @@ -1,1 +1,1 @@
> + | -c40
> + | +c43
> + |
> + o changeset: 7:08113e8ff5c8
> + | user: test
> + | date: Thu Jan 01 00:00:00 1970 +0000
> + | files: f16 f20 f22 f50 f51
> + | copies: f22 (f20) f50 (f51)
> + | description:
> + | 2
> + |
> + |
> + | diff --git a/f16 b/f16
> + | --- a/f16
> + | +++ b/f16
> + | @@ -1,1 +1,1 @@
> + | -c10
> + | +c12
> + | diff --git a/f20 b/f22
> + | rename from f20
> + | rename to f22
> + | diff --git a/f51 b/f50
> + | rename from f51
> + | rename to f50
> + |
> + o changeset: 6:365d1fa57acb
> + | parent: 0:1060512e332e
> + | user: test
> + | date: Thu Jan 01 00:00:00 1970 +0000
> + | files: f10 f16 f20 f30 f36 f40 f46 f50 f51
> + | copies: f16 (f10) f36 (f30) f46 (f40) f51 (f50)
> + | description:
> + | 6
> + |
> + |
> + | diff --git a/f10 b/f16
> + | rename from f10
> + | rename to f16
> + | diff --git a/f20 b/f20
> + | --- a/f20
> + | +++ b/f20
> + | @@ -1,1 +1,1 @@
> + | -c20
> + | +c26
> + | diff --git a/f30 b/f36
> + | rename from f30
> + | rename to f36
> + | diff --git a/f40 b/f46
> + | rename from f40
> + | rename to f46
> + | diff --git a/f50 b/f51
> + | rename from f50
> + | rename to f51
> + |
> + | o changeset: 5:d5015e1a1de5
> + | | user: test
> + | | date: Thu Jan 01 00:00:00 1970 +0000
> + | | files: f33 f40
> + | | description:
> + | | 3
> + | |
> + | |
> + | | diff --git a/f33 b/f33
> + | | new file mode 100644
> + | | --- /dev/null
> + | | +++ b/f33
> + | | @@ -0,0 +1,1 @@
> + | | +c30
> + | | diff --git a/f40 b/f40
> + | | --- a/f40
> + | | +++ b/f40
> + | | @@ -1,1 +1,1 @@
> + | | -c40
> + | | +c43
> + | |
> + | o changeset: 4:54f7d8995b01
> + |/ parent: 0:1060512e332e
> + | user: test
> + | date: Thu Jan 01 00:00:00 1970 +0000
> + | files: f10 f20 f22
> + | copies: f22 (f20)
> + | description:
> + | 2
> + |
> + |
> + | diff --git a/f10 b/f10
> + | --- a/f10
> + | +++ b/f10
> + | @@ -1,1 +1,1 @@
> + | -c10
> + | +c12
> + | diff --git a/f20 b/f22
> + | rename from f20
> + | rename to f22
> + |
> + | o changeset: 3:0f6524134ac0
> + | | user: test
> + | | date: Thu Jan 01 00:00:00 1970 +0000
> + | | files: f31 f33 f40
> + | | copies: f33 (f31)
> + | | description:
> + | | 3
> + | |
> + | |
> + | | diff --git a/f31 b/f33
> + | | rename from f31
> + | | rename to f33
> + | | diff --git a/f40 b/f40
> + | | --- a/f40
> + | | +++ b/f40
> + | | @@ -1,1 +1,1 @@
> + | | -c40
> + | | +c43
> + | |
> + | o changeset: 2:52f80b3a6c3e
> + | | user: test
> + | | date: Thu Jan 01 00:00:00 1970 +0000
> + | | files: f11 f20 f22 f50 f51
> + | | copies: f22 (f20) f50 (f51)
> + | | description:
> + | | 2
> + | |
> + | |
> + | | diff --git a/f11 b/f11
> + | | --- a/f11
> + | | +++ b/f11
> + | | @@ -1,1 +1,1 @@
> + | | -c10
> + | | +c12
> + | | diff --git a/f20 b/f22
> + | | rename from f20
> + | | rename to f22
> + | | diff --git a/f51 b/f50
> + | | rename from f51
> + | | rename to f50
> + | |
> + | o changeset: 1:7607a972f96d
> + |/ user: test
> + | date: Thu Jan 01 00:00:00 1970 +0000
> + | files: f10 f11 f30 f31 f50 f51
> + | copies: f11 (f10) f31 (f30) f51 (f50)
> + | description:
> + | 1
> + |
> + |
> + | diff --git a/f10 b/f11
> + | rename from f10
> + | rename to f11
> + | diff --git a/f30 b/f31
> + | rename from f30
> + | rename to f31
> + | diff --git a/f50 b/f51
> + | rename from f50
> + | rename to f51
> + |
> + o changeset: 0:1060512e332e
> + user: test
> + date: Thu Jan 01 00:00:00 1970 +0000
> + files: f10 f20 f30 f40 f50
> + description:
> + 0
> +
> +
> + diff --git a/f10 b/f10
> + new file mode 100644
> + --- /dev/null
> + +++ b/f10
> + @@ -0,0 +1,1 @@
> + +c10
> + diff --git a/f20 b/f20
> + new file mode 100644
> + --- /dev/null
> + +++ b/f20
> + @@ -0,0 +1,1 @@
> + +c20
> + diff --git a/f30 b/f30
> + new file mode 100644
> + --- /dev/null
> + +++ b/f30
> + @@ -0,0 +1,1 @@
> + +c30
> + diff --git a/f40 b/f40
> + new file mode 100644
> + --- /dev/null
> + +++ b/f40
> + @@ -0,0 +1,1 @@
> + +c40
> + diff --git a/f50 b/f50
> + new file mode 100644
> + --- /dev/null
> + +++ b/f50
> + @@ -0,0 +1,1 @@
> + +c50
> +
> + $ hg cat f22
> + c26
> diff --git a/tests/test-rebase-conflicts.t b/tests/test-rebase-conflicts.t
> --- a/tests/test-rebase-conflicts.t
> +++ b/tests/test-rebase-conflicts.t
> @@ -238,6 +238,10 @@
> merge against 9:e31216eec445
> detach base 8:8e4e2c1a07ae
> searching for copies back to rev 3
> + computing unmatched files in rotated DAG
> + computing unmatched files in unrotated DAG
> + unmatched files in other:
> + f2.txt
> resolving manifests
> branchmerge: True, force: True, partial: False
> ancestor: 8e4e2c1a07ae, local: 4bc80088dc6b+, remote: e31216eec445 @@
> -255,6 +259,10 @@
> merge against 10:2f2496ddf49d
> detach base 9:e31216eec445
> searching for copies back to rev 3
> + computing unmatched files in rotated DAG
> + computing unmatched files in unrotated DAG
> + unmatched files in other:
> + f2.txt
> resolving manifests
> branchmerge: True, force: True, partial: False
> ancestor: e31216eec445, local: 19c888675e13+, remote: 2f2496ddf49d
> _______________________________________________
> Mercurial-devel mailing list
> Mercurial-devel at mercurial-scm.org
> https://www.mercurial-scm.org/mailman/listinfo/mercurial-devel
More information about the Mercurial-devel
mailing list