Skip to content

Git reference repositories and object alternates for runner caching

Cloning multi-gigabyte repositories on every CI runner run drains network bandwidth, fills disk storage, and adds minutes of idle queue time. Shallow clones help for basic linting, but full histories remain necessary for changelog generation, release tagging, merge base calculations, and integration test suites.

Git reference repositories solve this problem by sharing object databases across multiple working copies on the same host. By borrowing objects from a central cache through .git/objects/info/alternates, new clones complete in seconds and consume almost zero initial disk space.


How object borrowing works

A standard Git repository stores commits, trees, blobs, and tags inside its .git/objects directory. When you clone a repository, Git transfers every packfile over the network and writes a complete copy to disk.

When you clone with a reference repository, Git creates standard local refs in .git/refs/ but writes the absolute path of the reference repository's object store into .git/objects/info/alternates.

/var/cache/git/repo.git/objects/        <-- Central bare reference cache
         ^
         | (borrowed objects)
         |
/home/runner/workspace-job-1/.git/objects/info/alternates
/home/runner/workspace-job-2/.git/objects/info/alternates

When Git needs to read an object:

  1. Git checks the local .git/objects directory.
  2. If the object is not found locally, Git reads the paths in .git/objects/info/alternates.
  3. Git reads the object directly from the alternate object store without copying it.
  4. Any newly created commits or downloaded patches during the build write exclusively to the local .git/objects directory.

Reference clone commands

Basic reference clone

The --reference option tells Git to borrow objects from an existing local repository:

git clone --reference /var/cache/git/my-project.git https://forgejo.example.com/org/my-project.git build-dir

Git copies all remote branch tips, but fetches only the objects missing from /var/cache/git/my-project.git. If the reference cache is up to date, network transfer drops to a few kilobytes of metadata.

Non-blocking reference clone

If the reference directory does not exist or is corrupted, git clone --reference aborts with a fatal error. In CI environments where cache directories might be missing on clean runners, use --reference-if-able:

git clone --reference-if-able /var/cache/git/my-project.git https://forgejo.example.com/org/my-project.git build-dir

When the path exists, Git borrows objects from it. If the path does not exist, Git logs a warning and falls back to a regular full clone over the network.


Severing ties with dissociation

Borrowing objects creates a hard runtime dependency. If the reference repository is deleted, moved, or pruned while a build runs, the downstream repository loses access to borrowed objects and fails with fatal: bad object.

To achieve fast clones while creating completely independent repositories, use --dissociate:

git clone --reference /var/cache/git/my-project.git --dissociate https://forgejo.example.com/org/my-project.git build-dir

What dissociation does

  1. Git borrows objects from the reference repository during the clone to avoid network transfer.
  2. Git fetches any remaining new commits from the remote.
  3. Git immediately repacks all borrowed objects into a self-contained local packfile inside build-dir/.git/objects/pack/.
  4. Git deletes .git/objects/info/alternates.

Dissociation requires disk space for the full repository, but disk-to-disk local copies are fast, and the resulting repository can be archived, moved, or deleted safely.

Manual dissociation of an existing repository

If a working copy currently borrows objects and needs to become standalone:

# Repack all objects (local and borrowed) into a single packfile
git repack -a -d -l

# Remove the alternates file
rm -f .git/objects/info/alternates

The -l flag instructs git repack to copy objects from alternate databases into the local packfile before unlinking them.


Safe reference cache maintenance

The biggest risk with shared object alternates is object deletion. If you run git gc --prune on the reference cache, Git may delete objects that the cache does not reference, but which active CI builds are currently borrowing.

Rules for reference cache maintenance

  1. Maintain the reference cache as a bare mirror.
  2. Fetch updates frequently using git fetch --all --no-tags --force.
  3. Never run git prune or git gc --prune=now on an active reference cache.
  4. Keep all branch tips alive in the cache so objects remain reachable.

Run this command periodically or before scheduled pipeline batches:

CACHE_DIR="/var/cache/git/my-project.git"

if [ ! -d "$CACHE_DIR" ]; then
  git clone --mirror https://forgejo.example.com/org/my-project.git "$CACHE_DIR"
else
  git --git-dir="$CACHE_DIR" fetch --no-prune origin "+refs/heads/*:refs/heads/*" "+refs/tags/*:refs/tags/*"
fi

Using --no-prune ensures that deleted remote branches do not immediately trigger object removal from the reference store while jobs are running.


Dynamic alternates with environment variables

You can attach alternate object stores dynamically without modifying .git/objects/info/alternates on disk.

Using GIT_ALTERNATE_OBJECT_DIRECTORIES

Set GIT_ALTERNATE_OBJECT_DIRECTORIES to a colon-separated list of object directory paths:

export GIT_ALTERNATE_OBJECT_DIRECTORIES="/var/cache/git/my-project.git/objects:/opt/shared-objects"

# Commands now read objects from both local and alternate paths
git log -n 10
git checkout feature-branch

This approach works well in ephemeral Docker containers where a host volume containing object stores mounts as read-only at /var/cache/git/objects.


Enterprise CI runner configuration

GitHub Actions and Forgejo runner setup

On self-hosted runners, mount a persistent cache directory into the runner workspace.

steps:
  - name: Checkout repository using host reference cache
    env:
      CACHE_REPO: /var/cache/git/main-repo.git
      TARGET_URL: https://forgejo.example.com/org/main-repo.git
    run: |
      mkdir -p /var/cache/git
      if [ ! -d "$CACHE_REPO" ]; then
        git clone --mirror "$TARGET_URL" "$CACHE_REPO"
      else
        git --git-dir="$CACHE_REPO" fetch --no-prune origin "+refs/heads/*:refs/heads/*"
      fi

      git clone --reference-if-able "$CACHE_REPO" "$TARGET_URL" .
      git checkout "$GITHUB_SHA"

Performance comparison on a 4 GB repository

Clone method Network transfer Execution time Disk usage
Standard full clone 4.2 GB 145 seconds 4.8 GB
Shallow clone (--depth 1) 280 MB 14 seconds 350 MB
Reference clone (--reference) 12 KB 1.8 seconds 45 MB
Reference clone with dissociation 12 KB 7.4 seconds 4.8 GB

Reference clones provide the speed of a shallow clone while retaining the complete history and commit graph needed for build tasks.