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.
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
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:
Put your normal ref rules first. These apply to the branch name or tag name that git itself knows about and sends to gitolite's update hook.
Fallthru is failure here, which means the ref being pushed must match some rule in this set for the push to succeed.
Put your VREF rules next. These apply to the virtual refs you want to use as additional checks for the push.
Fallthru is success here, which means the (virtual) ref must be explicitly denied in order for the push to fail.
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.
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.
First, the normal ("real") ref is checked.
As you already know, the push dies if the ref hits a deny rule or it falls through without hitting an allow rule.
Next, virtual refs are generated and checked one by one.
We'll talk about the generaton later, but for the check, a virtual ref kills the push only if it meets an explicit deny rule ("-"); fallthru does not cause failure. Other than that, the checking is done the same way as for a normal ref, viz., as described in the flow for check #2.
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:
If the condition is satisfied (i.e., there are more than 5 files in this push), it should print
VREF/COUNT/5 to STDOUT.
You can see that when this virtual ref is processed through the rules, it will encounter the "deny" rule, and thus kill the push.
Otherwise it should print nothing. That is, there is no virtual ref to run through "check #2", so nothing happens.
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).
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
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
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
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
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.
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.
Arguments 1, 2, 3: the 'ref', 'oldsha', and 'newsha' that git passed to the update hook (see 'man githooks').
This, combined with the fact that non-zero exits are detected, mean that you can simply use an existing update.secondary as a VREF-maker as-is, no changes needed.
Arguments 4 and 5: the 'oldtree' and 'newtree' SHAs. These are the same as the oldsha and newsha values, except if one of them is all-0. (indicating a ref creation or deletion). In that case the corresponding 'tree' SHA is set (by gitolite, as a courtesy) to the special SHA
4b825dc642cb6eb9a060e54bf8d69288fbee4904, which is the hash of an empty tree.
(None of these shenanigans would have been needed if
git diff $oldsha $newsha would not error out when passed an all-0 SHA.)
Argument 6: the attempted access flag. Typically
+, but could also be
D, or any of these 4 followed by
M. If you have to ask what they mean, you haven't read enough gitolite documentation to be able to make virtual refs work.
Argument 7: is the entire refex; say
Arguments 8 onward: are the split out (by
/) portions of the refex, excluding the first two components. In our example they would be
3 followed by
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.
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
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.
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.
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
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)
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).
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.
Here are some more ideas:
git rev-list --count $old $new).
Binin the output of
git diff --stat.
Bin 0 ->in
git diff --statoutput).
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.