strip: add a delayedstrip method that works in a transaction
authorJun Wu <quark@fb.com>
Sun, 25 Jun 2017 10:38:45 -0700
changeset 33087 fcd1c483f5ea
parent 33086 eb4c49f55f1f
child 33088 65cadeea6c22
strip: add a delayedstrip method that works in a transaction For long, the fact that strip does not work inside a transaction and some code has to work with both obsstore and fallback to strip lead to duplicated code like: with repo.transaction(): .... if obsstore: obsstore.createmarkers(...) if not obsstore: repair.strip(...) Things get more complex when you want to call something which may call strip under the hood. Like you cannot simply write: with repo.transaction(): .... rebasemod.rebase(...) # may call "strip", so this doesn't work But you do want rebase to run inside a same transaction if possible, so the code may look like: with repo.transaction(): .... if obsstore: rebasemod.rebase(...) obsstore.createmarkers(...) if not obsstore: rebasemod.rebase(...) repair.strip(...) That's ugly and error-prone. Ideally it's possible to just write: with repo.transaction(): rebasemod.rebase(...) saferemovenodes(...) This patch is the first step towards that. It adds a "delayedstrip" method to repair.py which maintains a postclose callback in the transaction object.
mercurial/repair.py
mercurial/transaction.py
tests/test-strip.t
--- a/mercurial/repair.py	Sun Jun 25 22:30:14 2017 -0700
+++ b/mercurial/repair.py	Sun Jun 25 10:38:45 2017 -0700
@@ -253,6 +253,63 @@
     # extensions can use it
     return backupfile
 
+def safestriproots(ui, repo, nodes):
+    """return list of roots of nodes where descendants are covered by nodes"""
+    torev = repo.unfiltered().changelog.rev
+    revs = set(torev(n) for n in nodes)
+    # tostrip = wanted - unsafe = wanted - ancestors(orphaned)
+    # orphaned = affected - wanted
+    # affected = descendants(roots(wanted))
+    # wanted = revs
+    tostrip = set(repo.revs('%ld-(::((roots(%ld)::)-%ld))', revs, revs, revs))
+    notstrip = revs - tostrip
+    if notstrip:
+        nodestr = ', '.join(sorted(short(repo[n].node()) for n in notstrip))
+        ui.warn(_('warning: orphaned descendants detected, '
+                  'not stripping %s\n') % nodestr)
+    return [c.node() for c in repo.set('roots(%ld)', tostrip)]
+
+class stripcallback(object):
+    """used as a transaction postclose callback"""
+
+    def __init__(self, ui, repo, backup, topic):
+        self.ui = ui
+        self.repo = repo
+        self.backup = backup
+        self.topic = topic or 'backup'
+        self.nodelist = []
+
+    def addnodes(self, nodes):
+        self.nodelist.extend(nodes)
+
+    def __call__(self, tr):
+        roots = safestriproots(self.ui, self.repo, self.nodelist)
+        if roots:
+            strip(self.ui, self.repo, roots, True, self.topic)
+
+def delayedstrip(ui, repo, nodelist, topic=None):
+    """like strip, but works inside transaction and won't strip irreverent revs
+
+    nodelist must explicitly contain all descendants. Otherwise a warning will
+    be printed that some nodes are not stripped.
+
+    Always do a backup. The last non-None "topic" will be used as the backup
+    topic name. The default backup topic name is "backup".
+    """
+    tr = repo.currenttransaction()
+    if not tr:
+        nodes = safestriproots(ui, repo, nodelist)
+        return strip(ui, repo, nodes, True, topic)
+    # transaction postclose callbacks are called in alphabet order.
+    # use '\xff' as prefix so we are likely to be called last.
+    callback = tr.getpostclose('\xffstrip')
+    if callback is None:
+        callback = stripcallback(ui, repo, True, topic)
+        tr.addpostclose('\xffstrip', callback)
+    if topic:
+        callback.topic = topic
+    callback.addnodes(nodelist)
+
 def striptrees(repo, tr, striprev, files):
     if 'treemanifest' in repo.requirements: # safe but unnecessary
                                             # otherwise
--- a/mercurial/transaction.py	Sun Jun 25 22:30:14 2017 -0700
+++ b/mercurial/transaction.py	Sun Jun 25 10:38:45 2017 -0700
@@ -412,7 +412,7 @@
 
     @active
     def addpostclose(self, category, callback):
-        """add a callback to be called after the transaction is closed
+        """add or replace a callback to be called after the transaction closed
 
         The transaction will be given as callback's first argument.
 
@@ -422,6 +422,11 @@
         self._postclosecallback[category] = callback
 
     @active
+    def getpostclose(self, category):
+        """return a postclose callback added before, or None"""
+        return self._postclosecallback.get(category, None)
+
+    @active
     def addabort(self, category, callback):
         """add a callback to be called when the transaction is aborted.
 
--- a/tests/test-strip.t	Sun Jun 25 22:30:14 2017 -0700
+++ b/tests/test-strip.t	Sun Jun 25 10:38:45 2017 -0700
@@ -2,6 +2,7 @@
   $ echo "usegeneraldelta=yes" >> $HGRCPATH
   $ echo "[extensions]" >> $HGRCPATH
   $ echo "strip=" >> $HGRCPATH
+  $ echo "drawdag=$TESTDIR/drawdag.py" >> $HGRCPATH
 
   $ restore() {
   >     hg unbundle -q .hg/strip-backup/*
@@ -940,4 +941,52 @@
   abort: boom
   [255]
 
+Use delayedstrip to strip inside a transaction
 
+  $ cd $TESTTMP
+  $ hg init delayedstrip
+  $ cd delayedstrip
+  $ hg debugdrawdag <<'EOS'
+  >   D
+  >   |
+  >   C F H    # Commit on top of "I",
+  >   | |/|    # Strip B+D+I+E+G+H+Z
+  > I B E G
+  >  \|/
+  >   A   Z
+  > EOS
+
+  $ hg up -C I
+  2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+  $ echo 3 >> I
+  $ cat > $TESTTMP/delayedstrip.py <<EOF
+  > from mercurial import repair, commands
+  > def reposetup(ui, repo):
+  >     def getnodes(expr):
+  >         return [repo.changelog.node(r) for r in repo.revs(expr)]
+  >     with repo.wlock():
+  >         with repo.lock():
+  >             with repo.transaction('delayedstrip'):
+  >                 repair.delayedstrip(ui, repo, getnodes('B+I+Z+D+E'), 'J')
+  >                 repair.delayedstrip(ui, repo, getnodes('G+H+Z'), 'I')
+  >                 commands.commit(ui, repo, message='J', date='0 0')
+  > EOF
+  $ hg log -r . -T '\n' --config extensions.t=$TESTTMP/delayedstrip.py
+  warning: orphaned descendants detected, not stripping 08ebfeb61bac, 112478962961, 7fb047a69f22
+  saved backup bundle to $TESTTMP/delayedstrip/.hg/strip-backup/f585351a92f8-81fa23b0-I.hg (glob)
+  
+  $ hg log -G -T '{rev}:{node|short} {desc}' -r 'sort(all(), topo)'
+  @  6:2f2d51af6205 J
+  |
+  o  3:08ebfeb61bac I
+  |
+  | o  5:64a8289d2492 F
+  | |
+  | o  2:7fb047a69f22 E
+  |/
+  | o  4:26805aba1e60 C
+  | |
+  | o  1:112478962961 B
+  |/
+  o  0:426bada5c675 A
+