censor: accept multiple revision in a single call
authorPierre-Yves David <pierre-yves.david@octobus.net>
Fri, 01 Dec 2023 22:56:08 +0100
changeset 51270 ceeb8fa23cc8
parent 51269 db121ddd171e
child 51271 eab5b061cd48
censor: accept multiple revision in a single call This is useful when dealing with corruption, as all the corrupted revision can be dealt with in one go.
hgext/censor.py
hgext/sqlitestore.py
mercurial/revlog.py
mercurial/revlogutils/rewrite.py
mercurial/testing/storage.py
tests/test-censor.t
tests/test-censor2.t
--- a/hgext/censor.py	Fri Dec 01 22:46:46 2023 +0100
+++ b/hgext/censor.py	Fri Dec 01 22:56:08 2023 +0100
@@ -36,7 +36,6 @@
 
 from mercurial import (
     error,
-    logcmdutil,
     registrar,
     scmutil,
 )
@@ -56,7 +55,7 @@
         (
             b'r',
             b'rev',
-            b'',
+            [],
             _(b'censor file from specified revision'),
             _(b'REV'),
         ),
@@ -71,7 +70,7 @@
     _(b'-r REV [-t TEXT] [FILE]'),
     helpcategory=command.CATEGORY_MAINTENANCE,
 )
-def censor(ui, repo, path, rev=b'', tombstone=b'', check_heads=True, **opts):
+def censor(ui, repo, path, rev=(), tombstone=b'', check_heads=True, **opts):
     with repo.wlock(), repo.lock():
         return _docensor(
             ui,
@@ -84,11 +83,11 @@
         )
 
 
-def _docensor(ui, repo, path, rev=b'', tombstone=b'', check_heads=True, **opts):
+def _docensor(ui, repo, path, revs=(), tombstone=b'', check_heads=True, **opts):
     if not path:
         raise error.Abort(_(b'must specify file path to censor'))
-    if not rev:
-        raise error.Abort(_(b'must specify revision to censor'))
+    if not revs:
+        raise error.Abort(_(b'must specify revisions to censor'))
 
     wctx = repo[None]
 
@@ -100,18 +99,17 @@
     if not len(flog):
         raise error.Abort(_(b'cannot censor file with no history'))
 
-    rev = logcmdutil.revsingle(repo, rev, rev).rev()
-    try:
-        ctx = repo[rev]
-    except KeyError:
-        raise error.Abort(_(b'invalid revision identifier %s') % rev)
+    revs = scmutil.revrange(repo, revs)
+    if not revs:
+        raise error.Abort(_(b'no matching revisions'))
+    file_nodes = set()
+    for r in revs:
+        try:
+            ctx = repo[r]
+            file_nodes.add(ctx.filectx(path).filenode())
+        except error.LookupError:
+            raise error.Abort(_(b'file does not exist at revision %s') % ctx)
 
-    try:
-        fctx = ctx.filectx(path)
-    except error.LookupError:
-        raise error.Abort(_(b'file does not exist at revision %s') % rev)
-
-    fnode = fctx.filenode()
     if check_heads:
         heads = []
         repo_heads = repo.heads()
@@ -120,7 +118,7 @@
         ui.status(msg)
         for headnode in repo_heads:
             hc = repo[headnode]
-            if path in hc and hc.filenode(path) == fnode:
+            if path in hc and hc.filenode(path) in file_nodes:
                 heads.append(hc)
         if heads:
             headlist = b', '.join([short(c.node()) for c in heads])
@@ -138,7 +136,8 @@
             hint=_(b'clean/delete/update first'),
         )
 
-    msg = b'censoring 1 file revision\n'
+    msg = b'censoring %d file revisions\n'
+    msg %= len(file_nodes)
     ui.status(msg)
     with repo.transaction(b'censor') as tr:
-        flog.censorrevision(tr, fnode, tombstone=tombstone)
+        flog.censorrevision(tr, file_nodes, tombstone=tombstone)
--- a/hgext/sqlitestore.py	Fri Dec 01 22:46:46 2023 +0100
+++ b/hgext/sqlitestore.py	Fri Dec 01 22:56:08 2023 +0100
@@ -810,7 +810,11 @@
 
         return not empty
 
