Merge branch 'rg/no-gecos-in-pwent'
[git] / Documentation / howto / update-hook-example.txt
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
9 When your developer runs git-push into the repository,
10 git-receive-pack is run (either locally or over ssh) as that
11 developer, so is hooks/update script.  Quoting from the relevant
12 section of the documentation:
13
14     Before each ref is updated, if $GIT_DIR/hooks/update file exists
15     and executable, it is called with three parameters:
16
17            $GIT_DIR/hooks/update refname sha1-old sha1-new
18
19     The refname parameter is relative to $GIT_DIR; e.g. for the
20     master head this is "refs/heads/master".  Two sha1 are the
21     object names for the refname before and after the update.  Note
22     that the hook is called before the refname is updated, so either
23     sha1-old is 0{40} (meaning there is no such ref yet), or it
24     should match what is recorded in refname.
25
26 So if your policy is (1) always require fast-forward push
27 (i.e. never allow "git-push repo +branch:branch"), (2) you
28 have a list of users allowed to update each branch, and (3) you
29 do not let tags to be overwritten, then you can use something
30 like this as your hooks/update script.
31
32 [jc: editorial note.  This is a much improved version by Carl
33 since I posted the original outline]
34
35 -- >8 -- beginning of script -- >8 --
36
37 #!/bin/bash
38
39 umask 002
40
41 # If you are having trouble with this access control hook script
42 # you can try setting this to true.  It will tell you exactly
43 # why a user is being allowed/denied access.
44
45 verbose=false
46
47 # Default shell globbing messes things up downstream
48 GLOBIGNORE=*
49
50 function grant {
51   $verbose && echo >&2 "-Grant-         $1"
52   echo grant
53   exit 0
54 }
55
56 function deny {
57   $verbose && echo >&2 "-Deny-          $1"
58   echo deny
59   exit 1
60 }
61
62 function info {
63   $verbose && echo >&2 "-Info-          $1"
64 }
65
66 # Implement generic branch and tag policies.
67 # - Tags should not be updated once created.
68 # - Branches should only be fast-forwarded unless their pattern starts with '+'
69 case "$1" in
70   refs/tags/*)
71     git rev-parse --verify -q "$1" &&
72     deny >/dev/null "You can't overwrite an existing tag"
73     ;;
74   refs/heads/*)
75     # No rebasing or rewinding
76     if expr "$2" : '0*$' >/dev/null; then
77       info "The branch '$1' is new..."
78     else
79       # updating -- make sure it is a fast-forward
80       mb=$(git-merge-base "$2" "$3")
81       case "$mb,$2" in
82         "$2,$mb") info "Update is fast-forward" ;;
83         *)        noff=y; info "This is not a fast-forward update.";;
84       esac
85     fi
86     ;;
87   *)
88     deny >/dev/null \
89     "Branch is not under refs/heads or refs/tags.  What are you trying to do?"
90     ;;
91 esac
92
93 # Implement per-branch controls based on username
94 allowed_users_file=$GIT_DIR/info/allowed-users
95 username=$(id -u -n)
96 info "The user is: '$username'"
97
98 if test -f "$allowed_users_file"
99 then
100   rc=$(cat $allowed_users_file | grep -v '^#' | grep -v '^$' |
101     while read heads user_patterns
102     do
103       # does this rule apply to us?
104       head_pattern=${heads#+}
105       matchlen=$(expr "$1" : "${head_pattern#+}")
106       test "$matchlen" = ${#1} || continue
107
108       # if non-ff, $heads must be with the '+' prefix
109       test -n "$noff" &&
110       test "$head_pattern" = "$heads" && continue
111
112       info "Found matching head pattern: '$head_pattern'"
113       for user_pattern in $user_patterns; do
114         info "Checking user: '$username' against pattern: '$user_pattern'"
115         matchlen=$(expr "$username" : "$user_pattern")
116         if test "$matchlen" = "${#username}"
117         then
118           grant "Allowing user: '$username' with pattern: '$user_pattern'"
119         fi
120       done
121       deny "The user is not in the access list for this branch"
122     done
123   )
124   case "$rc" in
125     grant) grant >/dev/null "Granting access based on $allowed_users_file" ;;
126     deny)  deny  >/dev/null "Denying  access based on $allowed_users_file" ;;
127     *) ;;
128   esac
129 fi
130
131 allowed_groups_file=$GIT_DIR/info/allowed-groups
132 groups=$(id -G -n)
133 info "The user belongs to the following groups:"
134 info "'$groups'"
135
136 if test -f "$allowed_groups_file"
137 then
138   rc=$(cat $allowed_groups_file | grep -v '^#' | grep -v '^$' |
139     while read heads group_patterns
140     do
141       # does this rule apply to us?
142       head_pattern=${heads#+}
143       matchlen=$(expr "$1" : "${head_pattern#+}")
144       test "$matchlen" = ${#1} || continue
145
146       # if non-ff, $heads must be with the '+' prefix
147       test -n "$noff" &&
148       test "$head_pattern" = "$heads" && continue
149
150       info "Found matching head pattern: '$head_pattern'"
151       for group_pattern in $group_patterns; do
152         for groupname in $groups; do
153           info "Checking group: '$groupname' against pattern: '$group_pattern'"
154           matchlen=$(expr "$groupname" : "$group_pattern")
155           if test "$matchlen" = "${#groupname}"
156           then
157             grant "Allowing group: '$groupname' with pattern: '$group_pattern'"
158           fi
159         done
160       done
161       deny "None of the user's groups are in the access list for this branch"
162     done
163   )
164   case "$rc" in
165     grant) grant >/dev/null "Granting access based on $allowed_groups_file" ;;
166     deny)  deny  >/dev/null "Denying  access based on $allowed_groups_file" ;;
167     *) ;;
168   esac
169 fi
170
171 deny >/dev/null "There are no more rules to check.  Denying access"
172
173 -- >8 -- end of script -- >8 --
174
175 This uses two files, $GIT_DIR/info/allowed-users and
176 allowed-groups, to describe which heads can be pushed into by
177 whom.  The format of each file would look like this:
178
179         refs/heads/master       junio
180         +refs/heads/pu          junio
181         refs/heads/cogito$      pasky
182         refs/heads/bw/.*        linus
183         refs/heads/tmp/.*       .*
184         refs/tags/v[0-9].*      junio
185
186 With this, Linus can push or create "bw/penguin" or "bw/zebra"
187 or "bw/panda" branches, Pasky can do only "cogito", and JC can
188 do master and pu branches and make versioned tags.  And anybody
189 can do tmp/blah branches. The '+' sign at the pu record means
190 that JC can make non-fast-forward pushes on it.
191
192 ------------