You might be interested in the update hook that Junio โโwrote and that Carl has improved. Put the code below in $GIT_DIR/hooks/update and don't forget to enable it with chmod +x .
#!/bin/bash umask 002 # If you are having trouble with this access control hook script # you can try setting this to true. It will tell you exactly # why a user is being allowed/denied access. verbose=false # Default shell globbing messes things up downstream GLOBIGNORE=* function grant { $verbose && echo >&2 "-Grant- $1" echo grant exit 0 } function deny { $verbose && echo >&2 "-Deny- $1" echo deny exit 1 } function info { $verbose && echo >&2 "-Info- $1" } # Implement generic branch and tag policies. # - Tags should not be updated once created. # - Branches should only be fast-forwarded unless their pattern starts with '+' case "$1" in refs/tags/*) git rev-parse --verify -q "$1" && deny >/dev/null "You can't overwrite an existing tag" ;; refs/heads/*) # No rebasing or rewinding if expr "$2" : '0*$' >/dev/null; then info "The branch '$1' is new..." else # updating -- make sure it is a fast-forward mb=$(git-merge-base "$2" "$3") case "$mb,$2" in "$2,$mb") info "Update is fast-forward" ;; *) noff=y; info "This is not a fast-forward update.";; esac fi ;; *) deny >/dev/null \ "Branch is not under refs/heads or refs/tags. What are you trying to do?" ;; esac # Implement per-branch controls based on username allowed_users_file=$GIT_DIR/info/allowed-users username=$(id -u -n) info "The user is: '$username'" if test -f "$allowed_users_file" then rc=$(cat $allowed_users_file | grep -v '^#' | grep -v '^$' | while read heads user_patterns do # does this rule apply to us? head_pattern=${heads#+} matchlen=$(expr "$1" : "${head_pattern#+}") test "$matchlen" = ${#1} || continue # if non-ff, $heads must be with the '+' prefix test -n "$noff" && test "$head_pattern" = "$heads" && continue info "Found matching head pattern: '$head_pattern'" for user_pattern in $user_patterns; do info "Checking user: '$username' against pattern: '$user_pattern'" matchlen=$(expr "$username" : "$user_pattern") if test "$matchlen" = "${#username}" then grant "Allowing user: '$username' with pattern: '$user_pattern'" fi done deny "The user is not in the access list for this branch" done ) case "$rc" in grant) grant >/dev/null "Granting access based on $allowed_users_file" ;; deny) deny >/dev/null "Denying access based on $allowed_users_file" ;; *) ;; esac fi allowed_groups_file=$GIT_DIR/info/allowed-groups groups=$(id -G -n) info "The user belongs to the following groups:" info "'$groups'" if test -f "$allowed_groups_file" then rc=$(cat $allowed_groups_file | grep -v '^#' | grep -v '^$' | while read heads group_patterns do # does this rule apply to us? head_pattern=${heads#+} matchlen=$(expr "$1" : "${head_pattern#+}") test "$matchlen" = ${#1} || continue # if non-ff, $heads must be with the '+' prefix test -n "$noff" && test "$head_pattern" = "$heads" && continue info "Found matching head pattern: '$head_pattern'" for group_pattern in $group_patterns; do for groupname in $groups; do info "Checking group: '$groupname' against pattern: '$group_pattern'" matchlen=$(expr "$groupname" : "$group_pattern") if test "$matchlen" = "${#groupname}" then grant "Allowing group: '$groupname' with pattern: '$group_pattern'" fi done done deny "None of the user groups are in the access list for this branch" done ) case "$rc" in grant) grant >/dev/null "Granting access based on $allowed_groups_file" ;; deny) deny >/dev/null "Denying access based on $allowed_groups_file" ;; *) ;; esac fi deny >/dev/null "There are no more rules to check. Denying access"
With this hook, you then give individual users or groups the opportunity to make changes to the repository. Anyone who can see this has read-only access.
Two files $GIT_DIR/info/allowed-users and allowed-groups are used here to describe which heads can be pressed. The format of each file will look like this:
refs/heads/master junio +refs/heads/pu junio refs/heads/cogito$ pasky refs/heads/bw/.* linus refs/heads/tmp/.* .* refs/tags/v[0-9].* junio
Linus can create or create bw/penguin or bw/zebra or bw/panda branches, Pasky can only execute cogito , and JC can execute master and pu branches and make tags with the version. And anyone can do tmp/blah branches. The "+" sign in the pu entry means that JC can click on it without acceleration.
If this person does not yet have access to the host where your repository lives, perhaps this person should have only git-shell access, and not unlimited access. Create a special git user and in ~git/.ssh/authorized_keys , add an external SSH key to the following form. Note that the key should be on one long line, but I wrapped it below to help in the presentation.
no-agent-forwarding, no-port-forwarding, no-pty, no-X11-forwarding,
command = "env myorg_git_user = joeuser / usr / local / bin / git-shell -c
\ "$ {SSH_ORIGINAL_COMMAND: -} \" "ssh-rsa AAAAB3 ... 2iQ == joeuser@foo.invalid Depending on your local setup, you may need to configure the path to git-shell . Remember that sshd very paranoid about permissions of the .ssh , so disable its group write bits and all files under it.
Enabling all git users means that you must be able to distinguish between people, and that is the purpose of the myorg_git_user environment myorg_git_user . Instead of relying on unconditional username=$(id -u -n) , adjust your update hook to use it:
# Implement per-branch controls based on username allowed_users_file=$GIT_DIR/info/allowed-users if [ -z "$myorg_git_user" ]; then username=$(id -u -n) else username=$myorg_git_user fi info "The user is: '$username'"
With this setting, your read-only friend will clone with a command like the one below. The exact path will depend on your setup. To make a good path, move the repository to the git userโs home directory or create a symlink pointing to it.
$ git clone git@blankman.com.invalid: coolproject.git
but will not be able to make updates.
$ git push origin mybranch
Total 0 (delta 0), reused 0 (delta 0)
remote: error: hook declined to update refs / heads / mybranch
To git@blankman.com.invalid: coolproject.git
! [remote rejected] mybranch -> mybranch (hook declined)
error: failed to push some refs to 'git@blankman.com.invalid: coolproject.git'
You said that you work in a team environment, so I assume that your central repository was created using the --shared option. (See core.sharedRepository in the git config documentation and --shared in git init .) Make sure the new git user is a member of the system group that gives you access to your central repository.