<> <> <> = Hook Examples = Getting started writing '''[[Hook|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: {{{#!python numbers=disable 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 .-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: {{{#!python numbers=disable _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 .-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.) {{{#!python numbers=disable 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 }}} == pretxncommit/pretxnchangegroup: Enforce commit message rules == Often commit messages follow conventions, most notably referring to a bug/issue number in their description. Since we can't enforce hooks for all distributed clones because hooks don't transfer, we have to enforce it on a known-good (central) repository. Thus, we have to write a pretxnchangegroup hook. Since fixing a bunch of commit messages at push time is a big headache requiring mq, we'll also define a pretxncommit hook that well-behaved developers can use to enforce rules at commit time. These hooks are defined as python in-process hooks to get portability between Windows and Linux, as well as to get nice error messages because in-process hooks can use the ui.warn() method. You must define checkMessage() to define the rule and printUsage() to explain the rule to the user. Given some work, the two methods could be combined into a more generic method, but that is an exercise left to the developer. {{{#!python numbers=disable import re def checkCommitMessage(ui, repo, **kwargs): """ Checks a single commit message for adherence to commit message rules. To use add the following to your project .hg/hgrc for each project you want to check, or to your user hgrc to apply to all projects. [hooks] pretxncommit = python:path/to/script/enforce-message.py:checkCommitMessage """ hg_commit_message = repo['tip'].description() if(checkMessage(hg_commit_message) == True): printUsage(ui) return True else: return False def checkAllCommitMessage(ui, repo, node, **kwargs): """ Checks all inbound changeset messages from a push for adherence to the commit message rules. [hooks] pretxnchangegroup = python:path/to/script/enforce-message.py:checkAllCommitMessage """ for rev in xrange(repo[node].rev(), len(repo)): message = repo[rev].description() if(checkMessage(message) == True): ui.warn("Revision "+str(rev)+" commit message:["+message+"] does not adhere to commit message rules\n") printUsage(ui) return True return False }}} == Generic pretxncommit/pretxnchangegroup Hook == Many of the hooks you'll be writing are pretxncommit/pretxnchangegroup. Here is a general framework for that type of hook. === In python === {{{#!python numbers=disable #!/usr/bin/env python def hook(ui, repo, hooktype, node=None, source=None, **kwargs): if hooktype not in ['pretxnchangegroup', 'pretxncommit']: ui.write('Hook should be pretxncommit/pretxnchangegroup not "%s".' % hooktype) return 1 for rev in xrange(repo[node], len(repo)): message=repo[rev].description() for file in repo[rev].files(): #if (error): # ui.write("\nERROR: Some criteria not met.\n\n") # return 1 return 0 }}} === In bash === {{{#!highlight bash numbers=disable #!/bin/bash revs=$(hg log -r "$HG_NODE:tip" --template '{rev} ') #Intentional space after {rev} rc=0 for rev in $revs do files=$(hg log -r $rev --template '{files}') #Above will include discards. So you cannot 'hg cat' them all. So you may want # files=$(hg log -r $rev --template '{file_mods} {file_adds}') branch=$(hg log -r $rev --template '{branch}') for file in $files do #You do not want to check file contents with 'hg cat' here; it is too slow. #If you need to check file contents, write a native python hook. #if [ error ] ; then # echo "ERROR: Some criteria not met." # exit 1 done done exit $rc }}} == pretxnchangegroup: enforce the stable branch to contains only merge commits on the server == If your company policy is to allow only merge on the stable branch, this hook is for you. Note that it allows the first commit (the one after {{{hg branch}}}). This might be abused. A possible improvement would be to only allow this single commit if the stable branch does not exists yet. {{{#!python numbers=disable from mercurial.node import bin, nullid from mercurial import util def hook(ui, repo, node, **kwargs): n = bin(node) start = repo.changelog.rev(n) end = len(repo.changelog) failed = False for rev in xrange(start, end): n = repo.changelog.node(rev) ctx = repo[n] p = ctx.parents() if ctx.branch() == 'stable' and len(p) == 1: if p[0].branch() != 'stable': # commit that creates the branch, allowed continue ui.warn(' - changeset %s on stable branch and is not a merge !\n' % (ctx)) failed = True if failed: ui.warn('* Please strip the offending changeset(s)\n' '* and re-do them, if needed, on another branch!\n') return True }}} == pretxnchangegroup: export pushed changes == This hook can be used to set up a server that allows pushes via HTTP for everybody, but which unconditionally rejects pushes and so doesn't modify actual history. Instead, it may expose incoming changesets as patches (mailing them or showing in a web interface). This could be used to ease contributing changes to a project. {{{#!highlight sh numbers=disable #!/bin/sh $HG export -o '%h-%m.patch' -r "$HG_NODE:$HG_NODE_LAST" echo 'Thanks for patches!' exit 1 }}} Another way may be to write bundle files for easier acceptance process (since they can be just pulled from), but it requires another repo: `project` repo contains canonical history and doesn't allow HTTP pushes, and `project-push` allows pushes for everybody and has this pretxnchangegroup hook: {{{#!highlight sh numbers=disable #!/bin/sh $HG bundle -r "$HG_NODE:$HG_NODE_LAST" incoming.hg /path/to/canonical/repo/project exit 1 }}}