Documentation / howto / update-hook-example.txton commit Merge branch 'mc/cd-p-pwd' (0f2d01d)
   1From: Junio C Hamano <gitster@pobox.com> and Carl Baldwin <cnb@fc.hp.com>
   2Subject: control access to branches.
   3Date: Thu, 17 Nov 2005 23:55:32 -0800
   4Message-ID: <7vfypumlu3.fsf@assigned-by-dhcp.cox.net>
   5Abstract: 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
   9When your developer runs git-push into the repository,
  10git-receive-pack is run (either locally or over ssh) as that
  11developer, so is hooks/update script.  Quoting from the relevant
  12section 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
  26So if your policy is (1) always require fast-forward push
  27(i.e. never allow "git-push repo +branch:branch"), (2) you
  28have a list of users allowed to update each branch, and (3) you
  29do not let tags to be overwritten, then you can use something
  30like this as your hooks/update script.
  31
  32[jc: editorial note.  This is a much improved version by Carl
  33since I posted the original outline]
  34
  35-- >8 -- beginning of script -- >8 --
  36
  37#!/bin/bash
  38
  39umask 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
  45verbose=false
  46
  47# Default shell globbing messes things up downstream
  48GLOBIGNORE=*
  49
  50function grant {
  51  $verbose && echo >&2 "-Grant-         $1"
  52  echo grant
  53  exit 0
  54}
  55
  56function deny {
  57  $verbose && echo >&2 "-Deny-          $1"
  58  echo deny
  59  exit 1
  60}
  61
  62function 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 '+'
  69case "$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    ;;
  91esac
  92
  93# Implement per-branch controls based on username
  94allowed_users_file=$GIT_DIR/info/allowed-users
  95username=$(id -u -n)
  96info "The user is: '$username'"
  97
  98if test -f "$allowed_users_file"
  99then
 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
 129fi
 130
 131allowed_groups_file=$GIT_DIR/info/allowed-groups
 132groups=$(id -G -n)
 133info "The user belongs to the following groups:"
 134info "'$groups'"
 135
 136if test -f "$allowed_groups_file"
 137then
 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
 169fi
 170
 171deny >/dev/null "There are no more rules to check.  Denying access"
 172
 173-- >8 -- end of script -- >8 --
 174
 175This uses two files, $GIT_DIR/info/allowed-users and
 176allowed-groups, to describe which heads can be pushed into by
 177whom.  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
 186With this, Linus can push or create "bw/penguin" or "bw/zebra"
 187or "bw/panda" branches, Pasky can do only "cogito", and JC can
 188do master and pu branches and make versioned tags.  And anybody
 189can do tmp/blah branches. The '+' sign at the pu record means
 190that JC can make non-fast-forward pushes on it.
 191
 192------------