hgext/inhibit.py
author Laurent Charignon <lcharignon@fb.com>
Fri, 27 Mar 2015 10:58:04 -0700
changeset 1232 37c00aeb4762
parent 1225 577f5340be6f
child 1233 63ee05dd557a
permissions -rw-r--r--
inhibit: enable direct access from parsing the revset tree To enable direct access we: - detect explicit hexadical reference to node in the revset tree - add these hashes to the static blockers to make them and their parent visible for the command to run

"""Reduce the changesets evolution feature scope for early and noob friendly UI

The full scale changeset evolution have some massive bleeding edge and it is
very easy for people not very intimate with the concept to end up in intricate
situation. In order to get some of the benefit sooner, this extension is
disabling some of the less polished aspect of evolution. It should gradually
get thinner and thinner as changeset evolution will get more polished. This
extension is only recommended for large scale organisations. Individual user
should probably stick on using Evolution in its current state, understand its
concept and provide feedback

The first feature provided by this extension is the ability to "inhibit"
obsolescence markers. Obsolete revision can be cheaply brought back to life
that way. However as the inhibitor are not fitting in an append only model,
this is incompatible with sharing mutable history.

The second feature is called direct access. It is the ability to refer and
access hidden sha in commands provided that you know their value.
For example hg log -r XXX where XXX is a commit has should work whether XXX is
hidden or not as we assume that the user knows what he is doing when referring
to XXX.
"""
from mercurial import localrepo
from mercurial import obsolete
from mercurial import extensions
from mercurial import cmdutil
from mercurial import scmutil
from mercurial import repoview
from mercurial import revset
from mercurial import error

cmdtable = {}
command = cmdutil.command(cmdtable)

def reposetup(ui, repo):

    class obsinhibitedrepo(repo.__class__):

        @localrepo.storecache('obsinhibit')
        def _obsinhibit(self):
            # XXX we should make sure it is invalidated by transaction failure
            obsinhibit = set()
            raw = self.sopener.tryread('obsinhibit')
            for i in xrange(0, len(raw), 20):
                obsinhibit.add(raw[i:i+20])
            return obsinhibit

    repo.__class__ = obsinhibitedrepo
    repo._explicitaccess = set()

# obsolescence inhibitor
########################

def _schedulewrite(tr, obsinhibit):
    """Make sure on disk content will be updated on transaction commit"""
    def writer(fp):
        """Serialize the inhibited list to disk.
        """
        raw = ''.join(obsinhibit)
        fp.write(raw)
    tr.addfilegenerator('obsinhibit', ('obsinhibit',), writer)
    tr.hookargs['obs_inbihited'] = '1'

def _filterpublic(repo, nodes):
    """filter out inhibitor on public changeset

    Public changesets are already immune to obsolescence"""
    getrev = repo.changelog.nodemap.get
    getphase = repo._phasecache.phase
    return (n for n in repo._obsinhibit if getphase(repo, getrev(n)))

def _inhibitmarkers(repo, nodes):
    """add marker inhibitor for all obsolete revision under <nodes>

    Content of <nodes> and all mutable ancestors are considered. Marker for
    obsolete revision only are created.
    """
    newinhibit = repo.set('::%ln and obsolete()', nodes)
    if newinhibit:
        tr = repo.transaction('obsinhibit')
        try:
            repo._obsinhibit.update(c.node() for c in newinhibit)
            _schedulewrite(tr, _filterpublic(repo, repo._obsinhibit))
            repo.invalidatevolatilesets()
            tr.close()
        finally:
            tr.release()

def _deinhibitmarkers(repo, nodes):
    """lift obsolescence inhibition on a set of nodes

    This will be triggered when inhibited nodes received new obsolescence
    markers. Otherwise the new obsolescence markers would also be inhibited.
    """
    deinhibited = repo._obsinhibit & set(nodes)
    if deinhibited:
        tr = repo.transaction('obsinhibit')
        try:
            repo._obsinhibit -= deinhibited
            _schedulewrite(tr, _filterpublic(repo, repo._obsinhibit))
            repo.invalidatevolatilesets()
            tr.close()
        finally:
            tr.release()

def _createmarkers(orig, repo, relations, flag=0, date=None, metadata=None):
    """wrap markers create to make sure we de-inhibit target nodes"""
    # wrapping transactio to unify the one in each function
    tr = repo.transaction('add-obsolescence-marker')
    try:
        orig(repo, relations, flag, date, metadata)
        precs = (r[0].node() for r in relations)
        _deinhibitmarkers(repo, precs)
        tr.close()
    finally:
        tr.release()

def extsetup(ui):
    # lets wrap the computation of the obsolete set
    # We apply inhibition there
    obsfunc = obsolete.cachefuncs['obsolete']
    def _computeobsoleteset(repo):
        """remove any inhibited nodes from the obsolete set

        This will trickle down to other part of mercurial (hidden, log, etc)"""
        obs = obsfunc(repo)
        getrev = repo.changelog.nodemap.get
        for n in repo._obsinhibit:
            obs.discard(getrev(n))
        return obs
    obsolete.cachefuncs['obsolete'] = _computeobsoleteset
    # drop divergence computation since it is incompatible with "light revive"
    obsolete.cachefuncs['divergent'] = lambda repo: set()
    # drop bumped computation since it is incompatible with "light revive"
    obsolete.cachefuncs['bumped'] = lambda repo: set()
    # wrap create marker to make it able to lift the inhibition
    extensions.wrapfunction(obsolete, 'createmarkers', _createmarkers)
    extensions.wrapfunction(repoview, '_getdynamicblockers', _accessvisible)
    extensions.wrapfunction(revset, 'posttreebuilthook', _posttreebuilthook)

def gethashsymbols(tree):
    # Returns the list of symbols of the tree that look like hashes
    # for example for the revset 3::abe3ff it will return ('abe3ff')
    if not tree:
        return []

    if len(tree) == 2 and tree[0] == "symbol":
        try:
            int(tree[1])
            return []
        except ValueError as e:
            return [tree[1]]
    elif len(tree) == 3:
        return gethashsymbols(tree[1]) + gethashsymbols(tree[2])
    else:
        return []

def _posttreebuilthook(orig, tree, repo):
    # This is use to enabled direct hash access
    # We extract the symbols that look like hashes and add them to the
    # explicitaccess set
    orig(tree, repo)
    if repo and repo.filtername == 'visible':
        prelength = len(repo._explicitaccess)
        repo.symbols = gethashsymbols(tree)
        cl = repo.unfiltered().changelog
        for node in repo.symbols:
            try:
                node = cl._partialmatch(node)
            except error.LookupError:
                node = None
            if node is not None:
                rev = cl.rev(node)
                if rev not in repo.changelog:
                    repo._explicitaccess.add(rev)
        if prelength != len(repo._explicitaccess):
            repo.invalidatevolatilesets()

@command('debugobsinhibit', [], '')
def cmddebugobsinhibit(ui, repo, *revs):
    """inhibit obsolescence markers effect on a set of revs"""
    nodes = (repo[r].node() for r in scmutil.revrange(repo, revs))
    _inhibitmarkers(repo, nodes)

# ensure revision accessed by hash are visible
###############################################

def _accessvisible(orig, repo):
    """ensure accessed revs stay visible"""
    blockers = orig(repo)
    blockers.update(getattr(repo, '_explicitaccess', ()))
    return blockers