-    def censorrevision(self, tr, censornode, tombstone=b''):
+    def censorrevision(self, tr, censor_nodes, tombstone=b''):
+        for node in censor_nodes:
+            self._censor_one_revision(tr, node, tombstone=tombstone)
+
+    def _censor_one_revision(self, tr, censornode, tombstone):
         tombstone = storageutil.packmeta({b'censored': tombstone}, b'')
 
         # This restriction is cargo culted from revlogs and makes no sense for
--- a/mercurial/revlog.py	Fri Dec 01 22:46:46 2023 +0100
+++ b/mercurial/revlog.py	Fri Dec 01 22:56:08 2023 +0100
@@ -3835,16 +3835,16 @@
             if addrevisioncb:
                 addrevisioncb(self, rev, node)
 
-    def censorrevision(self, tr, censornode, tombstone=b''):
+    def censorrevision(self, tr, censor_nodes, tombstone=b''):
         if self._format_version == REVLOGV0:
             raise error.RevlogError(
                 _(b'cannot censor with version %d revlogs')
                 % self._format_version
             )
         elif self._format_version == REVLOGV1:
-            rewrite.v1_censor(self, tr, censornode, tombstone)
+            rewrite.v1_censor(self, tr, censor_nodes, tombstone)
         else:
-            rewrite.v2_censor(self, tr, censornode, tombstone)
+            rewrite.v2_censor(self, tr, censor_nodes, tombstone)
 
     def verifyintegrity(self, state):
         """Verifies the integrity of the revlog.
--- a/mercurial/revlogutils/rewrite.py	Fri Dec 01 22:46:46 2023 +0100
+++ b/mercurial/revlogutils/rewrite.py	Fri Dec 01 22:56:08 2023 +0100
@@ -51,14 +51,14 @@
 )
 
 
-def v1_censor(rl, tr, censornode, tombstone=b''):
+def v1_censor(rl, tr, censor_nodes, tombstone=b''):
     """censors a revision in a "version 1" revlog"""
     assert rl._format_version == constants.REVLOGV1, rl._format_version
 
     # avoid cycle
     from .. import revlog
 
-    censorrev = rl.rev(censornode)
+    censor_revs = set(rl.rev(node) for node in censor_nodes)
     tombstone = storageutil.packmeta({b'censored': tombstone}, b'')
 
     # Rewriting the revlog in place is hard. Our strategy for censoring is
@@ -87,14 +87,14 @@
         node = rl.node(rev)
         p1, p2 = rl.parents(node)
 
-        if rev == censorrev:
+        if rev in censor_revs:
             newrl.addrawrevision(
                 tombstone,
                 tr,
-                rl.linkrev(censorrev),
+                rl.linkrev(rev),
                 p1,
                 p2,
-                censornode,
+                node,
                 constants.REVIDX_ISCENSORED,
             )
 
@@ -138,12 +138,12 @@
     rl._load_inner(chunk_cache)
 
 
-def v2_censor(revlog, tr, censornode, tombstone=b''):
+def v2_censor(revlog, tr, censor_nodes, tombstone=b''):
     """censors a revision in a "version 2" revlog"""
     assert revlog._format_version != REVLOGV0, revlog._format_version
     assert revlog._format_version != REVLOGV1, revlog._format_version
 
-    censor_revs = {revlog.rev(censornode)}
+    censor_revs = {revlog.rev(node) for node in censor_nodes}
     _rewrite_v2(revlog, tr, censor_revs, tombstone)
 
 
--- a/mercurial/testing/storage.py	Fri Dec 01 22:46:46 2023 +0100
+++ b/mercurial/testing/storage.py	Fri Dec 01 22:56:08 2023 +0100
@@ -1280,7 +1280,7 @@
             node2 = f.add(b'foo\n' * 32, None, tr, 2, node1, f.nullid)
 
         with self._maketransactionfn() as tr:
-            f.censorrevision(tr, node1)
+            f.censorrevision(tr, [node1])
 
         self.assertEqual(len(f), 3)
         self.assertEqual(list(f.revs()), [0, 1, 2])
--- a/tests/test-censor.t	Fri Dec 01 22:46:46 2023 +0100
+++ b/tests/test-censor.t	Fri Dec 01 22:56:08 2023 +0100
@@ -80,7 +80,7 @@
   $ hg --config extensions.censor= --cwd foo/bar/baz censor -r $C2 -t "remove password" ../../../target
   checking for the censored content in 2 heads
   checking for the censored content in the working directory
-  censoring 1 file revision
+  censoring 1 file revisions
   $ hg cat -r $H1 target | head -n 10
   Tainted file is now sanitized
   $ hg cat -r $H2 target | head -n 10
@@ -101,7 +101,7 @@
   $ hg --config extensions.censor= --cwd foo/bar/baz censor -r $C1 path:target
   checking for the censored content in 2 heads
   checking for the censored content in the working directory
-  censoring 1 file revision
+  censoring 1 file revisions
   $ hg cat -r $H1 target | head -n 10
   Tainted file is now sanitized
   $ hg cat -r $H2 target | head -n 10
@@ -243,7 +243,7 @@
   $ hg --config extensions.censor= censor -r $C3 target
   checking for the censored content in 2 heads
   checking for the censored content in the working directory
-  censoring 1 file revision
+  censoring 1 file revisions
   $ hg update -r $H2
   1 files updated, 0 files merged, 0 files removed, 0 files unresolved
   $ hg merge -r $C3
@@ -294,7 +294,7 @@
   $ hg --config extensions.censor= censor -r $C4 target
   checking for the censored content in 2 heads
   checking for the censored content in the working directory
-  censoring 1 file revision
+  censoring 1 file revisions
   $ hg cat -r $C4 target | head -n 10
   $ hg cat -r "$H2^^" target | head -n 10
   Tainted file now super sanitized
@@ -329,7 +329,7 @@
   $ hg --config extensions.censor= censor -r $C5 target
   checking for the censored content in 2 heads
   checking for the censored content in the working directory
-  censoring 1 file revision
+  censoring 1 file revisions
 
 The important part is for the censor operation to not crash and the repository
 to not be corrupted.  Right now this involve keeping the revlog split.
@@ -410,7 +410,7 @@
   $ hg --config extensions.censor= censor -r $REV target
   checking for the censored content in 3 heads
   checking for the censored content in the working directory
-  censoring 1 file revision
+  censoring 1 file revisions
   $ hg cat -r $REV target | head -n 10
   $ hg cat -r $CLEANREV target | head -n 10
   Re-sanitized; nothing to see here
@@ -513,7 +513,7 @@
   $ hg --config extensions.censor= censor -r 0 target
   checking for the censored content in 3 heads
   checking for the censored content in the working directory
-  censoring 1 file revision
+  censoring 1 file revisions
   $ hg bundle -r 0 --base null ../rinit/initbundle
   1 changesets found
   $ cd ../rinit
@@ -530,7 +530,17 @@
 
   $ hg --config extensions.censor= censor -r 0  --no-check-heads target
   checking for the censored content in the working directory
-  censoring 1 file revision
+  censoring 1 file revisions
+
+Can censor multiple revision in one go.
+
+  $ cd ../r
+  $ hg --config extensions.censor= censor -r 0+1  target
+  checking for the censored content in 3 heads
+  checking for the censored content in the working directory
+  censoring 2 file revisions
+
+
 
 #if revlogv2
 
@@ -572,7 +582,7 @@
   $ hg --config extensions.censor= censor -r $B1 target
   checking for the censored content in 1 heads
   checking for the censored content in the working directory
-  censoring 1 file revision
+  censoring 1 file revisions
   $ hg cat -r $B1 target | wc -l
    *0 (re)
 
--- a/tests/test-censor2.t	Fri Dec 01 22:46:46 2023 +0100
+++ b/tests/test-censor2.t	Fri Dec 01 22:56:08 2023 +0100
@@ -17,7 +17,7 @@
   $ hg censor target --config extensions.censor= -r ".^^"
   checking for the censored content in 1 heads
   checking for the censored content in the working directory
-  censoring 1 file revision
+  censoring 1 file revisions
   $ hg update ".^"
   1 files updated, 0 files merged, 0 files removed, 0 files unresolved
   $ cat target