IMPORTANT: fallthru is success in VREFs, unlike the normal refs. That won't make sense until you read further, but I had to put it up here for folks who stop reading halfway!


VREFs are a mechanism to add additional constraints to a push.

1 quick intro/example

Here's an example to start you off.

To disallow junior developers from changing more than five files, or from touching the Makefile, you can do this:

repo foo
    RW+                     =   @all-devs

    -   VREF/COUNT/5        =   @junior-devs
    -   VREF/NAME/Makefile  =   @junior-devs

Here's a pictorial representation of what happens, at a very high level, based on the VREF/COUNT/5 rule in the rule list above.

  1. To start with, git-receive-pack calls the update hook with what we will call a "real" ref, something like "refs/heads/master", or "refs/tags/v1.0" -- in general, something that starts with "refs/".

    This ref is sent through "check #2" (please click to refresh your memory if needed).

    Any rules that specify a refex starting with "VREF/" clearly won't match and are ignored in this check.

  2. Assuming that check did not fail, the gitolite code in the update hook then starts looking at each "VREF" rule in the rule list that applies to that repo accessed by that user. For each one, it runs the actual VREF program whose name is given in the word after "VREF/". (The rest of the words, if any, along with all sorts of other information, are supplied as arguments).

  3. The STDOUT of the called program is captured and any line that starts with the characters "VREF/" is taken as a "virtual" ref, and is run through the same "check #2". The only difference is that, unlike for a regular ref, fallthru does not result in failure, but success.

2 basic use and understanding

Normally, rules deal with branches and tags (which git collectively calls "refs"). The "ref" is a property of the push which gitolite checks against the set of rules.

"Virtual refs" are other properties of a push that gitolite can be told to check, in addition to the normal ref. For example, "this push has more than 5 changed files" could be one property. Or "this push changed the file called Makefile" could be another. These properties are represented as "virtual refs" that start with "VREF/". (Recall that "normal" refs start with "refs/").

The simplest way to use them is as additional "deny" rules to fail a push that might otherwise have passed. This is what the example at the top shows.

It helps to separate VREF rules from normal rules, since no access rule can match both a normal ref and a virtual ref. Here's a good way to structure your rules:

3 advanced use

More complex uses are possible, but may be harder to understand. You may want to experiment with the rules to solidify your understanding as you read this.

3.1 differences from normal refs

We know where normal refs (like "refs/heads/master" or "refs/tags/v1.0") come from -- they are supplied by git itself when it calls the update hook.

Virtual refs have two differences with normal refs:

Here's some more detail on how it works.

3.2 generating virtual refs

Gitolite uses the VREF rules themselves to help it generate the virtual refs.

Specifically, it looks at each rule that contains a VREF (there are 2 in the above example) and calls a VREF-maker for each of them.

We'll take the COUNT example rule above.

When gitolite sees that rule, it calls the "COUNT" VREF-maker. Specifically, this is the VREF/COUNT program (See here for actual locations on disk).

Gitolite passes it the string "5" as an argument (actually, as the eighth argument; details later).

The program (which can be written in any language) is expected to do one of two things:

It should exit with an exit code of zero in either case.

If it exits with a non-zero, the push dies regardless of what is printed (see "mimicking a plain old update hook" for why this is useful).

4 more details and nuances

4.1 mimicking a plain old update hook

If the VREF maker exists with a non-zero exit code, then regardless of what it prints or does not, the push dies.

This is just like a plain 'update' hook. Since the first 3 arguments (see later) are also the same that a plain 'update' hook receives, you can actually use any existing update hook as a VREF-maker.

To repurpose an existing update hook as a VREF-maker, just copy it to the VREF directory (again, see here for actual locations on disk). Then add this rule to your repos:

repo foo    # or maybe even 'repo @all'
    -   VREF/my-update-hook     =   @all

That's it.

4.2 what if the VREF-maker prints a different virtual ref?

Unless you know what you're upto, don't do that.

But it's allowed and the behaviour is defined. The VREF-maker for the NAME VREF is a good example. It ignores the arguments and just makes VREFs out of the name of every file that was changed in the push.

Here's another example. Consider the problem of not allowing pushes at specific times. Let's say repo 'foo' cannot be pushed between 4 and 7pm, and repo 'bar' can only be pushed before 9am. And of course all this only applies to the junior developers, the poor guys!

In this example, we write the "Hour" VREF-maker to ignore the argument passed and just print VREF/Hour/NN where NN can be between 00 to 23 inclusive and of course represents the current hour.

If foo is pushed at 6:30pm, the VREF-maker prints VREF/Hour/18, which satisfies the third rule and is rejected.

If bar is pushed at, say, 7:20am, the vref printed is VREF/Hour/07, which does not match any of the rules. And fallthru is success so it passes.

repo foo
    RW+                         =   @all

    -   VREF/Hour/16            =   @junior-devs
    -   VREF/Hour/17            =   @junior-devs
    -   VREF/Hour/18            =   @junior-devs

repo bar
    RW+                         =   @all

    -   VREF/Hour/09            =   @junior-devs
    -   VREF/Hour/1[0-9]        =   @junior-devs
    -   VREF/Hour/2[0-9]        =   @junior-devs

4.3 why is fallthru considered success with VREFs

Virtual refs are best used (1) as additional "deny" rules, performing extra checks that core gitolite cannot. You usually want such extra checks only for some people.

When fallthru is success, you can simply ignore all the other users (for whom such additional checks are not needed).

If fallthru were to be considered 'failure', you'd be forced to add a "success rule" like this for every virtual ref you used in this repo, in each case listing every user who was not already mentioned in the context of that vref:

