Differences between revisions 2 and 3
Revision 2 as of 2010-02-26 07:11:55
Size: 5028
Editor: abuehl
Comment: link to hook page
Revision 3 as of 2011-01-17 16:32:29
Size: 5061
Comment:
Deletions are marked like this. Additions are marked like this.
Line 2: Line 2:
Line 7: Line 6:
== General notes ==
Of note:

 * all hooks will take `ui`, `repo,hooktype ` -- that's a very common pattern in Mercurial code (core, extensions, hooks, whatever)
 * I don't care what other arguments Mercurial passes to my hook, so I just declare `**kwargs` so it can pass anything it likes
 * use `repo[None]` to get a `changectx` object representing the working dir, i.e. what is ''about'' to be committed
 * use `ui.warn()` to print an error message to the user (unfortunately there is no `ui.error()` -- generally Mercurial code just raises `util.Abort` on fatal error)
 * hooks use the peculiar convention of returning a true value to indicate failure. This is weird, but 1) it's consistent with external hooks, which return non-zero to indicate failure (the usual convention for child processes on Unix) and 2) it means that falling off the end of a hook (implicitly returning `None`) is success
Line 8: Line 16:
Say you have local policy that states branches must be named like ''major''.''minor''-branch, e.g. `1.0-branch` or `1.1-branch`. You don't want developers creating new branches willy-nilly, because that can cause a mess. You can enforce this with a precommit hook:
Line 9: Line 18:
Say you have local policy that states branches must be named like ''major''.''minor''-branch, e.g. `1.0-branch` or `1.1-branch`. You don't want developers creating new branches willy-nilly, because that can cause a mess. You can enforce this with a precommit hook:
Line 21: Line 29:
Of note:
  * all hooks will take `ui`, `repo` -- that's a very common pattern in Mercurial code (core, extensions, hooks, whatever)
  * I don't care what other arguments Mercurial passes to my hook, so I just declare `**kwargs` so it can pass anything it likes
  * use `repo[None]` to get a `changectx` object representing the working dir, i.e. what is ''about'' to be committed
  * use `ui.warn()` to print an error message to the user (unfortunately there is no `ui.error()` -- generally Mercurial code just raises `util.Abort` on fatal error)
  * hooks use the peculiar convention of returning a true value to indicate failure. This is weird, but 1) it's consistent with external hooks, which return non-zero to indicate failure (the usual convention for child processes on Unix) and 2) it means that falling off the end of a hook (implicitly returning `None`) is success
Line 29: Line 30:
Line 33: Line 33:
Line 49: Line 50:

Hook Examples

Getting started writing hooks can be tricky, especially in-process (Python) hooks. On the one hand, writing hooks is a good way to learn your way around Mercurial's internals. But if you don't already know those internals, getting your hooks working is hard work. Hence this page, which contains documented hook code based on real-world hooks. (I'm going to assume that you have a working knowledge of Python here. You don't need to be an expert, but please work through a tutorial or two before trying to write Mercurial hooks.)

Note that MercurialApi is the best available documentation for the internal APIs that you will most commonly use writing hooks. If anything in here seems unclear, pop over to MercurialApi to see if there is a better explanation there.

General notes

Of note:

  • all hooks will take ui, repo,hooktype  -- that's a very common pattern in Mercurial code (core, extensions, hooks, whatever)

  • I don't care what other arguments Mercurial passes to my hook, so I just declare **kwargs so it can pass anything it likes

  • use repo[None] to get a changectx object representing the working dir, i.e. what is about to be committed

  • use ui.warn() to print an error message to the user (unfortunately there is no ui.error() -- generally Mercurial code just raises util.Abort on fatal error)

  • hooks use the peculiar convention of returning a true value to indicate failure. This is weird, but 1) it's consistent with external hooks, which return non-zero to indicate failure (the usual convention for child processes on Unix) and 2) it means that falling off the end of a hook (implicitly returning None) is success

Precommit: disallow bad branch

Say you have local policy that states branches must be named like major.minor-branch, e.g. 1.0-branch or 1.1-branch. You don't want developers creating new branches willy-nilly, because that can cause a mess. You can enforce this with a precommit hook:

import re

def precommit_badbranch(ui, repo, **kwargs):
    branch = repo[None].branch()
    branch_re = re.compile(r'\d+\.\d+-branch$')
    if not branch_re.match(branch):
        ui.warn('invalid branch name %r (use <major>.<minor>-branch)\n')
        return True
    return False

Precommit: disallow bad branch and bad merge

If you have a strong convention for branch names like the above, it enables an additional sanity check: don't allow backwards merges. Imagine what would happen if someone accidentally merged 1.1-branch to 1.0-branch when they meant to do the reverse. Bad idea. So let's take advantage of our branch name convention to figure out which branch is later, and only allow merges from earlier to later branches.

First, we need to factor out the code that parses a branch name:

_branch_re = re.compile(r'(\d+)\.(\d+)-branch$')

def _parse_branch(branch):
    """Parse branch (a string) and return a tuple of ints.  Raise
    ValueError if branch is not a valid branch name according to local
    policy."""
    match = _branch_re.match(branch)
    if not match:
        raise ValueError('invalid branch name %r '
                         '(use <major>.<minor>-branch)\n')
    return tuple(map(int, match.group(1, 2)))

Return a tuple of ints makes branchs trivially comparable: 1.9-branch becomes (1, 9), 1.10-branch becomes (1, 10), and (1, 9) < (1, 10).

Now, one big hook to enforce both bits of policy. (This also disallows another strange type of merge that I discovered while writing the hook. Whether you want to disallow it is your business, of course.)

def precommit_badbranch_badmerge(ui, repo, parent1=None, parent2=None, **kwargs):
    branch = repo[None].branch()
    try:
        branch = _parse_branch(branch)
    except ValueError, err:
        ui.warn('%s\n' % err)
        return True

    if not parent2:
        # Not merging: nothing more to check.
        return False

    # parent1 and parent2 are both existing changesets, so assume that
    # their branch names are valid.  If that's not the case, we'll blow
    # up with a ValueError here.  If you're trying to enforce new policy
    # on a repo with existing branch names, this will have to be more
    # flexible.
    target_branch = _parse_branch(repo[parent1].branch())
    source_branch = _parse_branch(repo[parent2].branch())

    # This could happen if someone does
    #   hg update 1.1-branch
    #   hg branch 1.2-branch
    #   hg merge 1.0-branch
    # which is a strange thing to do.  So disallow it.
    if target_branch != branch:
        ui.warn('merging to a different branch from first parent '
                'is just weird: please don\'t do that\n')
        return True

    # Check for backwards merge.
    if source_branch > target_branch:
        ui.warn('invalid backwards merge from %r to %r\n'
                % (source_branch, target_branch))
        return True

    return False

HookExamples (last edited 2016-03-08 05:38:10 by AntonShestakov)