I recently found the need to venture into the somewhat shady realm of Mercurial hooks. (If you are not familiar, Mercurial is a git-like Version Control System written in Python.) Within my development environment, after making releases, we construct named branches of the release, to be kept pristine in perpetuity. This allows us, at any time in the future, to reconstruct a particular release identical to what has been released to the public, or to sub-branch from that point for any subsequent patch releases that might be necessary.
While Mercurial has a rather powerful branch management toolset (which is one of the reasons we switched to it from Subversion), one thing it does appear to lack is the ability to actually “freeze” a named branch. By “freeze”, I mean actively disallow any further modifications. Up to this point on my project (and even under Subversion), “freezing” a branch amounted to not much more than a mutual understanding and agreement that no further modifications were to be made. However, I recently ran into a situation where a released branch was being modified by a developer because they did not (for some reason) realize that it had been released, and had been modifying it for some months before it was discovered. Although it was an accident, and I could probably have walked back through and somehow undone the damage, I decided to harness my fury and put in place a mechanism that would actually prevent it in the future.
Why Not Just Close A Branch?
Mercurial does provide a mechanism to “close” a named branch. I explored this possibility before going for the throat. However, one of the major flaws I discovered with this approach is that any push to a “closed” branch automatically re-opens it and applies the modification. This is really unacceptable to my intent, and I realized that I was left with no other choice but to put a hook in place.
An Uncommon Task
Of course, I went in search of an existing piece of work that would accomplish what I wanted. Surely, I thought, this would be a common thing, and there would be numerous examples from which I could choose. Well…such was not the case. A considerable search for a Mercurial hook that would do exactly what I wanted proved to simply not exist.
Picking pieces from other existing hooks, and scanning through the Mercurial hook documentation (such as it is), I was able to put together a fairly simple Mercurial hook that will actually “freeze” a named branch.
So, in case you find yourself in the same place, here’s how I solved the problem…
Update The Config
First, you’ll want to open your repository’s
.hg/hgrc file. In my case, this is on a centralized server where Mercurial is being served via Apache. In this location, any and all interactions with the repository can be monitored by the hook.
hgrc file does not already have a “hooks” section, you’ll want to add one, along with an entry for a
pretxnchangegroup.frozen_branches = python:frozen_branches.frozen_branches.hook
This creates an “in-process” hook, meaning the hook will run within the Mercurial space instead of running as a stand-alone process (each type requires a different interface). You will want to place the file (we’ll call this one “frozen_branches.py”) that contains your hook function into one of the folders in Python’s
sys.path list. You can see all the paths Python will search by running this Python command:
python -c "import sys ; print sys.path"
Once you’ve selected a location from Python’s search paths, create a folder called “frozen_branches.” It is within this directory that you will deposit your hook files.
Within the same
hgrc file where you specified the hook itself, you’ll want to create a section for your hook’s data. This section will contain a single setting that lists all the named branches that are to be considered “frozen” by the hook. It might look something like this:
freeze_list = 10.0, 10.next, 11.0, 11.0.sp1, 11.0.sp2
Your hook needs to function as a Python package. This means that, along with your main Python file, you will need to create an empty “__init__.py” file in the same folder to flag it as a package. The existence of this file seemed to be necessary for the hook to successfully load into the Mercurial environment. YMMV.
The frozen_branches.hook begins with a default function signature that most hooks require:
def hook(ui, repo, **kwargs):
I’m not going to pretend to have deep arcane knowledge of these arguments. Based on other hook examples I could find, I know that
ui will be used for various I/O functions, and
repo represents all the information we will require for the pending changes to the repository.
Next, we need to get our settings from the
hgrc. We use
ui to access this information:
frozen_list = ui.configlist('frozen_branches', 'freeze_list')
if frozen_list is None:
# no frozen branches listed; allow all changes
Note that returning
False from the hook indicates that the action should be allowed, while returning
True tells Mercurial that the action should be rejected.
To ensure proper usage, we need to check the hook type to make sure it is not being used for anything other than the
pretxnchangegroup hook type. Using it with another type may have unpredictable results. We extract this information from the
hooktype = kwargs['hooktype']
if hooktype != 'pretxnchangegroup':
ui.warn('frozen_branches: Only "pretxnchangegroup" hooks are supported by this script\n')
There are other (potentially valuable) keyword values contained in the
kwargs list; we use a few — like ‘hooktype’ and ‘node’ — in this script.
We now need to examine each incoming changeset that is about to be applied to the repository, and if there are any that target a designated “frozen” branch, the entire action will be rejected with a message to the user:
ctx = repo[kwargs['node']]
start = ctx.rev()
end = len(repo)
for rev in xrange(start, end):
node = repo[rev]
branch = node.branch()
if branch in frozen_list:
ui.warn(“abort: %d:%s includes modifications to a frozen branch ‘%s’!\n” % (rev, node.hex()[:12], branch))
# reject the entire changegroup
Note the formatting in the message:
(rev, node.hex()[:12], ...
These values will produce a changeset identifier which should match what the user will see if they do an “hg log” or “hg outgoing”. This should make it easier to identify the offending entry.
Finally, if everything is acceptable (i.e., the loop finished without hitting its embedded return), we let Mercurial know that the entire changegroup can be applied to the repository:
# allow the changegroup
No More Nasty Surprises
Once in place, any attempt to modify a “frozen” branch will result in an abort on the client end, with an explanatory error message:
$ hg push
pushing to https://...
searching for changes
remote: adding changesets
remote: adding manifests
remote: adding file changes
remote: added 1 changesets with 1 changes to 1 files
remote: abort: 26218:de8955e0b9f4 includes modifications to a frozen branch '11.0.sp2'!
remote: transaction abort!
remote: rollback completed
$ hg outgoing
comparing with https://…
searching for changes
And the day will never come again when you find a nasty surprise waiting in a released — and supposedly frozen — branch. 🙂