RW+ VREF/VREFNAME   =   @userlist   # uggh! what a pain!

Worse, since every virtual ref involves calling an external program, many of these calls may be wasted.

(1) "best used as..." does not mean "only used as...". For example it's perfectly easy to turn this around if, instead of having a list of people who do need extra checks, all you have is the complementary list:

RW+ VREF/NAME/Makefile      =   @senior-devs
-   VREF/NAME/Makefile      =   @all

4.4 what if the VREF-maker prints something that's not even a virtual ref?

The VREF-maker can print anything it wants to STDOUT. Lines not starting with VREF/ are printed as is (so your VREF-maker can do mostly-normal printing to STDOUT). This is especially useful if you've turned an existing update hook into a VREF-maker, and it prints stuff meant for the user, but you don't want to touch the code.

For lines starting with VREF/, the first word in each such line will be treated as a virtual ref, while the rest, if any, is a message to be added to the standard "...DENIED..." message that gitolite will print if that refex matches and the rule is a deny rule.

4.5 in what order are VREF-makers called?

VREF-makers are called in the sequence in which they appear in the conf file.

There are some optimisations to prevent calling the same VREF-maker with the same arguments more than once, and the VREF-maker code for the NAME VREF (which is special) is called only once regardless of how many times it appears but these details should not concern anyone but a developer.

4.6 what arguments are passed to the vref-maker?

Yes, argument 7 is redundant if you have 8 and 9. It's meant to make it easy to write vref-maker scripts in any language. See script examples in source.

5 VREF-makers shipped with gitolite

5.1 restricting pushes by dir/file name

The "NAME" VREF allows you to restrict pushes by the names of dirs and files changed. (Side note: the NAME VREF is the only one directly implemented within the update hook, so you won't find it in the VREF directory).

Here's an example. Say you don't want junior developers pushing changes to the Makefile, because it's quite complex:

repo foo
        RW+                             =   @senior_devs
        RW                              =   @junior_devs

        -   VREF/NAME/Makefile          =   @junior_devs

When a senior dev pushes, the VREF is not invoked at all. But when a junior dev pushes, the VREF is invoked, and it returns a list of files changed as virtual refs, looking like this:

VREF/NAME/file-1
VREF/NAME/dir-2/file-3
...etc...

Each of these refs is matched against the access rules. If one of them happens to be the Makefile, then the ref returned (VREF/NAME/Makefile) will match the deny rule and kill the push.

Another way to use this is when you know what is allowed instead of what is not allowed. Let's say the QA person is only allowed to touch a file called CHANGELOG and any files in a directory called ReleaseNotes:

repo foo
        RW+                             =   @senior_devs
        RW                              =   @junior_devs
        RW+                             =   QA-guy

        RW+ VREF/NAME/CHANGELOG         =   QA-guy
        RW+ VREF/NAME/ReleaseNotes/     =   QA-guy
        -   VREF/NAME/                  =   QA-guy

5.2 number of changed or new files

The COUNT VREF is used like this:

-   VREF/COUNT/9                    =   @junior-developers

In response, if anyone in the user list pushes a commit series that changes more than 9 files, a virtual ref of "VREF/COUNT/9" is returned. Gitolite uses that as a "ref" to match against all the rules, hits the same rule that invoked it, and denies the request.

If the user did not push more than 9 files, the VREF code returns nothing, and nothing happens.

COUNT can take one more argument:

-   VREF/COUNT/9/NEWFILES           =   @junior-developers

This is the same as before, but have to be more than 9 new files not just changed files.

5.3 advanced filetype detection

Note: this is more for illustration than use; it's rather specific to one of the projects I manage but the idea is the important thing.

Sometimes a file has a standard extension (that cannot be 'gitignore'd), but it is actually automatically generated. Here's one way to catch it:

 -   VREF/FILETYPE/AUTOGENERATED     =   @all

You can look at src/VREF/FILETYPE to see how it handles the 'AUTOGENERATED' option. You could also have a more generic option, like perhaps BINARY, and handle that in the FILETYPE vref too.

5.4 checking author email

Some people want to ensure that "you can only push your own commits".

If you force it on everyone, this is a very silly idea (see "Philosophical Notes" section of src/VREF/EMAIL-CHECK).

But there may be value in enforcing it just for the junior developers.

The neat thing is that the existing contrib/update.email-check was just copied to src/VREF/EMAIL-CHECK and it works, because VREFs get the same first 3 arguments and those are all that it cares about. (Note: you have to change one subroutine in that script if you want to use it)

5.5 voting on commits

Although gitolite can't/won't do the whole "code review + workflow enforcement" thing that Gerrit Code Review does, a basic implementation of voting on a commit is surprisingly easy. See src/VREF/VOTES for details (and note that the actual code is just 2-3 lines; the rest is inline documentation).

6 other ideas -- code welcome!

6.1 "no non-merge first-parents"

Shruggar on #gitolite wanted this. Possible code to implement it would be something like this (untested)

[ -z "$(git rev-list --first-parent --no-merges $2..$3)" ]

This can be implemented using src/VREF/MERGE-CHECK as a model. That script does what the 'M' qualifier does in access rules (see last part of this), although the syntax to be used in conf/gitolite will be quite different.

6.2 other ideas for VREFs

Here are some more ideas:

Note that pretty much anything that involves $oldsha..$newsha will have to deal with the issue that when you push a new tag or branch, the "old" part is all 0's, and unless you consider --all existing branches and tags it becomes meaningless in terms of "number of new files" etc.