Is it possible to have a custom .gitignore? Read only access?

  1. Put your private ignore rules in .git/info/exclude. See gitignore(5).
  2. For read-only access, use git-daemon, a web server, or Gitosis, or Gitolite.

I know I am a little late to the conversation but you might want to consider using

git update-index --assume-unchanged [ FILE ]

As the git help document states:

When the "assume unchanged" bit is on, git stops checking the working tree files for possible modifications, so you need to manually unset the bit to tell git when you change the working tree file...

Emphasis mine. It goes on to say

This option can be ... used as a coarse file-level mechanism to ignore uncommitted changes in tracked files (akin to what .gitignore does for untracked files). Git will fail (gracefully) in case it needs to modify this file in the index e.g. when merging in a commit; thus, in case the assumed-untracked file is changed upstream, you will need to handle the situation manually.

So just keep in mind that you will have to be aware of any upstream changes made to these files.

In the event that you want to start tracking the file again all you have to do is use

git update-index --no-assume-unchange [ FILE ]

I hope this helps any future viewers of this post.


Like Fred Frodo said, you can put your private exclude rules in the .git/info/exclude of the repository.

If you want to apply the same exclude rules to all the repositories on your machine, you can add the following to the .gitconfig file in your user directory.

[core]       
    excludesfile = /home/<myusername>/.gitexclude 

Then add your exclude patterns to ~/.gitexclude.


For the ssh part, you should consider using Gitolite (a replacement for gitosis).


You may be interested in an update hook that Junio wrote and that Carl improved. Place 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's 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 in place, you then give particular users or groups to make changes to the repository. Anyone else who can see it has read-only access.

This uses two files, $GIT_DIR/info/allowed-users and allowed-groups, to describe which heads can be pushed into by whom. The format of each file would 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

With this, Linus can push or create bw/penguin or bw/zebra or bw/panda branches, Pasky can do only cogito, and JC can do master and pu branches and make versioned tags. And anybody can do tmp/blah branches. The '+' sign at the pu record means that JC can make non-fast-forward pushes on it.

If this person doesn't already have access to the host where your repository lives, maybe that person should have only git-shell access rather than unrestricted access. Create a special-purpose git user and in ~git/.ssh/authorized_keys, add the outsider's SSH key in the following form. Note that the key should be on one long line, but I've wrapped it below to aid 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== [email protected]

Depending on your local setup, you may need to adjust the path to git-shell. Remember that sshd is highly paranoid about permissions of the .ssh directory, so turn off its group-write bits and all files beneath it.

Funneling everyone through the git user means you need to be able tell people apart, and this is the purpose of the myorg_git_user environment variable. Instead of relying on an unconditional username=$(id -u -n), tweak 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 setup, your friend with readonly access will clone with a command resembling the one below. The particular path will depend on your setup. To make the nice path work, either relocate your repository to the git user's home directory or create a symlink that points to it.

$ git clone [email protected]:coolproject.git

but won't 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 [email protected]:coolproject.git
 ! [remote rejected] mybranch -> mybranch (hook declined)
error: failed to push some refs to '[email protected]:coolproject.git'

You said you're working in a team environment, so I assume your central repository was created with the --shared option. (See core.sharedRepository in the git config documentation and --shared in the git init documentation.) Make sure the new git user is a member of the system group that gives all of you access to your central repository.