Git and libgit2

Bare Git Repositories

Published 2025-01-22.
Time to read: 4 minutes.

This page is part of the git collection.

A bare git repository is a server-side Git repository. Because users do not interact with it directly, no working tree or index is required.

By convention, the name of the directory containing a bare Git repository ends with .git. Although this is not required, if a bare Git repository is named this way, when a user clones the bare Git repository, the name of the directory containing the cloned repository will not end with .git. Experienced Git users expect this behavior, so it is a good idea to provide a .git suffix for bare Git repositories.

Bare repositories do not have a working tree, so you cannot add files to them as you would for a normal repository. Instead, you must update a bare repository by pushing to it from a clone of the repository.

Where to Use

Bare Git repositories are often stored on a local area network server.

Bare Git repositories need to be accessible via a supported protocol. That means the bare Git repository must either be stored on the local machine, or on a local area network, or accessed remotely via the ssh or git protocols.

When to Use

The advantages of bare Git repositories over online hosted repositories are:

  1. Security can be better than storing files in the cloud.
  2. Simple setup because there is no Git server.

The git init ‑‑shared Option

The article entitled Shared Directories With POSIX Groups and SGID discusses POSIX groups and the SGID permission bit. This section uses the git_access POSIX group created in that article.

The most convenient way to allow others to push to a bare Git repository is to use the git init ‑‑shared option. This option causes the SGID bit to be set on the directory holding the Git repository. The general format is:

Shell
$ git --bare --shared=some_value init

The --shared option can have several values. The possible values that are relevant to a locally accessible Git LFS from a bare repository are: all, everybody, group, true, and world.

When the ‑‑shared option is specified, the config variable core.sharedRepository is set to 1 for group sharing, and is set to 2 for sharing with everybody.

Otherwise, if the ‑‑shared option is not specified, Git will use the permissions reported by umask. See the man page for git-init for further details.

If you specify ‑‑shared=group or ‑‑shared=true, the Set Group ID (SGID) permission for that directory will be set, as shown in the highlighted s permission bit in the code example below. Note that test_repo.git is created in the home directory of user mslinn for this example.

Shell
mslinn@gojira ~ mkdir --mode=g+s test_repo.git
mslinn@gojira ~ sudo chgrp git_access test_repo.git
mslinn@gojira ~ ls -ld test_repo.git drwxrwsrwx 2 mslinn git_access 4096 Jan 20 13:51 test_repo.git/
mslinn@gojira ~ git init --bare --shared=group test_repo.git Initialized empty shared Git repository in /home/mslinn/test_repo.git
mslinn@gojira ~ ls -l test_repo.git total 28 -rw-rw-r-- 1 mslinn git_access 126 Jan 20 09:34 config -rw-rw-r-- 1 mslinn git_access 73 Jan 20 09:34 description -rw-rw-r-- 1 mslinn git_access 23 Jan 20 09:34 HEAD drwxrwsr-x 2 mslinn git_access 4096 Jan 20 09:34 hooks/ drwxrwsr-x 2 mslinn git_access 4096 Jan 20 09:34 info/ drwxrwsr-x 4 mslinn git_access 4096 Jan 20 09:34 objects/ drwxrwsr-x 4 mslinn git_access 4096 Jan 20 09:34 refs/
mslinn@gojira ~ cd test_repo.git
mslinn@gojira test_repo.git git config core.sharedRepository 1

Now lets clone the new repo from another computer, a Windows machine with WSL, which has had /etc/fstab augmented with the necessary incantation to mount my home directory on gojira on the WSL file system.

See Mounting Shared Directories on WSL & Ubuntu for background on mounting shared drives on Ubuntu and WSL/Ubuntu. The remainder of this article assumes you have read the background article and it is fresh in your mind. Ubuntu on WSL is just a little bit different from native Ubuntu in how shared directories are mounted. The very small difference is quite important to get right.

Shell
mslinn@bear ~ sudo mount /mnt/gojira_mslinn
mslinn@bear ~ ls /mnt/gojira_mslinn/test_repo.git/ HEAD* config* description* hooks/ info/ objects/ refs/
mslinn@bear ~ git clone /mnt/gojira_mslinn/test_repo.git Cloning into 'test_repo'... warning: You appear to have cloned an empty repository. done.

You can open up the permissions such that anyone with access can update the repository. This is the simplest configuration. There are three equivalent ways of specifying this value: ‑‑shared=all, ‑‑shared=world and ‑‑shared=everybody.

