3

Id like to set a pre-receive hook in our git server that checks a config file and discards the push if it's invalid (I want to check presence of certain tokens depending on branch name). But I've seen preceive hook just receives a list of (old-rev, new-rev, refname) and the only way I've found to inspect file contents is diffing those references, which is not very confortable.

Is there an easy way to do this? With a precommit hook it would be easier but I'd like to have a last barrier in the server.

hithwen
  • 2,154
  • 28
  • 46

1 Answers1

6

A pre-receive or update hook is called after the new objects (commits, annotated tag objects, trees, and blobs) have been loaded into the repository, but before the references (branch names, tag names, etc) have been changed.

This is why the pre-receive hook gets a list of (old, new, ref) triples: the existing repo has the objects and the existing ref (if any) points to the object (usually commit, sometimes tag) whose SHA-1 is old. Git is proposing to change it to point to the object whose SHA-1 is new (or create it or delete it if exactly one of those two is the all-zeros "null SHA-1").

the only way I've found to inspect file contents is diffing those references

That's one way, but you have the entire suite of git commands to extract everything. You can, for instance, make a new, empty directory somewhere (mkdir path) and run git --work-tree=path checkout sha1 to get a complete tree into that path. (If the tree is large this could take some time. Of course any tests you run on it will take even more time.)

You must decide what, precisely, you want to check. This is as complicated as you want it to be, but for branch names (a ref of the form refs/heads/name, where name is any branch name, i.e., may contain more slashes), consider that the ref update may do one or more of the following (some combinations are obviously impossible):

  • Add a new branch name
  • Add new commits to an existing branch
  • Remove commits from an existing branch
  • Remove an existing branch name

For instance, if I have a clone of a bare repo origin and I do this:

git fetch origin                      # get up-to-date with origin
git checkout -b branch origin/branch  # make tracking branch for origin/branch
git reset --hard HEAD~3               # back up 3 commits
echo more stuff >> existing_file      # modify something
git commit -a -m 'add new text'       # commit the change
git revert --no-edit HEAD             # add another commit that undoes change
git push -f origin branch             # and push

then the update will remove three commits and add two more. The tree you would get if you checked out the new SHA-1 (again, using the old, new, ref triple notation) would look exactly like the version I have asked to push. If there is some test that trees must pass, presumably the version that was HEAD~3 did in fact pass those tests, so this version would too. However, the thing I committed that added one line to existing_file might not pass the tests, and you might not like the fact that I removed three commits.

So, again, it's up to you to decide what you want to check, and write code to achieve that. Check whether a force-push is removing commits; allow or forbid this. Check whether a new branch name is being created; allow or forbid. Check whether a branch name is being deleted; allow or forbid. If commits are being added, check every intermediate commit's tree, or check only the final tree; allow or forbid. Do commits being added involve merges? Are tags being added, removed, or changed? And so on.

Just for fun, a while ago I wrote a pre-receive shell script (in POSIX-style shell) that does many of these (it does not check the contents of any commit). I did some very light testing and it seems to work. It could be used as a starting-point for more thorough checking.

If you're doing serious checking, though, you might want to look into using gitolite.

torek
  • 448,244
  • 59
  • 642
  • 775
  • This is a great answer @torek. Can you clarify one thing, near the end where you recommend to check (for example) if a force-push is removing commits, how would one check that? Do you count the number of ancestors of both `old` and `new`? – Segfault Aug 25 '14 at 18:35
  • @Segfault: The quick and easy way to see if `$new` discards commits that `$old` kept reachable is to use `git rev-list $new..$old` (add `--count` to get just a count, rather than an actual list of such revs). This might be clearer if you spell it `git rev-list $old ^$new`: commits reachable from `$old` that are not reachable from `$new`. – torek Aug 25 '14 at 18:43