censor.py
143 lines
| 4.7 KiB
| text/x-python
|
PythonLexer
/ hgext / censor.py
Mike Edgar
|
r24347 | # Copyright (C) 2015 - Mike Edgar <adgar@google.com> | ||
# | ||||
# This extension enables removal of file content at a given revision, | ||||
# rewriting the data/metadata of successive revisions to preserve revision log | ||||
# integrity. | ||||
"""erase file content at a given revision | ||||
The censor command instructs Mercurial to erase all content of a file at a given | ||||
revision *without updating the changeset hash.* This allows existing history to | ||||
remain valid while preventing future clones/pulls from receiving the erased | ||||
data. | ||||
Typical uses for censor are due to security or legal requirements, including:: | ||||
Mads Kiilerich
|
r26781 | * Passwords, private keys, cryptographic material | ||
Mike Edgar
|
r24347 | * Licensed data/code/libraries for which the license has expired | ||
* Personally Identifiable Information or other private data | ||||
Censored nodes can interrupt mercurial's typical operation whenever the excised | ||||
data needs to be materialized. Some commands, like ``hg cat``/``hg revert``, | ||||
simply fail when asked to produce censored data. Others, like ``hg verify`` and | ||||
``hg update``, must be capable of tolerating censored data to continue to | ||||
function in a meaningful way. Such commands only tolerate censored file | ||||
r52159 | As having a censored version in a checkout is impractical. The current head | |||
revisions of the repository are checked. If the revision to be censored is in | ||||
any of them the command will abort. | ||||
Jordi Gutiérrez Hermoso
|
r43623 | |||
A few informative commands such as ``hg grep`` will unconditionally | ||||
ignore censored data and merely report that it was encountered. | ||||
Mike Edgar
|
r24347 | """ | ||
Gregory Szorc
|
r28092 | |||
from mercurial.i18n import _ | ||||
Mike Edgar
|
r24347 | from mercurial.node import short | ||
Gregory Szorc
|
r28092 | |||
from mercurial import ( | ||||
error, | ||||
Yuya Nishihara
|
r32337 | registrar, | ||
Gregory Szorc
|
r28092 | scmutil, | ||
) | ||||
Mike Edgar
|
r24347 | |||
cmdtable = {} | ||||
Yuya Nishihara
|
r32337 | command = registrar.command(cmdtable) | ||
Augie Fackler
|
r29841 | # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for | ||
Augie Fackler
|
r25186 | # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should | ||
# be specifying the version(s) of Mercurial they are tested with, or | ||||
# leave the attribute unspecified. | ||||
Augie Fackler
|
r43347 | testedwith = b'ships-with-hg-core' | ||
Mike Edgar
|
r24347 | |||
Augie Fackler
|
r43346 | |||
@command( | ||||
Augie Fackler
|
r43347 | b'censor', | ||
Augie Fackler
|
r43346 | [ | ||
Augie Fackler
|
r43347 | ( | ||
b'r', | ||||
b'rev', | ||||
r52163 | [], | |||
Augie Fackler
|
r43347 | _(b'censor file from specified revision'), | ||
_(b'REV'), | ||||
), | ||||
r52161 | ( | |||
b'', | ||||
b'check-heads', | ||||
True, | ||||
_(b'check that repository heads are not affected'), | ||||
), | ||||
Augie Fackler
|
r43347 | (b't', b'tombstone', b'', _(b'replacement tombstone data'), _(b'TEXT')), | ||
Augie Fackler
|
r43346 | ], | ||
Augie Fackler
|
r43347 | _(b'-r REV [-t TEXT] [FILE]'), | ||
Augie Fackler
|
r43346 | helpcategory=command.CATEGORY_MAINTENANCE, | ||
) | ||||
r52163 | def censor(ui, repo, path, rev=(), tombstone=b'', check_heads=True, **opts): | |||
Matt Harbison
|
r38460 | with repo.wlock(), repo.lock(): | ||
r52161 | return _docensor( | |||
ui, | ||||
repo, | ||||
path, | ||||
rev, | ||||
tombstone, | ||||
check_heads=check_heads, | ||||
**opts, | ||||
) | ||||
FUJIWARA Katsunori
|
r27290 | |||
Augie Fackler
|
r43346 | |||
r52163 | def _docensor(ui, repo, path, revs=(), tombstone=b'', check_heads=True, **opts): | |||
Mike Edgar
|
r24347 | if not path: | ||
Augie Fackler
|
r43347 | raise error.Abort(_(b'must specify file path to censor')) | ||
r52163 | if not revs: | |||
raise error.Abort(_(b'must specify revisions to censor')) | ||||
Mike Edgar
|
r24347 | |||
FUJIWARA Katsunori
|
r25806 | wctx = repo[None] | ||
m = scmutil.match(wctx, (path,)) | ||||
if m.anypats() or len(m.files()) != 1: | ||||
Augie Fackler
|
r43347 | raise error.Abort(_(b'can only specify an explicit filename')) | ||
FUJIWARA Katsunori
|
r25806 | path = m.files()[0] | ||
Mike Edgar
|
r24347 | flog = repo.file(path) | ||
if not len(flog): | ||||
Augie Fackler
|
r43347 | raise error.Abort(_(b'cannot censor file with no history')) | ||
Mike Edgar
|
r24347 | |||
r52163 | 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) | ||||
Mike Edgar
|
r24347 | |||
r52161 | if check_heads: | |||
heads = [] | ||||
repo_heads = repo.heads() | ||||
msg = b'checking for the censored content in %d heads\n' | ||||
msg %= len(repo_heads) | ||||
ui.status(msg) | ||||
for headnode in repo_heads: | ||||
hc = repo[headnode] | ||||
r52163 | if path in hc and hc.filenode(path) in file_nodes: | |||
r52161 | heads.append(hc) | |||
if heads: | ||||
headlist = b', '.join([short(c.node()) for c in heads]) | ||||
raise error.Abort( | ||||
_(b'cannot censor file in heads (%s)') % headlist, | ||||
hint=_(b'clean/delete and commit first'), | ||||
) | ||||
Mike Edgar
|
r24347 | |||
r52162 | msg = b'checking for the censored content in the working directory\n' | |||
ui.status(msg) | ||||
Mike Edgar
|
r24347 | wp = wctx.parents() | ||
if ctx.node() in [p.node() for p in wp]: | ||||
Augie Fackler
|
r43346 | raise error.Abort( | ||
Augie Fackler
|
r43347 | _(b'cannot censor working directory'), | ||
hint=_(b'clean/delete/update first'), | ||||
Augie Fackler
|
r43346 | ) | ||
Mike Edgar
|
r24347 | |||
r52163 | msg = b'censoring %d file revisions\n' | |||
msg %= len(file_nodes) | ||||
r52162 | ui.status(msg) | |||
Gregory Szorc
|
r39814 | with repo.transaction(b'censor') as tr: | ||
r52163 | flog.censorrevision(tr, file_nodes, tombstone=tombstone) | |||