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.
--- 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
+