How to enforce a commit policy
From a mail from Bryan O'Sullivan : http://marc.theaimsgroup.com/?l=mercurial&m=114443474322125&w=2
Here's an interesting application of hooks to make sure that changes conform to your project's policies (mercurial 0.8.1 and after required).
I'll take the Linux kernel as an example; Linus requires patch descriptions to have at least one "Signed-off-by:" line, so that people can tell who wrote and reviewed each patch. Obviously, you could change this to require that a bug ID be referenced or something. You get the idea
There are two easy ways to enforce this kind of policy in Mercurial. The first is to forbid people from even committing changesets that don't follow the policy. You can do this using the "pretxncommit" hook:
[hooks] pretxncommit.signoff = hg log --template '{desc}\n' -r $HG_NODE | \ grep -qi '^signed-off-by:'
Here's a step-by-step breakdown of what's happening above.
- The pretxncommit hook runs before a changeset is finally committed. It's only run for commands like "commit" and "import" that create new changesets.
- If the hook exits with status 0, the commit is allowed to complete. Any non-zero exit status will cause the commit to abort. This leaves the working directory completely untouched, but the changeset won't get created.
- The hook is an arbitrary shell command, so you can use all normal shell syntax.
- We can add a "." and an arbitrary suffix to the end of the pretxncommit hook (and all other hooks), so that it's possible to run more than one such hook. These hooks are run in sorted order.
- The hook is run with the environment variable $HG_NODE set to the ID of the changeset that's being created. While the hook runs, normal Mercurial commands, such as log, can see the changeset as if it's a normal changeset.
- We use "hg log" to examine the new changeset. We use "--template" to output the description of the changeset, "-r $HG_NODE" to examine only that changeset, and grep to check the description to see if it contains a "signed-off-by" line. The reason we display only the description is to make sure that grep only looks at the needed information, and nothing more.
- If grep doesn't find a match, it will exit with non-zero status, and cause the commit to be rolled back. Afterwards, the changeset we saw in $HG_NODE will no longer exist.
This hook could print an informative error message, but I've kept it simple to illustrate the point.
As an alternative, we can allow people to create whatever changesets they wanted, by leaving this hook out, and instead install a hook on a shared server that will enforce this rule. This model works well with the Mercurial Queues (mq) extension, where you might want to edit and re-edit a patch locally many times before finally pushing it for others to see.
On a shared repository, we use the "pretxnchangegroup" hook. This gets fired whenever someone pushes, pulls, or unbundles changesets from somewhere else into the repository. It doesn't get run on a commit.
[hooks] pretxnchangegroup.signoff = hg log --template '{desc}\n' \ -r $HG_NODE: | grep -qi 'signed-off-by:'
There are a few minor differences between "pretxnchangegroup" and "pretxncommit".
- pretxncommit is run on each changeset created, but pretxnchangegroup is run once for all changesets brought in by a single pull, push, or unbundle.
- For pretxnchangegroup, the $HG_NODE environment variable contains the ID of the *first* changeset added. To examine all new changesets, we thus need to use range syntax when running "hg log" above - "-r $HG_NODE:" (see the ":" at the end? that means "all revs up to and including the end").
Here's something to bear in mind: since "hg log" can see those changes from inside the hook, other hg commands outside the hook can see those changes, too. If you're pushing to a repo that uses a hook like this, you should make sure that nobody can pull from it, or else they'll be able to pull a change that the hook may not allow to be committed.
How do you forbid pulls? Simple.
[hooks] preoutgoing.never = test $HG_SOURCE = push -o $HG_SOURCE = bundle
This hook allows push or bundle from the repository, but not pull.
So if people can't pull from that repo because they might get changes that the pretxnchangegroup hook will forbid, what should they do? Use a hook to push approved changes to another repo that it's OK to pull from.
[hooks] changegroup.push = hg push ../ok-to-pull
The "changegroup" hook gets fired after the transaction adding a set of changesets has been committed, so it's the perfect place to push from. Its exit status can't cause anything to abort, either.