Skip to content

Git Shallow Clone: Limiting History Depth

Lesson 5 of 11Intermediate8 min readModern Git Workflows · Modern Git ProductivityVerified: Git 2.43.0 on Ubuntu 24.04; every command and output in this lesson was run

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.

Terminal window
git clone --depth 1 <url> project

What 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.

Terminal window
cd project
git log --oneline | wc -l
1

One 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:

Terminal window
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.

Anything referring to a commit beyond the truncation:

Terminal window
git log HEAD~1
fatal: 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:

OperationIn a --depth 1 clone
git logShows only the commits you have
git blameAttributes everything to the boundary commit
git bisectEffectively useless — no range to search
git describeFails without tags in range
git merge-base with an older commitFails
git diff HEAD~5Fails
Building the working treeWorks perfectly

That last row is why shallow clones suit CI: a build needs the files, not the history.

A shallow clone can be extended after the fact.

Terminal window
git fetch --deepen=2
git log --oneline | wc -l
3

--deepen=<n> adds <n> more commits to each shallow boundary. To specify an absolute depth instead:

Terminal window
git fetch --depth=10

To retrieve everything and become a normal repository:

Terminal window
git fetch --unshallow
git log --oneline | wc -l
6
Terminal window
test -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 sense

--depth implies --single-branch: only the branch you cloned is fetched, and the remote’s fetch refspec is narrowed to it.

Terminal window
git config --get remote.origin.fetch
+refs/heads/main:refs/remotes/origin/main

Other branches are not available, and git fetch will not bring them. To widen it:

Terminal window
git remote set-branches origin '*'
git fetch --depth 1

This trips people up in CI when a job needs to compare against another branch — the branch simply is not there. Fetch it explicitly:

Terminal window
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:

Terminal window
git describe --tags
fatal: No tags can describe '49062b4...'.

Fetch tags explicitly if your build needs them:

Terminal window
git fetch --depth 1 --tags

Or 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.

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 blame and git 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.
LimitsMissing data is…Best for
ShallowCommit history depthGenuinely absent — operations failCI, one-off checkouts
PartialWhich objects arrive initiallyFetched automatically on demandDeveloper workstations
SparseWhich paths are on diskPresent — just not checked outMonorepos

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:

Terminal window
git clone --depth 1 --filter=blob:none <url> project

This is unusual — it makes sense mainly for CI on a repository that is both deep and full of large files.

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 shapeSaving from --depth 1
Long history, small treeVery large
Short history, large treeSmall
Long history, many large binariesLarge, but partial clone may suit better
A few hundred commitsNegligible — not worth the limitations

Measure it rather than assuming:

Terminal window
git clone --depth 1 "$URL" shallow && du -sh shallow/.git
git clone "$URL" full && du -sh full/.git

If the two numbers are close, the repository’s size is its current content, and a shallow clone buys you nothing while costing you history.

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.

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.

  • --depth <n> truncates history to the most recent <n> commits; the working tree is complete.
  • .git/shallow records 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, --unshallow retrieves everything.
  • --depth implies --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.
  1. 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-repo
    echo 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
  2. Clone it shallowly — note the file:// URL:

    Terminal window
    cd ~/shallow-lab
    git clone --depth 1 "file://$HOME/shallow-lab/src-repo" shallow
    cd shallow
  3. Count the commits: git log --oneline | wc -l. Predict the number first.

  4. Confirm it is shallow: test -f .git/shallow && echo yes.

  5. Confirm the working tree is complete: cat file.txt — every change should be present, even though the commits that made them are not.

  6. Try to reach past the boundary: git log --oneline HEAD~1. Read the error.

  7. Deepen by two: git fetch --deepen=2, then count again.

  8. Unshallow: git fetch --unshallow, count again, and confirm .git/shallow is gone.

  9. Try to unshallow twice and read the error.

  10. 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.

That completes the three large-repository features. The next lessons cover automation and ergonomics, starting with hooks.