@@ -0,0 +1,182 @@
+# Copyright (C) 2014 - 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::
+
+ * Passwords, private keys, crytographic material
+ * Licensed data/code/libraries for which the license has expired
+ * Personally Identifiable Information or other private data
+
+Censored file revisions are listed in a tracked file called .hgcensored stored
+in the repository root. The censor command adds an entry to the .hgcensored file
+in the working directory and commits it (much like ``hg tag`` and .hgtags). The
+censored file data is then replaced with a pointer to the new commit, enabling
+verification.
+
+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
+revisions if they are allowed by the policy specified by the "censor.allow"
+config option.
+"""
+
+from mercurial.node import hex, short, nullid, nullrev
+from mercurial import censor as censormod
+from mercurial import commands, cmdutil, error, filelog, revlog, scmutil, util
+from mercurial.i18n import _
+
+cmdtable = {}
+command = cmdutil.command(cmdtable)
+testedwith = 'internal'
+
+@command('censor',
+ [('r', 'rev', '', _('censor file from specified revision'), _('REV')),
+ ('m', 'message', '', _('use text as commit message'), _('TEXT')),
+ ] + commands.commitopts2,
+ _('[-m TEXT] [-d DATE] [-u USER] [-r REV] [FILE]'))
+def censor(ui, repo, path, rev='', message='', **opts):
+ if not path:
+ raise util.Abort(_('must specify file path to censor'))
+ if not rev:
+ raise util.Abort(_('must specify revision to censor'))
+ if not message:
+ raise util.Abort(_('must specify a message to justify censorship'))
+
+ flog = repo.file(path)
+ if not len(flog):
+ raise util.Abort(_('cannot censor file with no history'))
+
+ rev = scmutil.revsingle(repo, rev, rev).rev()
+ try:
+ ctx = repo[rev]
+ except KeyError:
+ raise util.Abort(_('invalid revision identifier %s') % rev)
+
+ try:
+ fctx = ctx.filectx(path)
+ except error.LookupError:
+ raise util.Abort(_('file does not exist at revision %s') % rev)
+
+ fnode = fctx.filenode()
+ headctxs = [repo[c] for c in repo.heads()]
+ heads = [c for c in headctxs if path in c and c.filenode(path) == fnode]
+ if heads:
+ headlist = ', '.join([short(c.node()) for c in heads])
+ raise util.Abort(_('cannot censor file in heads (%s)') % headlist,
+ hint=_('clean/delete and commit first'))
+
+ wctx = repo[None]
+ wp = wctx.parents()
+ if ctx.node() in [p.node() for p in wp]:
+ raise util.Abort(_('cannot censor working directory'),
+ hint=_('clean/delete/update first'))
+
+ flogv = flog.version & 0xFFFF
+ if flogv != revlog.REVLOGNG:
+ raise util.Abort(
+ _('censor does not support revlog version %d') % (flogv,))
+
+ date = opts.get('date')
+ if date:
+ date = util.parsedate(date)
+ tombstone = censormod.record(repo, path, fctx.filenode(), message,
+ opts.get('user'), date)
+
+ if flog.version & revlog.REVLOGNGINLINEDATA:
+ _censor_inline(repo, flog, fctx.filerev(), tombstone)
+ else:
+ raise util.Abort(_('censor currently only supports inline revlogs'))
+
+def _censor_inline(repo, flog, crev, tombstone):
+ # Using two files instead of one makes it easy to rewrite entry-by-entry
+ ifp = flog.opener(flog.indexfile, 'r')
+ ofp = flog.opener(flog.indexfile, 'wb', atomictemp=True)
+
+ rio = revlog.revlogio()
+
+ def reindex(r, noffset, ncomp=None, nuncomp=None):
+ """Recomputes the index entry for the given revision at a new offset.
+
+ If ncomp and nuncomp are both provided, the new index entry will be
+ for a full-text revision with the given compressed/uncompressed sizes.
+
+ Args:
+ r: int, the filelog revision number to reindex
+ noffset: int, the new data offset
+ ncomp: int, the compressed size of the full text of the revision
+ data which will be found at noffset
+ nuncomp: int, the uncompressed size of the full text of the revision
+ data
+ Returns:
+ A string representing the recomputed index entry in revlogng format.
+ """
+ offlags, comp, uncomp, base, link, p1, p2, nodeid = flog.index[r]
+ offlags = revlog.offset_type(noffset, revlog.gettype(offlags))
+ if ncomp and nuncomp:
+ comp = ncomp
+ uncomp = nuncomp
+ base = r
+ e = (offlags, comp, uncomp, base, link, p1, p2, nodeid)
+ return rio.packentry(e, None, flog.version, r)
+
+ # Copy all revlog data up to the entry to be censored.
+ offset = flog.start(crev)
+ ioffs = offset + crev * rio.size
+
+ for chunk in util.filechunkiter(ifp, limit=ioffs):
+ ofp.write(chunk)
+
+ def writefull(r, offs, data):
+ """Write the given fulltext revision to a revlog.
+
+ Args:
+ r: int, the filelog revision to write as full text
+ offs: int, the data offset at which to write the full revision text
+ data: string, the full text of the revision to write
+ Returns:
+ The integer number of data bytes written, for tracking data offsets.
+ """
+ flag, compdata = flog.compress(data)
+ complen = len(flag) + len(compdata)
+ ofp.write(reindex(r, offs, complen, len(data)))
+ ofp.write(flag)
+ ofp.write(compdata)
+ ifp.seek(rio.size + flog.length(r), 1)
+ return complen
+
+ # Write censored revlog entry.
+ offset += writefull(crev, offset, tombstone)
+
+ # Rewrite all following filelog revisions to fixup offsets and deltas.
+ for srev in xrange(crev + 1, len(flog)):
+ if crev in flog.parentrevs(srev):
+ # Immediate children of censored node must be re-added as fulltext.
+ try:
+ data = flog.revision(srev)
+ except error.CensoredNodeError:
+ data = flog.censormeta(srev)
+ dlen = writefull(srev, offset, data)
+ else:
+ # Copy any other revision data verbatim after fixing up the
+ # index offset.
+ ofp.write(reindex(srev, offset))
+ ifp.seek(rio.size, 1)
+ dlen = flog.length(srev)
+ for chunk in util.filechunkiter(ifp, limit=dlen):
+ ofp.write(chunk)
+ offset += dlen
+
+ ifp.close()
+ ofp.close()
@@ -0,0 +1,522 @@
+ $ cat >> $HGRCPATH <<EOF
+ > [extensions]
+ > censor=
+ > EOF
+ $ cp $HGRCPATH $HGRCPATH.orig
+
+Create repo with unimpeachable content
+
+ $ hg init r
+ $ cd r
+ $ echo 'Initially untainted file' > target
+ $ echo 'Normal file here' > bystander
+ $ hg add target bystander
+ $ hg ci -m init
+
+Clone repo so we can test pull later
+
+ $ cd ..
+ $ hg clone r rpull
+ updating to branch default
+ 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+ $ cd r
+
+Introduce content which will ultimately require censorship
+
+ $ echo 'Tainted file' > target
+ $ echo 'Passwords: hunter2' >> target
+ $ hg ci -m taint target
+
+ $ echo 'hunter3' >> target
+ $ echo 'Normal file v2' > bystander
+ $ hg ci -m moretaint target bystander
+
+Add a new sanitized version to correct our mistake
+
+ $ echo 'Tainted file is now sanitized' > target
+ $ hg ci -m sanitized target
+
+Add a second sanitized version with the same parent
+
+ $ hg bookmarks b1
+ $ hg update 'tip^'
+ 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+ (leaving bookmark b1)
+ $ echo 'Tainted file now super sanitized' > target
+ $ hg ci -m 'super sanitized' target
+ created new head
+ $ hg bookmarks b2
+
+Verify target contents before censorship at each revision
+
+ $ hg cat -r b1 target
+ Tainted file is now sanitized
+ $ hg cat -r b2 target
+ Tainted file now super sanitized
+ $ hg cat -r 'tip^' target
+ Tainted file
+ Passwords: hunter2
+ hunter3
+ $ hg cat -r 'tip^^' target
+ Tainted file
+ Passwords: hunter2
+ $ hg cat -r 'tip^^^' target
+ Initially untainted file
+
+Censor revision with 2 offenses
+
+ $ hg censor -r 'b1^' target -m 'censor two passwords' -d '2014-08-28 11:22:33 -0800'
+ $ hg cat -r b1 target
+ Tainted file is now sanitized
+ $ hg cat -r b2 target
+ Tainted file now super sanitized
+ $ hg cat -r 'b1^' target
+ abort: node 1e0247a9a4b7 in file target is censored
+ [255]
+ $ hg cat -r 'b1^^' target
+ Tainted file
+ Passwords: hunter2
+ $ hg cat -r 'b1^^^' target
+ Initially untainted file
+
+Censor revision with 1 offense
+
+ $ hg censor -r 'b1^^' target -m 'censor one password' -d '2014-08-28 11:44:55 -0800'
+ $ hg cat -r b1 target
+ Tainted file is now sanitized
+ $ hg cat -r b2 target
+ Tainted file now super sanitized
+ $ hg cat -r 2 target
+ abort: node 1e0247a9a4b7 in file target is censored
+ [255]
+ $ hg cat -r 1 target
+ abort: node 613bc869fceb in file target is censored
+ [255]
+ $ hg cat -r 0 target
+ Initially untainted file
+
+Can only checkout target at uncensored revisions, -X is workaround for --all
+
+ $ hg revert -r 'b1^' target
+ abort: file target is censored at b2ae1465d532, cannot checkout
+ [255]
+ $ hg revert -r 'b1^^' target
+ abort: file target is censored at 186fb27560c3, cannot checkout
+ [255]
+ $ hg revert -r 'b1^^' --all
+ removing .hgcensored
+ reverting bystander
+ reverting target
+ abort: file target is censored at 186fb27560c3, cannot checkout
+ [255]
+ $ hg revert -r 'b1^^' --all -X target
+ $ cat target
+ Tainted file now super sanitized
+ $ hg revert -r 'b1^^^' --all
+ reverting target
+ $ cat target
+ Initially untainted file
+ $ hg revert -r b2 --all
+ undeleting .hgcensored
+ reverting bystander
+ reverting target
+ $ cat target
+ Tainted file now super sanitized
+
+Uncensored file can be viewed at any revision
+
+ $ hg cat -r 3 bystander
+ Normal file v2
+ $ hg cat -r 2 bystander
+ Normal file v2
+ $ hg cat -r 1 bystander
+ Normal file here
+ $ hg cat -r 0 bystander
+ Normal file here
+
+Can update to children of censored revision
+
+ $ hg update b1
+ 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
+ (activating bookmark b1)
+ $ cat target
+ Tainted file is now sanitized
+ $ hg update b2
+ 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+ (activating bookmark b2)
+ $ cat target
+ Tainted file now super sanitized
+
+Require signed censorship commits in trusted $HGRC so hg verify fails
+
+ $ cp $HGRCPATH.orig $HGRCPATH
+ $ cat >> $HGRCPATH <<EOF
+ > [censor]
+ > allow = signed
+ > EOF
+
+Repo fails verification due to censorship
+
+ $ hg verify
+ checking changesets
+ checking manifests
+ crosschecking files in changesets and manifests
+ checking files
+ 1: node 613bc869fceb censored from target
+ 2: node 1e0247a9a4b7 censored from target
+ 3 files, 7 changesets, 9 total revisions
+ 2 integrity errors encountered!
+ (first damaged changeset appears to be 1)
+ [1]
+
+Cannot update to revision with censored data
+
+ $ hg update 2
+ abort: file target censored at revision b2ae1465d532
+ [255]
+ $ hg update 1
+ abort: file target censored at revision 186fb27560c3
+ [255]
+ $ hg update 0
+ 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+ (leaving bookmark b2)
+ $ hg update b2
+ 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
+ (activating bookmark b2)
+
+Allow censorship without validation in trusted $HGRC so hg verify passes
+
+ $ cp $HGRCPATH.orig $HGRCPATH
+ $ cat >> $HGRCPATH <<EOF
+ > [censor]
+ > allow = hgcensor
+ > EOF
+
+Repo passes verification with warnings with explicit config
+
+ $ hg verify
+ checking changesets
+ checking manifests
+ crosschecking files in changesets and manifests
+ checking files
+ warning: node 613bc869fceb censored from target
+ warning: node 1e0247a9a4b7 censored from target
+ 3 files, 7 changesets, 9 total revisions
+ 2 warnings encountered!
+
+May update to revision with censored data with explicit config
+
+ $ hg update 2
+ warning: censored file is empty: target
+ 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
+ (leaving bookmark b2)
+ $ cat target
+ $ hg update 1
+ warning: censored file is empty: target
+ 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+ $ cat target
+ $ hg update 0
+ 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+ $ cat target
+ Initially untainted file
+ $ hg update b2
+ 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
+ (activating bookmark b2)
+ $ cat target
+ Tainted file now super sanitized
+
+Cannot merge in revision with censored data
+
+ $ hg update b1
+ 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
+ (activating bookmark b1)
+ $ echo 'b1 clean head' > target
+ $ hg ci -m 'clean head for b1' target
+ $ hg censor -r 'b1^' target -m 'no good reason really' -d '2014-08-28 22:33:44 -0800'
+ $ hg update b2
+ 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
+ (activating bookmark b2)
+ $ hg merge -r 'b1^'
+ abort: censored node 1e0247a9a4b7!
+ [255]
+
+Revisions present in repository heads may not be censored
+
+ $ hg update -C b2
+ 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
+ (activating bookmark b2)
+ $ hg bookmark b3
+ $ hg censor -r b3 target -m 'bad content at head'
+ abort: cannot censor file in heads (c9b11ca51575)
+ (clean/delete and commit first)
+ [255]
+ $ echo 'twiddling thumbs' > bystander
+ $ hg ci -m 'bystander commit'
+ $ hg censor -r 'b3^' target -m 'bad content at head'
+ abort: cannot censor file in heads (f62edcb67fd3)
+ (clean/delete and commit first)
+ [255]
+
+Cannot censor working directory
+
+ $ echo 'seriously no passwords' > target
+ $ hg ci -m 'extend b3 arbitrarily' target
+ $ hg update 'b3^'
+ 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+ (leaving bookmark b3)
+ $ hg censor -r 'b3^' target -m 'bad content in working directory'
+ abort: cannot censor working directory
+ (clean/delete/update first)
+ [255]
+ $ hg update b3
+ 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+ (activating bookmark b3)
+
+Can re-add file after being deleted + censored
+
+ $ hg rm target
+ $ hg ci -m 'delete target so it may be censored'
+ $ hg censor -r 'b3^' target -m 'trying to censor a deletion' -d '2000-01-01 00:00:00 +0000'
+ $ hg cat -r 'b3^' target
+ target: no such file in rev d159fe213f85
+ [1]
+ $ hg cat -r 'b3^^' target
+ abort: node ee84e09d5a88 in file target is censored
+ [255]
+ $ echo 'fresh start' > target
+ $ hg add target
+ $ hg ci -m reincarnated target
+ $ hg cat -r b3 target
+ fresh start
+ $ hg cat -r 'b3^' target
+ target: no such file in rev 6ef15ea426ee
+ [1]
+ $ hg cat -r 'b3^^' target
+ target: no such file in rev d159fe213f85
+ [1]
+ $ hg cat -r 'b3^^^' target
+ abort: node ee84e09d5a88 in file target is censored
+ [255]
+ $ hg update 'b2'
+ 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
+ (activating bookmark b2)
+
+Repo with censored nodes can be cloned and cloned nodes are censored
+
+ $ cd ..
+ $ hg clone r rclone
+ updating to branch default
+ 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
+ $ cd rclone
+ $ hg cat -r b1 target
+ b1 clean head
+ $ hg cat -r b2 target
+ Tainted file now super sanitized
+ $ hg cat -r 2 target
+ abort: node 1e0247a9a4b7 in file target is censored
+ [255]
+ $ hg cat -r 1 target
+ abort: node 613bc869fceb in file target is censored
+ [255]
+ $ hg cat -r 0 target
+ Initially untainted file
+ $ hg verify
+ checking changesets
+ checking manifests
+ crosschecking files in changesets and manifests
+ checking files
+ warning: node 613bc869fceb censored from target
+ warning: node 1e0247a9a4b7 censored from target
+ warning: node 6c266b519681 censored from target
+ warning: node ee84e09d5a88 censored from target
+ 3 files, 14 changesets, 15 total revisions
+ 4 warnings encountered!
+
+Repo cloned before tainted content introduced can pull censored nodes
+
+ $ cd ../rpull
+ $ hg cat -r tip target
+ Initially untainted file
+ $ hg verify
+ checking changesets
+ checking manifests
+ crosschecking files in changesets and manifests
+ checking files
+ 2 files, 1 changesets, 2 total revisions
+ $ hg pull -B b1 -B b2 -B b3
+ pulling from $TESTTMP/r (glob)
+ searching for changes
+ adding changesets
+ adding manifests
+ adding file changes
+ added 13 changesets with 13 changes to 3 files (+1 heads)
+ adding remote bookmark b1
+ adding remote bookmark b2
+ adding remote bookmark b3
+ (run 'hg heads' to see heads, 'hg merge' to merge)
+ importing bookmark b1
+ importing bookmark b2
+ importing bookmark b3
+ $ hg update b2
+ 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
+ (activating bookmark b2)
+ $ cat target
+ Tainted file now super sanitized
+ $ hg cat -r b1 target
+ b1 clean head
+ $ hg cat -r b2 target
+ Tainted file now super sanitized
+ $ hg cat -r 2 target
+ abort: node 1e0247a9a4b7 in file target is censored
+ [255]
+ $ hg cat -r 1 target
+ abort: node 613bc869fceb in file target is censored
+ [255]
+ $ hg cat -r 0 target
+ Initially untainted file
+ $ hg verify
+ checking changesets
+ checking manifests
+ crosschecking files in changesets and manifests
+ checking files
+ warning: node 613bc869fceb censored from target
+ warning: node 1e0247a9a4b7 censored from target
+ warning: node 6c266b519681 censored from target
+ warning: node ee84e09d5a88 censored from target
+ 3 files, 14 changesets, 15 total revisions
+ 4 warnings encountered!
+
+Censored nodes can be pushed if they censor previously unexchanged nodes
+
+ $ echo 'Passwords: hunter2hunter2' > target
+ $ hg ci -m 're-add password from clone' target
+ created new head
+ $ REV=`hg id --debug -i`
+ $ echo 'Re-sanitized; nothing to see here' > target
+ $ hg ci -m 're-sanitized' target
+ $ CLEANREV=`hg id --debug -i`
+ $ hg cat -r $REV target
+ Passwords: hunter2hunter2
+ $ hg censor -r $REV target -m 'censor doubly-secure password' -d '2014-08-29 00:00:00 -0800'
+ $ hg cat -r $REV target
+ abort: node d09db1c45bd3 in file target is censored
+ [255]
+ $ hg cat -r $CLEANREV target
+ Re-sanitized; nothing to see here
+ $ hg push -B b1 -B b2 -B b3
+ pushing to $TESTTMP/r (glob)
+ searching for changes
+ adding changesets
+ adding manifests
+ adding file changes
+ added 3 changesets with 3 changes to 2 files (+1 heads)
+ updating bookmark b2
+ exporting bookmark b1
+ exporting bookmark b2
+ exporting bookmark b3
+
+ $ cd ../r
+ $ hg cat -r $REV target
+ abort: node d09db1c45bd3 in file target is censored
+ [255]
+ $ hg cat -r $CLEANREV target
+ Re-sanitized; nothing to see here
+ $ hg update $CLEANREV
+ 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
+ (leaving bookmark b2)
+ $ cat target
+ Re-sanitized; nothing to see here
+
+Censored nodes can be bundled up and unbundled in another repo
+
+ $ hg bundle --base 0 ../pwbundle
+ 16 changesets found
+ $ cd ../rclone
+ $ hg unbundle ../pwbundle
+ adding changesets
+ adding manifests
+ adding file changes
+ added 3 changesets with 3 changes to 3 files (+1 heads)
+ (run 'hg heads .' to see heads, 'hg merge' to merge)
+ $ hg cat -r $REV target
+ abort: node d09db1c45bd3 in file target is censored
+ [255]
+ $ hg cat -r $CLEANREV target
+ Re-sanitized; nothing to see here
+ $ hg update $CLEANREV
+ 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
+ $ cat target
+ Re-sanitized; nothing to see here
+ $ hg verify
+ checking changesets
+ checking manifests
+ crosschecking files in changesets and manifests
+ checking files
+ warning: node 613bc869fceb censored from target
+ warning: node 1e0247a9a4b7 censored from target
+ warning: node 6c266b519681 censored from target
+ warning: node ee84e09d5a88 censored from target
+ warning: node d09db1c45bd3 censored from target
+ 3 files, 17 changesets, 18 total revisions
+ 5 warnings encountered!
+
+Censored nodes can be imported on top of censored nodes, consecutively
+
+ $ hg init ../rimport
+ $ hg bundle --base 1 ../rimport/splitbundle
+ 15 changesets found
+ $ cd ../rimport
+ $ hg pull -r 1 -B b1 -B b2 -B b3 ../r
+ pulling from ../r
+ adding changesets
+ adding manifests
+ adding file changes
+ added 17 changesets with 18 changes to 3 files (+2 heads)
+ adding remote bookmark b1
+ adding remote bookmark b2
+ adding remote bookmark b3
+ (run 'hg heads' to see heads, 'hg merge' to merge)
+ importing bookmark b1
+ importing bookmark b2
+ importing bookmark b3
+ $ hg unbundle splitbundle
+ adding changesets
+ adding manifests
+ adding file changes
+ added 0 changesets with 0 changes to 3 files
+ (run 'hg update' to get a working copy)
+ $ hg update b2
+ 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
+ (activating bookmark b2)
+ $ cat target
+ Re-sanitized; nothing to see here
+ $ hg verify
+ checking changesets
+ checking manifests
+ crosschecking files in changesets and manifests
+ checking files
+ warning: node 613bc869fceb censored from target
+ warning: node 1e0247a9a4b7 censored from target
+ warning: node 6c266b519681 censored from target
+ warning: node ee84e09d5a88 censored from target
+ warning: node d09db1c45bd3 censored from target
+ 3 files, 17 changesets, 18 total revisions
+ 5 warnings encountered!
+ $ cd ../r
+
+Can import bundle where first revision of a file is censored
+
+ $ hg init ../rinit
+ $ hg censor -r 0 target -m 'first revision is suspicious' -d '2014-08-29 01:02:03 -0800'
+ $ hg bundle -r 0 --base null ../rinit/initbundle
+ 1 changesets found
+ $ cd ../rinit
+ $ hg unbundle initbundle
+ adding changesets
+ adding manifests
+ adding file changes
+ added 1 changesets with 2 changes to 2 files
+ (run 'hg update' to get a working copy)
+ $ hg cat -r 0 target
+ abort: node 14b47fa43d12 in file target is censored
+ [255]
@@ -247,6 +247,7 @@
acl hooks for controlling repository access
blackbox log repository events to a blackbox for debugging
bugzilla hooks for integrating with the Bugzilla bug tracker
+ censor erase file content at a given revision
churn command to display statistics about repository history
color colorize output from some commands
convert import revisions from foreign VCS repositories into