scmutil: add a cleanupnodes method for developers
authorJun Wu <quark@fb.com>
Sun, 25 Jun 2017 13:31:56 -0700
changeset 33100 65cadeea6c22
parent 33099 fcd1c483f5ea
child 33101 9e0d222f5687
scmutil: add a cleanupnodes method for developers It's now common that an old node gets replaced by zero or more new nodes, that could happen with amend, rebase, histedit, etc. And it's a common requirement to do bookmark movements, strip or obsolete nodes and even moving working copy parent. Previously, amend, rebase, history have their own logic doing the above. This patch is an attempt to unify them and future code. This enables new developers to be able to do "replace X with Y" thing correctly, without any knowledge about bookmarks, strip or obsstore. The next step will be migrating rebase to the new API, so it works inside a transaction, and its code could be simplified.
mercurial/scmutil.py
tests/test-strip.t
--- a/mercurial/scmutil.py	Sun Jun 25 10:38:45 2017 -0700
+++ b/mercurial/scmutil.py	Sun Jun 25 13:31:56 2017 -0700
@@ -16,6 +16,8 @@
 
 from .i18n import _
 from .node import (
+    hex,
+    nullid,
     wdirid,
     wdirrev,
 )
@@ -24,6 +26,7 @@
     encoding,
     error,
     match as matchmod,
+    obsolete,
     pathutil,
     phases,
     pycompat,
@@ -561,6 +564,68 @@
 
     return fullorigpath + ".orig"
 
+def cleanupnodes(repo, mapping, operation):
+    """do common cleanups when old nodes are replaced by new nodes
+
+    That includes writing obsmarkers or stripping nodes, and moving bookmarks.
+    (we might also want to move working directory parent in the future)
+
+    mapping is {oldnode: [newnode]} or a iterable of nodes if they do not have
+    replacements. operation is a string, like "rebase".
+    """
+    if not util.safehasattr(mapping, 'items'):
+        mapping = {n: () for n in mapping}
+
+    with repo.transaction('cleanup') as tr:
+        # Move bookmarks
+        bmarks = repo._bookmarks
+        bmarkchanged = False
+        for oldnode, newnodes in mapping.items():
+            oldbmarks = repo.nodebookmarks(oldnode)
+            if not oldbmarks:
+                continue
+            bmarkchanged = True
+            if len(newnodes) > 1:
+                heads = list(repo.set('heads(%ln)', newnodes))
+                if len(heads) != 1:
+                    raise error.ProgrammingError(
+                        'cannot figure out bookmark movement')
+                newnode = heads[0].node()
+            elif len(newnodes) == 0:
+                # move bookmark backwards
+                roots = list(repo.set('max((::%n) - %ln)', oldnode,
+                                      list(mapping)))
+                if roots:
+                    newnode = roots[0].node()
+                else:
+                    newnode = nullid
+            else:
+                newnode = newnodes[0]
+            repo.ui.debug('moving bookmarks %r from %s to %s\n' %
+                          (oldbmarks, hex(oldnode), hex(newnode)))
+            for name in oldbmarks:
+                bmarks[name] = newnode
+        if bmarkchanged:
+            bmarks.recordchange(tr)
+
+        # Obsolete or strip nodes
+        if obsolete.isenabled(repo, obsolete.createmarkersopt):
+            # If a node is already obsoleted, and we want to obsolete it
+            # without a successor, skip that obssolete request since it's
+            # unnecessary. That's the "if s or not isobs(n)" check below.
+            # Also sort the node in topology order, that might be useful for
+            # some obsstore logic.
+            # NOTE: the filtering and sorting might belong to createmarkers.
+            isobs = repo.obsstore.successors.__contains__
+            sortfunc = lambda ns: repo.changelog.rev(ns[0])
+            rels = [(repo[n], (repo[m] for m in s))
+                    for n, s in sorted(mapping.items(), key=sortfunc)
+                    if s or not isobs(n)]
+            obsolete.createmarkers(repo, rels, operation=operation)
+        else:
+            from . import repair # avoid import cycle
+            repair.delayedstrip(repo.ui, repo, list(mapping), operation)
+
 def addremove(repo, matcher, prefix, opts=None, dry_run=None, similarity=None):
     if opts is None:
         opts = {}
--- a/tests/test-strip.t	Sun Jun 25 10:38:45 2017 -0700
+++ b/tests/test-strip.t	Sun Jun 25 13:31:56 2017 -0700
@@ -955,6 +955,7 @@
   >  \|/
   >   A   Z
   > EOS
+  $ cp -R . ../scmutilcleanup
 
   $ hg up -C I
   2 files updated, 0 files merged, 0 files removed, 0 files unresolved
@@ -990,3 +991,104 @@
   |/
   o  0:426bada5c675 A
   
