Source

hgban / hgban.py

The default branch has multiple heads

Full commit
#!/usr/bin/env python
# hgban by Jason Harris and Angel Ezquerra
# Copyright (c) 2012, Jason Harris and Angel Ezquerra
# hgban is licensed under the standard 3 caluse BSD license.

'''hgban is a Mercurial extension which can be used to abort any push, pull or
bundle operation that matches a given criteria.

The extension sets up a pretxnchangegroup hook, which is executed on every
push, pull and bundle operation. The hook tries to match every new changeset
in the changegroup that is being pushed, pulled or bundled into a repository
against a list of "banned" revision sets.

If any of the changesets in the changegroup matches any of the "banned"
revision sets the entire changegroup is be rejected, and the push, pull or
bundle operation is aborted.

Setting up the list of banned revision sets
===========================================

There are two complementary ways to specify the list of banned revision sets:

1. Create a file called ".hgban" at the root of the repository.
   Each line in that file corresponds to a banned revision set.
   A banned revision set can be specified as simple revision id
   or a a complex revision set query.

   You can add comments to your .hgban file by beginning a line with
   a "#" character. You can put spaces in front of or after a revision
   set or comment and they will be ignored.
   
   The .hgban file does not need to be committed, although in practice
   it usually makes sense to do so.

2. Set a hgban.revsets key in one of your mecurial configuration files
   (i.e. add a "hgban" section, and in it set a "revsets" key).
   In order to ban more than one revset using this method you must create a
   multi-line configuration key as explained in
   (http://www.selenic.com/mercurial/hgrc.5.html#syntax). Note that you can
   add comments to this key, but you cannot add empty lines between revsets
   (see below for an example)

   This is useful when you want to ban a certain revset but you do not want
   to track the fact that you ban that revset in your repository history.

(Note either or both methods can be used to ban changesets.)

Enabling the extension
======================

You can enable the hgban extension just like any other Mercurial extension by
adding the following to your hgrc:

[extensions]
hgban = /path/to/hgban.py

Configuration Examples
======================

Using this extension you could for example ban the revision id with
the following hash:

ce3b00de97cf04655227554a13be8b077d5a3d2f

by creating a file called ".hgban" at the root of your repo
with the following contents:

ce3b00de97cf04655227554a13be8b077d5a3d2f

You could _also_ ban any revisions commited by "John Doe" by
adding an additional line to the .hgban file:

ce3b00de97cf04655227554a13be8b077d5a3d2f
author("John Doe")

Note that you could also add comments as follows:

# Ban changeset that adds nuclear launch keys
ce3b00de97cf04655227554a13be8b077d5a3d2f

# Do not allow John Doe to push any of his changes
author("John Doe")

Alternativelly, instead of using the .hgban file you could set
the hgban.revsets configuration key as follows:

[hgban]
revsets = # Ban changeset that adds nuclear launch keys
          ce3b00de97cf04655227554a13be8b077d5a3d2f
          # Do not allow John Doe to push any of its changes
          author("John Doe")

Note that each line on the revsets key (except the first one)
_must_ be indented, and that while you can add comments you cannot
add empty lines.

You could have combined the use of the .hgban file and of the
hgban.revsets key to achieved the same result:

[hgban]
revsets = # Ban changeset that adds nuclear launch keys
          ce3b00de97cf04655227554a13be8b077d5a3d2f

".hgban" file:

# Do not allow John Doe to push any of its changes
author("John Doe")
'''

import os.path, re
from mercurial import hg, revset


def getSetOfBannedRevsets(repo):
    try:
        bannedRevsetsPath = repo.wjoin('.hgban')
        f = open(bannedRevsetsPath, 'r')
        banned = set()
        commentPat = re.compile(r"#.*$")
        nodePat = re.compile(r"\s*(^[0-9a-fA-F]+).*")
        for line in f:
            rset = line.strip()
            m = re.match(commentPat, rset)
            if not m:
                if re.match(nodePat, rset):
                    # Explicitly consider as node id references those
                    # ban conditions matching the "node id pattern"
                    rset = 'id(%s)' % rset
                if rset:
                    banned.add(rset)
        f.close()
    except:
        banned = set()

    # Get the banned revsets defined in the mercurial configuration files
    rcbanned = repo.ui.config('hgban', 'revsets', '').strip()
    if rcbanned:
        rcbanned = set(rcbanned.splitlines())

    banned = banned.union(rcbanned)

    return banned

def getreposetfunc():
    '''
    Yield a context for each matching revision, after doing arg
    replacement via revset.formatspec
    '''
    def rsetfunc(repo, expr, *args):
        m = revset.match(None, expr)

        for r in m(repo, range(len(repo))):
            yield repo[r]

    try:
        # hg >= 2.0
        return repo.set
    except:
        # hg <= 1.9
        return rsetfunc

def reposet(repo, expr, *args):
    '''
    Yield a context for each matching revision, after doing arg
    replacement via revset.formatspec
    '''

    m = revset.match(None, expr)

    for r in m(repo, range(len(repo))):
        yield repo[r]

def checkForBannedRevsets(ui, repo, **kwargs):
    node = kwargs.get('node')
    if node:
        bannedRevsets = getSetOfBannedRevsets(repo)
        if not bannedRevsets:
            # Successful early exit if there are no banned changesets
            return False

        startRev = int(repo[node])
        descendantRevs = list(repo.changelog.descendants(startRev))
        descendantRevs.append(startRev)

        rejectedChangesets = set()

        for revsetexpr in bannedRevsets:
            try:
                rsetfunc = getreposetfunc()
                for ctx in rsetfunc(repo, "%d: and (%s)" % (startRev, revsetexpr)):
                    if ctx:
                        rejectedChangesets.add(ctx.rev())
            except:
                # most likely, and no valid revisions were found
                # if that is the case, we can simply check the next condition
                continue

        if len(rejectedChangesets) > 0:
            repoName = os.path.basename(repo.root)
            ui.warn('The ban-changeset extension rejected the %s operation on the repository \'%s\' due to the changeset(s):\n' % (kwargs.get('source'), repoName))
            for changeset in rejectedChangesets:
                ui.warn('    %s\n' % changeset)
            if (len(rejectedChangesets) < len(descendantRevs)):
                if len(rejectedChangesets) == 1:
                    ui.warn('Rebase, transplant, or otherwise move any valid changesets in the source repository which are derived from this rejected changeset. Strip the banned changesets, and then redo the operation.\n')
                else:
                    ui.warn('Rebase, transplant, or otherwise move any valid changesets in the source repository which are derived from these rejected changesets. Strip the banned changesets, and then redo the operation.\n')
            return True # We found a banned changeset, return True (exit code 1) which causes the changegroup addition to be aborted.
    return False # No banned changesets were found. The changegroup addition can go ahead.

def reposetup(ui, repo):
    ui.setconfig("hooks", "pretxnchangegroup.hgban", checkForBannedRevsets)