[PATCH 2 of 2] scmutil: add a cleanupnodes method for developers

Jun Wu quark at fb.com
Sun Jun 25 16:32:49 EDT 2017


# HG changeset patch
# User Jun Wu <quark at fb.com>
# Date 1498422716 25200
#      Sun Jun 25 13:31:56 2017 -0700
# Node ID c96acc70ab665ea76c4154b793c4071c4c57da1e
# Parent  b70ebbb5b3ffb417d8fe13548b39c9196fadda1f
# Available At https://bitbucket.org/quark-zju/hg-draft
#              hg pull https://bitbucket.org/quark-zju/hg-draft -r c96acc70ab66
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.

diff --git a/mercurial/scmutil.py b/mercurial/scmutil.py
--- a/mercurial/scmutil.py
+++ b/mercurial/scmutil.py
@@ -17,4 +17,6 @@ import socket
 from .i18n import _
 from .node import (
+    hex,
+    nullid,
     wdirid,
     wdirrev,
@@ -25,4 +27,5 @@ from . import (
     error,
     match as matchmod,
+    obsolete,
     pathutil,
     phases,
@@ -562,4 +565,66 @@ def origpath(ui, repo, filepath):
     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:
diff --git a/tests/test-strip.t b/tests/test-strip.t
--- a/tests/test-strip.t
+++ b/tests/test-strip.t
@@ -956,4 +956,5 @@ Use delayedstrip to strip inside a trans
   >   A   Z
   > EOS
+  $ cp -R . ../scmutilcleanup
 
   $ hg up -C I
@@ -991,2 +992,103 @@ Use delayedstrip to strip inside a trans
   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'}


More information about the Mercurial-devel mailing list