+Test high-level scmutil.cleanupnodes API
+
+  $ cd $TESTTMP/scmutilcleanup
+  $ hg debugdrawdag <<'EOS'
+  >   D2  F2  G2   # D2, F2, G2 are replacements for D, F, G
+  >   |   |   |
+  >   C   H   G
+  > EOS
+  $ for i in B C D F G I Z; do
+  >     hg bookmark -i -r $i b-$i
+  > done
+  $ cp -R . ../scmutilcleanup.obsstore
+
+  $ cat > $TESTTMP/scmutilcleanup.py <<EOF
+  > from mercurial import scmutil
+  > def reposetup(ui, repo):
+  >     def nodes(expr):
+  >         return [repo.changelog.node(r) for r in repo.revs(expr)]
+  >     def node(expr):
+  >         return nodes(expr)[0]
+  >     with repo.wlock():
+  >         with repo.lock():
+  >             with repo.transaction('delayedstrip'):
+  >                 mapping = {node('F'): [node('F2')],
+  >                            node('D'): [node('D2')],
+  >                            node('G'): [node('G2')]}
+  >                 scmutil.cleanupnodes(repo, mapping, 'replace')
+  >                 scmutil.cleanupnodes(repo, nodes('((B::)+I+Z)-D2'), 'replace')
+  > EOF
+  $ hg log -r . -T '\n' --config extensions.t=$TESTTMP/scmutilcleanup.py
+  warning: orphaned descendants detected, not stripping 112478962961, 1fc8102cda62, 26805aba1e60
+  saved backup bundle to $TESTTMP/scmutilcleanup/.hg/strip-backup/f585351a92f8-73fb7c03-replace.hg (glob)
+  
+  $ hg log -G -T '{rev}:{node|short} {desc} {bookmarks}' -r 'sort(all(), topo)'
+  o  8:1473d4b996d1 G2 b-G
+  |
+  | o  7:d94e89b773b6 F2 b-F
+  | |
+  | o  5:7fe5bac4c918 H
+  |/|
+  | o  3:7fb047a69f22 E
+  | |
+  | | o  6:7c78f703e465 D2 b-D
+  | | |
+  | | o  4:26805aba1e60 C
+  | | |
+  | | o  2:112478962961 B
+  | |/
+  o |  1:1fc8102cda62 G
+   /
+  o  0:426bada5c675 A b-B b-C b-I
+  
+  $ hg bookmark
+     b-B                       0:426bada5c675
+     b-C                       0:426bada5c675
+     b-D                       6:7c78f703e465
+     b-F                       7:d94e89b773b6
+     b-G                       8:1473d4b996d1
+     b-I                       0:426bada5c675
+     b-Z                       -1:000000000000
+
+Test the above using obsstore "by the way". Not directly related to strip, but
+we have reusable code here
+
+  $ cd $TESTTMP/scmutilcleanup.obsstore
+  $ cat >> .hg/hgrc <<EOF
+  > [experimental]
+  > evolution=all
+  > evolution.track-operation=1
+  > EOF
+
+  $ hg log -r . -T '\n' --config extensions.t=$TESTTMP/scmutilcleanup.py
+  
+  $ rm .hg/localtags
+  $ hg log -G -T '{rev}:{node|short} {desc} {bookmarks}' -r 'sort(all(), topo)'
+  o  12:1473d4b996d1 G2 b-G
+  |
+  | o  11:d94e89b773b6 F2 b-F
+  | |
+  | o  8:7fe5bac4c918 H
+  |/|
+  | o  4:7fb047a69f22 E
+  | |
+  | | o  10:7c78f703e465 D2 b-D
+  | | |
+  | | x  6:26805aba1e60 C
+  | | |
+  | | x  3:112478962961 B
+  | |/
+  x |  1:1fc8102cda62 G
+   /
+  o  0:426bada5c675 A b-B b-C b-I
+  
+  $ hg debugobsolete
+  1fc8102cda6204549f031015641606ccf5513ec3 1473d4b996d1d1b121de6b39fab6a04fbf9d873e 0 (Thu Jan 01 00:00:00 1970 +0000) {'operation': 'replace', 'user': 'test'}
+  64a8289d249234b9886244d379f15e6b650b28e3 d94e89b773b67e72642a931159ada8d1a9246998 0 (Thu Jan 01 00:00:00 1970 +0000) {'operation': 'replace', 'user': 'test'}
+  f585351a92f85104bff7c284233c338b10eb1df7 7c78f703e465d73102cc8780667ce269c5208a40 0 (Thu Jan 01 00:00:00 1970 +0000) {'operation': 'replace', 'user': 'test'}
+  48b9aae0607f43ff110d84e6883c151942add5ab 0 {0000000000000000000000000000000000000000} (Thu Jan 01 00:00:00 1970 +0000) {'operation': 'replace', 'user': 'test'}
+  112478962961147124edd43549aedd1a335e44bf 0 {426bada5c67598ca65036d57d9e4b64b0c1ce7a0} (Thu Jan 01 00:00:00 1970 +0000) {'operation': 'replace', 'user': 'test'}
+  08ebfeb61bac6e3f12079de774d285a0d6689eba 0 {426bada5c67598ca65036d57d9e4b64b0c1ce7a0} (Thu Jan 01 00:00:00 1970 +0000) {'operation': 'replace', 'user': 'test'}
+  26805aba1e600a82e93661149f2313866a221a7b 0 {112478962961147124edd43549aedd1a335e44bf} (Thu Jan 01 00:00:00 1970 +0000) {'operation': 'replace', 'user': 'test'}