Shell
mslinn@gojira ~ mkdir --mode=g+s test_repo2.git
mslinn@gojira ~ sudo chgrp git_access test_repo2.git
mslinn@gojira ~ git init --bare --shared=everybody test_repo2.git Initialized empty shared Git repository in /home/mslinn/test_repo2.git
mslinn@gojira ~ cd test_repo2.git
mslinn@gojira test_repo.git git config core.sharedRepository 2

We can clone test_repo2.git from the other computer as before. The mount at /mnt/gojira_mslinn is assumed to still be in place.

Shell
mslinn@bear ~ ls /mnt/gojira_mslinn/test_repo2.git
HEAD* config* description* hooks/ info/ objects/ refs/ 
mslinn@bear ~ git clone /mnt/gojira_mslinn/test_repo2.git Cloning into 'test_repo2'... warning: You appear to have cloned an empty repository. done.

receive.denyCurrentBranch

You could set the receive.denyCurrentBranch configuration variable to ignore or updateInstead. While this does nothing for a null Git LFS server, it eliminates a warning that is meaningless for this configuration. We will see this in action later.

new_bare_repo Creation Script

The new_bare_repo script creates a bare Git repository with --shared=all. Installation instructions are provided in Git LFS Scripts.

new_bare_repo
#!/bin/bash

function help {
  if [ "$1" ]; then echo "$1"; fi
  echo "$(basename "$0") - Create a new bare Git repository.

Syntax: $(basename "$0") /path/to/new/repo.git

Normally this script would be run on a Git server, because
that is where bare Git repositories normally live.

A new Git repository will be created in /path/to/new/repo.git,
which should not already exist.

The SGID permission for the new Git repository will be set for group git_access,
which is created if it does not exist.

The parent directory (/path/to/new/) will be created if it does not already exist.
The name of the repo must not contain spaces.
If the specified name does not end with a .git suffix, the suffix is appended.

Git configuration parameter 'receive.denyCurrentBranch' is set to ignore.
"
  exit 1
}

function configure {
  git config receive.denyCurrentBranch ignore
}

function initialize {
  mkdir --mode=g+s "$REPO_PATH/$REPO_NAME"
  sudo chgrp git_access $REPO_NAME
  git init --bare --shared=everybody "$REPO_PATH/$REPO_NAME"
}

if [ -z "$1" ]; then help; fi

if [ "$(getent group admin)" ]; then
  sudo groupadd git_access
fi

export REPO_PATH="$( dirname "$1" )"
export REPO_NAME="$( basename "$1" )"
if [[ "$REPO_NAME" != *.git ]]; then export REPO_NAME="$REPO_NAME.git"; fi
if [ -f "$REPO_PATH/$REPO_NAME" ]; then help "Error: '$REPO_PATH/$REPO_NAME' already exists."; fi
cd "$REPO_PATH" || exit 1

initialize "$1"
cd "$REPO_PATH/$REPO_NAME" || exit 3
configure

This is the help message:

mslinn@gojira ~ $ new_bare_repo
new_bare_repo - Create a new bare git repository.

Syntax: new_bare_repo /path/to/new/repo

A new git repository will be created in /path/to/new/repo,
which should not already exist.
The parent directory (/path/to/new/) must already exist.
The name of the repo must not contain spaces.

After creation, a new environment variable will be created
in /etc/environment.
The name that you specified ('repo' in the above example) will be used
for the name of a new environment variable, set to the path that
you specified. 

Blow-By-Blow Explanation

The following commands make a bare repository in a new directory on gojira called $REPO_HOME. The repository will be open to all users that can access the server. You do not need to type all of this out; the next section shows how to use the above script to accomplish the same objective.

In the following console session, an environment variable is created, called REPO_HOME that points to the directory that will contain the bare git repository. I placed the definition for REPO_HOME in the /etc/environment directory so it is available to every shell, even cron jobs. I used the sudo tee -a command to append to /etc/environment, which is owned by root. The contents of /etc/environment are then incorporated into the console session with the source command.

Shell
mslinn@bear ~ $ ssh gojira
Welcome to Ubuntu 24.04 LTS (GNU/Linux 6.8.0-35-generic x86_64)
mslinn@gojira ~ $ REPO_HOME=$HOME/eval_repos
mslinn@gojira ~ $ echo "REPO_HOME=$REPO_HOME" | \ sudo tee -a /etc/environment > /dev/null
mslinn@gojira ~ $ REPO_NAME=repo1.git

