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