Mercurial > hg
view hgext/censor.py @ 27220:4374d819ccd5
mercurial: implement import hook for handling C/Python modules
There are a handful of modules that have both pure Python and C
extension implementations. Currently, setup.py copies files from
mercurial/pure/*.py to mercurial/ during the install process if C
extensions are not available. This way, "import mercurial.X" will
work whether C extensions are available or not.
This approach has a few drawbacks. First, there aren't run-time checks
verifying the C extensions are loaded when they should be. This could
lead to accidental use of the slower pure Python modules. Second, the
C extensions aren't compatible with PyPy and running Mercurial with
PyPy requires installing Mercurial - you can't run ./hg from a source
checkout. This makes developing while running PyPy somewhat difficult.
This patch implements a PEP-302 import hook for finding and loading the
modules with both C and Python implementations. When a module with dual
implementations is requested for import, its import is handled by our
import hook.
The importer has a mechanism that controls what types of modules we
allow to load. We call this loading behavior the "module load policy."
There are 3 settings:
* Only load C extensions
* Only load pure Python
* Try to load C and fall back to Python
An environment variable allows overriding this policy at run time. This
is mainly useful for developers and for performing actions against the
source checkout (such as installing), which require overriding the
default (strict) policy about requiring C extensions.
The default mode for now is to allow both. This isn't proper and is
technically backwards incompatible. However, it is necessary to
implement a sane patch series that doesn't break the world during
future bisections. The behavior will be corrected in future patch.
We choose the main mercurial/__init__.py module for this code out of
necessity: in a future world, if the custom module importer isn't
registered, we'll fail to find/import certain modules when running
from a pure installation. Without the magical import-time side-effects,
*any* importer of mercurial.* modules would be required to call a
function to register our importer. I'm not a fan of import time side
effects and I initially attempted to do this. However, I was foiled by
our own test harness, which has numerous `python` invoked scripts that
"import mercurial" and fail because the importer isn't registered.
Realizing this problem is probably present in random Python scripts
that have been written over the years, I decided that sacrificing
purity for backwards compatibility is necessary. Plus, if you are
programming Python, "import" should probably "just work."
It's worth noting that now that we have a custom module loader, it
would be possible to hook up demand module proxies at this level
instead of replacing __import__. We leave this work for another time,
if it's even desired.
This patch breaks importing in environments where Mercurial modules
are loaded from a zip file (such as py2exe distributions). This will
be addressed in a subsequent patch.
author | Gregory Szorc <gregory.szorc@gmail.com> |
---|---|
date | Thu, 03 Dec 2015 21:37:01 -0800 |
parents | 1aee2ab0f902 |
children | 525d9b3f0a31 |
line wrap: on
line source
# 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:: * Passwords, private keys, cryptographic material * 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 revisions if they are allowed by the "censor.policy=ignore" config option. """ from mercurial.node import short from mercurial import cmdutil, error, filelog, revlog, scmutil, util from mercurial.i18n import _ cmdtable = {} command = cmdutil.command(cmdtable) # Note for extension authors: ONLY specify testedwith = 'internal' for # 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. testedwith = 'internal' @command('censor', [('r', 'rev', '', _('censor file from specified revision'), _('REV')), ('t', 'tombstone', '', _('replacement tombstone data'), _('TEXT'))], _('-r REV [-t TEXT] [FILE]')) def censor(ui, repo, path, rev='', tombstone='', **opts): if not path: raise error.Abort(_('must specify file path to censor')) if not rev: raise error.Abort(_('must specify revision to censor')) wctx = repo[None] m = scmutil.match(wctx, (path,)) if m.anypats() or len(m.files()) != 1: raise error.Abort(_('can only specify an explicit filename')) path = m.files()[0] flog = repo.file(path) if not len(flog): raise error.Abort(_('cannot censor file with no history')) rev = scmutil.revsingle(repo, rev, rev).rev() try: ctx = repo[rev] except KeyError: raise error.Abort(_('invalid revision identifier %s') % rev) try: fctx = ctx.filectx(path) except error.LookupError: raise error.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 error.Abort(_('cannot censor file in heads (%s)') % headlist, hint=_('clean/delete and commit first')) wp = wctx.parents() if ctx.node() in [p.node() for p in wp]: raise error.Abort(_('cannot censor working directory'), hint=_('clean/delete/update first')) flogv = flog.version & 0xFFFF if flogv != revlog.REVLOGNG: raise error.Abort( _('censor does not support revlog version %d') % (flogv,)) tombstone = filelog.packmeta({"censored": tombstone}, "") crev = fctx.filerev() if len(tombstone) > flog.rawsize(crev): raise error.Abort(_( 'censor tombstone must be no longer than censored data')) # Using two files instead of one makes it easy to rewrite entry-by-entry idxread = repo.svfs(flog.indexfile, 'r') idxwrite = repo.svfs(flog.indexfile, 'wb', atomictemp=True) if flog.version & revlog.REVLOGNGINLINEDATA: dataread, datawrite = idxread, idxwrite else: dataread = repo.svfs(flog.datafile, 'r') datawrite = repo.svfs(flog.datafile, 'wb', atomictemp=True) # Copy all revlog data up to the entry to be censored. rio = revlog.revlogio() offset = flog.start(crev) for chunk in util.filechunkiter(idxread, limit=crev * rio.size): idxwrite.write(chunk) for chunk in util.filechunkiter(dataread, limit=offset): datawrite.write(chunk) def rewriteindex(r, newoffs, newdata=None): """Rewrite the index entry with a new data offset and optional new data. The newdata argument, if given, is a tuple of three positive integers: (new compressed, new uncompressed, added flag bits). """ offlags, comp, uncomp, base, link, p1, p2, nodeid = flog.index[r] flags = revlog.gettype(offlags) if newdata: comp, uncomp, nflags = newdata flags |= nflags offlags = revlog.offset_type(newoffs, flags) e = (offlags, comp, uncomp, r, link, p1, p2, nodeid) idxwrite.write(rio.packentry(e, None, flog.version, r)) idxread.seek(rio.size, 1) def rewrite(r, offs, data, nflags=revlog.REVIDX_DEFAULT_FLAGS): """Write the given full text to the filelog with the given data offset. Returns: The integer number of data bytes written, for tracking data offsets. """ flag, compdata = flog.compress(data) newcomp = len(flag) + len(compdata) rewriteindex(r, offs, (newcomp, len(data), nflags)) datawrite.write(flag) datawrite.write(compdata) dataread.seek(flog.length(r), 1) return newcomp # Rewrite censored revlog entry with (padded) tombstone data. pad = ' ' * (flog.rawsize(crev) - len(tombstone)) offset += rewrite(crev, offset, tombstone + pad, revlog.REVIDX_ISCENSORED) # Rewrite all following filelog revisions fixing up 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: revdata = flog.revision(srev) except error.CensoredNodeError as e: revdata = e.tombstone dlen = rewrite(srev, offset, revdata) else: # Copy any other revision data verbatim after fixing up the offset. rewriteindex(srev, offset) dlen = flog.length(srev) for chunk in util.filechunkiter(dataread, limit=dlen): datawrite.write(chunk) offset += dlen idxread.close() idxwrite.close() if dataread is not idxread: dataread.close() datawrite.close()