Now we can create the bare Git repository called $REPO_NAME in the $REPO_HOME directory according to the information in the previous section.

Shell
mslinn@gojira eval_repos $ git init --bare --shared=all "$REPO_HOME/$REPO_NAME"
Initialized empty shared git repository in /home/mslinn/eval_repos/repo1.git 
mslinn@gojira eval_repos $ mkdir --mode=g+s "$REPO_HOME/$REPO_NAME"
mslinn@gojira eval_repos $ sudo chgrp git_access "$REPO_HOME/$REPO_NAME"
mslinn@gojira ~ $ git init --bare --shared=everybody "$REPO_HOME/$REPO_NAME"
mslinn@gojira ~ $ cd "$REPO_HOME/$REPO_NAME"
mslinn@gojira repo1.git $ git config receive.denyCurrentBranch ignore

Alright, the bare Git repository has been created. Let’s admire our handiwork:

Shell
mslinn@gojira bare_repo $ tree -d
.
├── branches
├── hooks
├── info
├── objects
│   ├── info
│   └── pack
└── refs
├── heads
└── tags 
9 directories

Using new_bare_repo

Instead of typing out the commands in the previous section, you can create a bare Git repository by using the new_bare_repo script:

Shell
mslinn@gojira ~ $ mkdir -p ~/eval_repos && cd ~/eval_repos
mslinn@gojira ~ $ new_bare_repo repo1.git Initialized empty shared Git repository in /home/mslinn/eval_repos/repo1.git The new repository contains the following subdirectory tree . ├── hooks ├── info ├── objects │   ├── info │   └── pack └── refs ├── heads └── tags
9 directories All done! The bare repository was created in '/home/mslinn/eval_repos/repo1.git'.

Choosing a (Client-Side) Protocol

The Git remote.origin.url configuration setting can be set to any suitable protocol, including ssh and both flavors of the local protocol. While you could theoretically use the http or https protocols, it would be unusual for a webserver to be configured on a local server for this purpose; however, this would work.

SSH is the most flexible protocol and is well understood.

The local protocol can be expressed two ways. In the following two examples, /path/to/bare/ is a locally accessible path on a user machine. This path could originate from a locally mounted file system, as discussed in Mounting Shared Directories on WSL & Ubuntu.

Shell
mslinn@bear ~ $ sudo mount /mnt/gojira_mslinn
mslinn@bear ~ $ ls /mnt/gojira_mslinn/test_repo.git HEAD* config* description* hooks/ info/ objects/ refs/

file:///gojira_mslinn/path/to/bare/repo.git
Note the 3 slashes after file:.

Shell
mslinn@bear ~ $ git clone file:///mnt/gojira_mslinn/test_repo.git test1
Cloning into 'test1'...
warning: You appear to have cloned an empty repository. 

/gojira_mslinn/path/to/bare/repo.git

Shell
mslinn@bear ~ $ git clone /mnt/gojira_mslinn/test_repo.git test2
Cloning into 'test2'...
warning: You appear to have cloned an empty repository. 

If you are unaware of the restrictions of Git’s local protocol implementation, or the differences between the various flavors of the local protocol, please read The File URI Schema And The Local Protocol. That same article also discusses UNC paths.

The Git repository on the server is protocol-independent. Multiple users can use different protocols to access it.

In contrast, the Git client must establish and save the protocol that will be used by this instance of the Git repository to access a specific remote Git repository.

Cloning On the Same Machine

On my server gojira, the file system containing the the bare repo is of course already mounted. The Git repository can be cloned from another local directory on gojira like this:

Shell
mslinn@gojira tmp $ git clone file:///home/mslinn/bare_repo.git
Cloning into 'bare_repo'...
warning: You appear to have cloned an empty repository. 

Also like this:

Shell
mslinn@gojira tmp $ git clone /home/mslinn/bare_repo.git
Cloning into 'bare_repo'...
warning: You appear to have cloned an empty repository. 
* indicates a required field.

Please select the following to receive Mike Slinn’s newsletter:

You can unsubscribe at any time by clicking the link in the footer of emails.

Mike Slinn uses Mailchimp as his marketing platform. By clicking below to subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. Learn more about Mailchimp’s privacy practices.