1 From: Junio C Hamano <gitster@pobox.com> and Carl Baldwin <cnb@fc.hp.com>
2 Subject: control access to branches.
3 Date: Thu, 17 Nov 2005 23:55:32 -0800
4 Message-ID: <7vfypumlu3.fsf@assigned-by-dhcp.cox.net>
5 Abstract: An example hooks/update script is presented to
6 implement repository maintenance policies, such as who can push
7 into which branch and who can make a tag.
8 Content-type: text/asciidoc
10 How to use the update hook
11 ==========================
13 When your developer runs git-push into the repository,
14 git-receive-pack is run (either locally or over ssh) as that
15 developer, so is hooks/update script. Quoting from the relevant
16 section of the documentation:
18 Before each ref is updated, if $GIT_DIR/hooks/update file exists
19 and executable, it is called with three parameters:
21 $GIT_DIR/hooks/update refname sha1-old sha1-new
23 The refname parameter is relative to $GIT_DIR; e.g. for the
24 master head this is "refs/heads/master". Two sha1 are the
25 object names for the refname before and after the update. Note
26 that the hook is called before the refname is updated, so either
27 sha1-old is 0{40} (meaning there is no such ref yet), or it
28 should match what is recorded in refname.
30 So if your policy is (1) always require fast-forward push
31 (i.e. never allow "git-push repo +branch:branch"), (2) you
32 have a list of users allowed to update each branch, and (3) you
33 do not let tags to be overwritten, then you can use something
34 like this as your hooks/update script.
36 [jc: editorial note. This is a much improved version by Carl
37 since I posted the original outline]
39 ----------------------------------------------------
44 # If you are having trouble with this access control hook script
45 # you can try setting this to true. It will tell you exactly
46 # why a user is being allowed/denied access.
50 # Default shell globbing messes things up downstream
54 $verbose && echo >&2 "-Grant- $1"
60 $verbose && echo >&2 "-Deny- $1"
66 $verbose && echo >&2 "-Info- $1"
69 # Implement generic branch and tag policies.
70 # - Tags should not be updated once created.
71 # - Branches should only be fast-forwarded unless their pattern starts with '+'
74 git rev-parse --verify -q "$1" &&
75 deny >/dev/null "You can't overwrite an existing tag"
78 # No rebasing or rewinding
79 if expr "$2" : '0*$' >/dev/null; then
80 info "The branch '$1' is new..."
82 # updating -- make sure it is a fast-forward
83 mb=$(git merge-base "$2" "$3")
85 "$2,$mb") info "Update is fast-forward" ;;
86 *) noff=y; info "This is not a fast-forward update.";;
92 "Branch is not under refs/heads or refs/tags. What are you trying to do?"
96 # Implement per-branch controls based on username
97 allowed_users_file=$GIT_DIR/info/allowed-users
99 info "The user is: '$username'"
101 if test -f "$allowed_users_file"
103 rc=$(cat $allowed_users_file | grep -v '^#' | grep -v '^$' |
104 while read heads user_patterns
106 # does this rule apply to us?
107 head_pattern=${heads#+}
108 matchlen=$(expr "$1" : "${head_pattern#+}")
109 test "$matchlen" = ${#1} || continue
111 # if non-ff, $heads must be with the '+' prefix
113 test "$head_pattern" = "$heads" && continue
115 info "Found matching head pattern: '$head_pattern'"
116 for user_pattern in $user_patterns; do
117 info "Checking user: '$username' against pattern: '$user_pattern'"
118 matchlen=$(expr "$username" : "$user_pattern")
119 if test "$matchlen" = "${#username}"
121 grant "Allowing user: '$username' with pattern: '$user_pattern'"
124 deny "The user is not in the access list for this branch"
128 grant) grant >/dev/null "Granting access based on $allowed_users_file" ;;
129 deny) deny >/dev/null "Denying access based on $allowed_users_file" ;;
134 allowed_groups_file=$GIT_DIR/info/allowed-groups
136 info "The user belongs to the following groups:"
139 if test -f "$allowed_groups_file"
141 rc=$(cat $allowed_groups_file | grep -v '^#' | grep -v '^$' |
142 while read heads group_patterns
144 # does this rule apply to us?
145 head_pattern=${heads#+}
146 matchlen=$(expr "$1" : "${head_pattern#+}")
147 test "$matchlen" = ${#1} || continue
149 # if non-ff, $heads must be with the '+' prefix
151 test "$head_pattern" = "$heads" && continue
153 info "Found matching head pattern: '$head_pattern'"
154 for group_pattern in $group_patterns; do
155 for groupname in $groups; do
156 info "Checking group: '$groupname' against pattern: '$group_pattern'"
157 matchlen=$(expr "$groupname" : "$group_pattern")
158 if test "$matchlen" = "${#groupname}"
160 grant "Allowing group: '$groupname' with pattern: '$group_pattern'"
164 deny "None of the user's groups are in the access list for this branch"
168 grant) grant >/dev/null "Granting access based on $allowed_groups_file" ;;
169 deny) deny >/dev/null "Denying access based on $allowed_groups_file" ;;
174 deny >/dev/null "There are no more rules to check. Denying access"
175 ----------------------------------------------------
177 This uses two files, $GIT_DIR/info/allowed-users and
178 allowed-groups, to describe which heads can be pushed into by
179 whom. The format of each file would look like this:
181 refs/heads/master junio
182 +refs/heads/seen junio
183 refs/heads/cogito$ pasky
184 refs/heads/bw/.* linus
186 refs/tags/v[0-9].* junio
188 With this, Linus can push or create "bw/penguin" or "bw/zebra"
189 or "bw/panda" branches, Pasky can do only "cogito", and JC can
190 do master and "seen" branches and make versioned tags. And anybody
191 can do tmp/blah branches. The '+' sign at the "seen" record means
192 that JC can make non-fast-forward pushes on it.