Merge branch 'jk/filter-branch-require-clean-work-tree'
[git/jnareb-git.git] / Documentation / howto / update-hook-example.txt
blobb7f8d416d65ca1cf17d6348e858a13f6ed72c7fe
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.
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:
14     Before each ref is updated, if $GIT_DIR/hooks/update file exists
15     and executable, it is called with three parameters:
17            $GIT_DIR/hooks/update refname sha1-old sha1-new
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.
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.
32 [jc: editorial note.  This is a much improved version by Carl
33 since I posted the original outline]
35 -- >8 -- beginning of script -- >8 --
37 #!/bin/bash
39 umask 002
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.
45 verbose=false
47 # Default shell globbing messes things up downstream
48 GLOBIGNORE=*
50 function grant {
51   $verbose && echo >&2 "-Grant-         $1"
52   echo grant
53   exit 0
56 function deny {
57   $verbose && echo >&2 "-Deny-          $1"
58   echo deny
59   exit 1
62 function info {
63   $verbose && echo >&2 "-Info-          $1"
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
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'"
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
108       # if non-ff, $heads must be with the '+' prefix
109       test -n "$noff" &&
110       test "$head_pattern" = "$heads" && continue
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
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'"
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
146       # if non-ff, $heads must be with the '+' prefix
147       test -n "$noff" &&
148       test "$head_pattern" = "$heads" && continue
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
171 deny >/dev/null "There are no more rules to check.  Denying access"
173 -- >8 -- end of script -- >8 --
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:
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
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.
192 ------------