Git Shallow Clone: Limiting History Depth
A shallow clone downloads only the most recent commits, truncating history at a depth you choose. The files you get are complete; the history behind them is not.
Unlike partial clone, which fetches missing objects automatically when needed, a shallow clone genuinely lacks the history. Commands that reach past the truncation point fail rather than fetching.
That difference determines where each belongs: shallow clones are excellent for CI and poor for workstations.
Creating one
Section titled “Creating one”git clone --depth 1 <url> projectWhat it doesClones the repository but stops after the given number of commits, discarding the parent links beyond that point.
Why we run itOn a repository with years of history, downloading only the latest commit is dramatically faster and smaller — which is why CI systems do it by default.
Expected resultA normal-looking clone with a complete working tree. git log shows only the requested number of commits.
cd projectgit log --oneline | wc -l1One commit. The working tree is complete — every file at that commit is present and correct. What is missing is everything that came before.
Git records the truncation in a file:
test -f .git/shallow && echo "this is a shallow repository"this is a shallow repository.git/shallow lists the commits whose parents are deliberately absent. Git treats those as roots, which is
how a truncated history behaves like a complete one for most read operations.
What breaks
Section titled “What breaks”Anything referring to a commit beyond the truncation:
git log HEAD~1fatal: ambiguous argument 'HEAD~1': unknown revision or path not in the working tree.The commit does not exist locally, and Git does not fetch it. Compare with a partial clone, where the same operation would quietly retrieve what it needed.
The practical consequences:
| Operation | In a --depth 1 clone |
|---|---|
git log | Shows only the commits you have |
git blame | Attributes everything to the boundary commit |
git bisect | Effectively useless — no range to search |
git describe | Fails without tags in range |
git merge-base with an older commit | Fails |
git diff HEAD~5 | Fails |
| Building the working tree | Works perfectly |
That last row is why shallow clones suit CI: a build needs the files, not the history.
Deepening
Section titled “Deepening”A shallow clone can be extended after the fact.
git fetch --deepen=2git log --oneline | wc -l3--deepen=<n> adds <n> more commits to each shallow boundary. To specify an absolute depth instead:
git fetch --depth=10To retrieve everything and become a normal repository:
git fetch --unshallowgit log --oneline | wc -l6test -f .git/shallow || echo "no longer shallow"no longer shallow.git/shallow is removed, and the repository behaves like any other.
Running --unshallow on a complete repository is an error rather than a no-op:
fatal: --unshallow on a complete repository does not make senseSingle-branch clones
Section titled “Single-branch clones”--depth implies --single-branch: only the branch you cloned is fetched, and the remote’s fetch refspec
is narrowed to it.
git config --get remote.origin.fetch+refs/heads/main:refs/remotes/origin/mainOther branches are not available, and git fetch will not bring them. To widen it:
git remote set-branches origin '*'git fetch --depth 1This trips people up in CI when a job needs to compare against another branch — the branch simply is not there. Fetch it explicitly:
git fetch --depth 1 origin main--depth also limits tags: only tags pointing at fetched commits arrive. A repository with hundreds of
release tags will appear to have almost none.
That matters for build systems that derive a version from git describe:
git describe --tagsfatal: No tags can describe '49062b4...'.Fetch tags explicitly if your build needs them:
git fetch --depth 1 --tagsOr clone with enough depth to include the most recent tag. There is no way to ask for “the latest tag” — you either fetch tags or deepen until one is in range.
Where shallow clones belong
Section titled “Where shallow clones belong”Good fit:
- CI and build systems. The dominant use. A build needs files, not history, and cloning a large repository on every job is a real cost. Most CI platforms shallow-clone by default.
- Containers and deployment. Fetching source into an image where history is dead weight.
- One-off inspections. Reading a project you do not intend to contribute to.
- Automated analysis of current state. Linting, scanning, dependency auditing.
Poor fit:
- Developer workstations. You will want
git log,git blameandgit bisect, and hitting the truncation mid-investigation is a genuine interruption. - Anything that pushes, unless verified.
- Jobs needing a merge base, unless you fetch enough depth for one to exist.
- Version derivation from tags, without fetching tags.
- Repositories that are not actually large. The saving is proportional to history size.
Shallow, partial and sparse
Section titled “Shallow, partial and sparse”| Limits | Missing data is… | Best for | |
|---|---|---|---|
| Shallow | Commit history depth | Genuinely absent — operations fail | CI, one-off checkouts |
| Partial | Which objects arrive initially | Fetched automatically on demand | Developer workstations |
| Sparse | Which paths are on disk | Present — just not checked out | Monorepos |
The key contrast: partial clone degrades gracefully; shallow clone does not. In a partial clone, a
deep git log -p is slow. In a shallow clone, it fails.
For a workstation on a large repository, --filter=blob:none gives most of the clone-time benefit while
keeping the repository fully functional. That is usually the better choice.
They can be combined:
git clone --depth 1 --filter=blob:none <url> projectThis is unusual — it makes sense mainly for CI on a repository that is both deep and full of large files.
What a shallow clone actually saves
Section titled “What a shallow clone actually saves”Worth being concrete, because the saving varies enormously by repository.
The transfer is dominated by history, not by the current state. A repository whose checkout is 50 MB
may have a 2 GB .git if it has ten years of commits — every version of every file that ever existed.
--depth 1 fetches the commits you asked for and the objects they need, skipping the rest.
The saving is therefore proportional to how much history exists relative to the current tree:
| Repository shape | Saving from --depth 1 |
|---|---|
| Long history, small tree | Very large |
| Short history, large tree | Small |
| Long history, many large binaries | Large, but partial clone may suit better |
| A few hundred commits | Negligible — not worth the limitations |
Measure it rather than assuming:
git clone --depth 1 "$URL" shallow && du -sh shallow/.gitgit clone "$URL" full && du -sh full/.gitIf the two numbers are close, the repository’s size is its current content, and a shallow clone buys you nothing while costing you history.
Common mistakes
Section titled “Common mistakes”Using a shallow clone as a development clone. You will hit the boundary, usually mid-investigation.
Assuming git log shows everything. It shows what you fetched.
Expecting --depth to work on a local path clone. It is ignored. Use file://.
Forgetting --depth implies --single-branch. Other branches are not fetched, which breaks
comparisons in CI.
Expecting tags to be there. Only those pointing at fetched commits arrive.
Pushing from a shallow clone without checking. Unshallow first, or clone with more depth.
Confusing it with partial clone. Shallow limits history; partial limits objects and fetches them back.
Deepening repeatedly instead of unshallowing. Several --deepen calls often cost more than one
--unshallow.
Mental Model
Section titled “Mental Model”A shallow clone is the last few pages of a book.
Everything on those pages is complete and readable. Ask what happened in chapter two and there is no answer — not “let me fetch it”, but “those pages are not here”.
A partial clone, by contrast, is the whole book with some illustrations left at the printers. Ask for one and it arrives.
What You Learned
Section titled “What You Learned”--depth <n>truncates history to the most recent<n>commits; the working tree is complete..git/shallowrecords the truncation boundary; Git treats those commits as roots.- Operations referring to older commits fail; Git does not fetch them automatically.
--deepen=<n>extends the history,--unshallowretrieves everything.--depthimplies--single-branch, so other branches are absent.- Only tags pointing at fetched commits arrive, which breaks
git describe. - Shallow clones suit CI and one-off checkouts, not development.
- Partial clone is usually the better choice for a workstation, because it degrades gracefully.
Try It Yourself
Section titled “Try It Yourself”-
Create a repository with several commits to act as the remote:
Terminal window mkdir ~/shallow-lab && cd ~/shallow-lab && git init src-repo && cd src-repoecho start > file.txt && git add . && git commit -m "Initial"for i in 1 2 3 4 5; do echo "change $i" >> file.txt; git commit -am "change $i"; done -
Clone it shallowly — note the
file://URL:Terminal window cd ~/shallow-labgit clone --depth 1 "file://$HOME/shallow-lab/src-repo" shallowcd shallow -
Count the commits:
git log --oneline | wc -l. Predict the number first. -
Confirm it is shallow:
test -f .git/shallow && echo yes. -
Confirm the working tree is complete:
cat file.txt— every change should be present, even though the commits that made them are not. -
Try to reach past the boundary:
git log --oneline HEAD~1. Read the error. -
Deepen by two:
git fetch --deepen=2, then count again. -
Unshallow:
git fetch --unshallow, count again, and confirm.git/shallowis gone. -
Try to unshallow twice and read the error.
-
Compare the fetch refspec:
git config --get remote.origin.fetch— is it narrowed to one branch?
Step 5 is the point most people miss: the files are complete. Shallow clones truncate history, not content.
Next Lesson
Section titled “Next Lesson”That completes the three large-repository features. The next lessons cover automation and ergonomics, starting with hooks.