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:
- Git checks the local
.git/objectsdirectory. - If the object is not found locally, Git reads the paths in
.git/objects/info/alternates. - Git reads the object directly from the alternate object store without copying it.
- Any newly created commits or downloaded patches during the build write exclusively to the local
.git/objectsdirectory.
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¶
- Git borrows objects from the reference repository during the clone to avoid network transfer.
- Git fetches any remaining new commits from the remote.
- Git immediately repacks all borrowed objects into a self-contained local packfile inside
build-dir/.git/objects/pack/. - 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¶
- Maintain the reference cache as a bare mirror.
- Fetch updates frequently using
git fetch --all --no-tags --force. - Never run
git pruneorgit gc --prune=nowon an active reference cache. - Keep all branch tips alive in the cache so objects remain reachable.
Recommended cache update routine¶
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.