comparison hgext/censor.py @ 51270:ceeb8fa23cc8

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.
author Pierre-Yves David <pierre-yves.david@octobus.net>
date Fri, 01 Dec 2023 22:56:08 +0100
parents db121ddd171e
children
comparison
equal deleted inserted replaced
51269:db121ddd171e 51270:ceeb8fa23cc8
34 from mercurial.i18n import _ 34 from mercurial.i18n import _
35 from mercurial.node import short 35 from mercurial.node import short
36 36
37 from mercurial import ( 37 from mercurial import (
38 error, 38 error,
39 logcmdutil,
40 registrar, 39 registrar,
41 scmutil, 40 scmutil,
42 ) 41 )
43 42
44 cmdtable = {} 43 cmdtable = {}
54 b'censor', 53 b'censor',
55 [ 54 [
56 ( 55 (
57 b'r', 56 b'r',
58 b'rev', 57 b'rev',
59 b'', 58 [],
60 _(b'censor file from specified revision'), 59 _(b'censor file from specified revision'),
61 _(b'REV'), 60 _(b'REV'),
62 ), 61 ),
63 ( 62 (
64 b'', 63 b'',
69 (b't', b'tombstone', b'', _(b'replacement tombstone data'), _(b'TEXT')), 68 (b't', b'tombstone', b'', _(b'replacement tombstone data'), _(b'TEXT')),
70 ], 69 ],
71 _(b'-r REV [-t TEXT] [FILE]'), 70 _(b'-r REV [-t TEXT] [FILE]'),
72 helpcategory=command.CATEGORY_MAINTENANCE, 71 helpcategory=command.CATEGORY_MAINTENANCE,
73 ) 72 )
74 def censor(ui, repo, path, rev=b'', tombstone=b'', check_heads=True, **opts): 73 def censor(ui, repo, path, rev=(), tombstone=b'', check_heads=True, **opts):
75 with repo.wlock(), repo.lock(): 74 with repo.wlock(), repo.lock():
76 return _docensor( 75 return _docensor(
77 ui, 76 ui,
78 repo, 77 repo,
79 path, 78 path,
82 check_heads=check_heads, 81 check_heads=check_heads,
83 **opts, 82 **opts,
84 ) 83 )
85 84
86 85
87 def _docensor(ui, repo, path, rev=b'', tombstone=b'', check_heads=True, **opts): 86 def _docensor(ui, repo, path, revs=(), tombstone=b'', check_heads=True, **opts):
88 if not path: 87 if not path:
89 raise error.Abort(_(b'must specify file path to censor')) 88 raise error.Abort(_(b'must specify file path to censor'))
90 if not rev: 89 if not revs:
91 raise error.Abort(_(b'must specify revision to censor')) 90 raise error.Abort(_(b'must specify revisions to censor'))
92 91
93 wctx = repo[None] 92 wctx = repo[None]
94 93
95 m = scmutil.match(wctx, (path,)) 94 m = scmutil.match(wctx, (path,))
96 if m.anypats() or len(m.files()) != 1: 95 if m.anypats() or len(m.files()) != 1:
98 path = m.files()[0] 97 path = m.files()[0]
99 flog = repo.file(path) 98 flog = repo.file(path)
100 if not len(flog): 99 if not len(flog):
101 raise error.Abort(_(b'cannot censor file with no history')) 100 raise error.Abort(_(b'cannot censor file with no history'))
102 101
103 rev = logcmdutil.revsingle(repo, rev, rev).rev() 102 revs = scmutil.revrange(repo, revs)
104 try: 103 if not revs:
105 ctx = repo[rev] 104 raise error.Abort(_(b'no matching revisions'))
106 except KeyError: 105 file_nodes = set()
107 raise error.Abort(_(b'invalid revision identifier %s') % rev) 106 for r in revs:
107 try:
108 ctx = repo[r]
109 file_nodes.add(ctx.filectx(path).filenode())
110 except error.LookupError:
111 raise error.Abort(_(b'file does not exist at revision %s') % ctx)
108 112
109 try:
110 fctx = ctx.filectx(path)
111 except error.LookupError:
112 raise error.Abort(_(b'file does not exist at revision %s') % rev)
113
114 fnode = fctx.filenode()
115 if check_heads: 113 if check_heads:
116 heads = [] 114 heads = []
117 repo_heads = repo.heads() 115 repo_heads = repo.heads()
118 msg = b'checking for the censored content in %d heads\n' 116 msg = b'checking for the censored content in %d heads\n'
119 msg %= len(repo_heads) 117 msg %= len(repo_heads)
120 ui.status(msg) 118 ui.status(msg)
121 for headnode in repo_heads: 119 for headnode in repo_heads:
122 hc = repo[headnode] 120 hc = repo[headnode]
123 if path in hc and hc.filenode(path) == fnode: 121 if path in hc and hc.filenode(path) in file_nodes:
124 heads.append(hc) 122 heads.append(hc)
125 if heads: 123 if heads:
126 headlist = b', '.join([short(c.node()) for c in heads]) 124 headlist = b', '.join([short(c.node()) for c in heads])
127 raise error.Abort( 125 raise error.Abort(
128 _(b'cannot censor file in heads (%s)') % headlist, 126 _(b'cannot censor file in heads (%s)') % headlist,
136 raise error.Abort( 134 raise error.Abort(
137 _(b'cannot censor working directory'), 135 _(b'cannot censor working directory'),
138 hint=_(b'clean/delete/update first'), 136 hint=_(b'clean/delete/update first'),
139 ) 137 )
140 138
141 msg = b'censoring 1 file revision\n' 139 msg = b'censoring %d file revisions\n'
140 msg %= len(file_nodes)
142 ui.status(msg) 141 ui.status(msg)
143 with repo.transaction(b'censor') as tr: 142 with repo.transaction(b'censor') as tr:
144 flog.censorrevision(tr, fnode, tombstone=tombstone) 143 flog.censorrevision(tr, file_nodes, tombstone=tombstone)