gitolite/doc/virtual-refs.mkd

223 lines
8.8 KiB
Markdown
Raw Normal View History

2012-02-25 15:29:49 +01:00
# virtual refs
The traditional method of adding additional checks to gitolite was to use [hook
chaining][hookchaining], which basically means you put your code in a new
hook called `update.secondary`.
But this is not ideal -- it runs for all repos and all users, which you may
not want.
Here's a teaser example for a better way. Copy `contrib/VREF/gl-VREF-COUNT`
to `src`, install/upgrade gitolite, then add this to your admin conf:
repo r1
RW+ = lead_dev dev2 dev3
- VREF/COUNT/9 = dev2 dev3
- VREF/COUNT/3/NEWFILES = dev2 dev3
Now dev2 and dev3 cannot push changes that affect more than 9 files at a time,
nor those that have more than 3 new files. It doesn't affect any other repo,
nor does it affect the lead developer.
----
[[TOC]]
----
## rule matching recap
You won't get any joy out of this if you don't understand at least
[refex][]es, [deny][] rules, and [NAME][]-based restrictions.
And here's an important **warning**. Recall the "summary" from [this][aac]
document:
> The first matching refex that has the permission you're looking for (`W`
> or `+`) **or a minus (`-`)**, results in success **or failure,
> respectively**. A fallthrough **also** results in failure.
Note the last sentence in that summary. **That sentence does NOT apply** to
virtual refs; a fallthru results in success here. You'll see why this is more
convenient as you read on.
----
## what is a virtual ref
A ref like `refs/heads/master` is the main property of a push that gitolite
uses to make its yes/no decision. I call this a "real" ref.
Any *other* property of the push that you want to use to help in the decision
is therefore a *virtual* ref. This could be a property that git knows about,
like in the example above, or comes from outside git like, say, the current
time; see examples section later for some ideas.
(To be honest, [NAME][]-based restrictions should also be called virtual refs,
but they've been in gitolite forever so they're grandfathered in, and the
material in this document does not apply to them).
## fallthru is success here
Notice that you didn't need to add an `RW+ VREF/...` rule for user `lead_dev`
in our example. This is the opposite of what we do in a similar situation
[here][NAME]. This section explains why.
**Virtual refs are best used as additional "deny" rules**, using extra checks
that core gitolite cannot perform.
Making fallthru be a "fail" forces you to add rules for all users, instead of
just the ones who should have those extra checks. Worse, since every virtual
ref involves calling an external program, many of these calls may be wasted.
(Yes, I should have used this logic for [NAME][] also. What can I say -- I am
older and wiser now. Sadly, we can't change [NAME][] without breaking a lot
of existing configs, so it stays like a real ref -- fallthru is failure).
## how it works -- overview
Briefly, a refex starting with `VREF/FOO` triggers a call to a program called
`gl-VREF-FOO` in `$GL_BINDIR`.
That program is expected to print zero or more lines to its STDOUT; each line
is taken by gitolite as a new "ref" to be matched against all the refexes for
this user in the config. Including, the refex that caused the vref call, of
course.
Normally, you send back the refex itself, if the test determines that the rule
should be matched, otherwise nothing. So, in our example, we print
`VREF/COUNT/9` if the count was indeed greater than 9. Otherwise we just
exit.
## how it works -- details
* the VREF code is only called if there are any VREF rules for the user,
which means when the lead developer pushes, the VREF is not called at all.
* when dev2 or dev3 push, gitolite first checks the real ref
(`ref/heads/master` or whatever), then any [NAME][] rules. So far this is
normal processing.
After this it looks at VREF rules, and calls an external program for every
one it finds. Specifically, in a line like
- VREF/COUNT/3/NEWFILES = user
COUNT is the vref name, so the program called is
`$GL_BINDIR/gl-VREF-COUNT`.
The program is passed **nine arguments** in this case (see next section
for details).
* the script can print anything it wants to STDOUT; the first word in each
such line will be treated as a virtual ref to be matched against all the
rules, while the rest, if any, is a message to be added to the standard
"...DENIED..." message that gitolite prints if that refex matches.
Usually it only makes sense to either
* print nothing -- if you don't want the rule that triggered it to match
(ie., whatever condition being tested was not violated; like if the
count of changed files did not exceed 9, in our earlier example)
* print the refex itself (plus an optional message), so that it matches
the line which invoked it
### arguments passed to the vref code
* arguments **1, 2, 3**: the 'ref', 'oldsha', and 'newsha' that git passed
to the update hook (see 'man githooks')
* 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 `W` or `+`, but
could also be `C`, `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; in our example
`VREF/COUNT/3/NEWFILES`.
* 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 `NEWFILES`.
Yes, argument 7 is redundant if you have 8 and 9. It's meant to make it easy
to write vref scripts in any language. See script examples in source.
## what (else) can the vref code pass back
Actually, the vref code can pass anything back; each line in its output will
be matched against all the rules as usual (with the exception that fallthru is
not failure).
For example, you could have a ruleset like this:
repo r1
# ... normal rules ...
- VREF/TIME/WEEKEND = @interns
- VREF/TIME/WEEKNIGHT = @interns
- VREF/TIME/HOLIDAY = @interns
and you could write the TIME vref code (gl-VREF-TIME) to passback any or all
of the times that match. Then if an intern tried to access the system, each
rule would trigger a call to gl-VREF-TIME.
The script should send back any of the applicable times (even more than one,
or none at all, as the case may be). So even if it was invoked using the
first rule, it might pass back (to gitolite) a virtual ref saying
'VREF/TIME/HOLIDAY', which would promptly cause the request to be denied.
## other possible examples
I use these. Don't analyse the numbers -- I fully expect to tune them as time
passes; the idea is the main thing.
* if a dev pushes more than 2 *new* files, the top commit needs to have a
signed-off by line in its commit message. For example if he has 4 new
files this text should be:
4 new files signed-off by: <top commit author's email>
The config entry for this is this (NO_SIGNOFF applies only to, and thus
implies, NEWFILES). This applies to everyone except me ;-)
RW+ VREF/COUNT/2/NO_SIGNOFF = sitaram
- VREF/COUNT/2/NO_SIGNOFF = @all
Notice how the refex in both cases is *exactly* the same. If you make it
different (even change the number on my access line), things won't work.
* junior devs can't push more than 10 new files, even with a signed-off by
line:
- VREF/COUNT/10/NEWFILES = @junior_devs
* we also need to catch auto-generated files that have filename extensions
that cannot be ".ignore"
- VREF/FILETYPE/AUTOGENERATED = @all
Here are some more ideas:
* number of commits (`git rev-list --count $old $new`)
* number of binary files in commit (currently I only know to count
occurrences of ` Bin ` in the output of `git diff --stat`
* number of *new* binary files (count ` Bin 0 ->` in `git diff --stat`
output)
* time of day/day of week (see example snippet somewhere above)
* IP address
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.