skip to content

Which Git operations break in a shallow clone, and how do you restore the full history?

level: middleimportance: should knowfreq 52%

answer

  1. Ancestry walking is what suffers
  2. Tags, blame and common ancestors
  3. Two flags: one refills all, one adds some
  4. Widening the refspec is the second half
  5. --unshallow versus --deepen

basics

~20 s

Anything needing ancestry degrades in a shallow clone: log, blame, describe, bisect and merge-base against a long-diverged branch. Run git fetch --unshallow to download the missing history, or git fetch --deepen=N to extend it gradually.

solid answer

~40 s

A shallow clone stops history at the commits listed in `.git/shallow`, so every command that walks ancestry is affected: `git log` and `git blame` end at the boundary, `git describe` cannot reach an older tag, `git bisect` can only search what is present, and `git merge-base` may find no common ancestor — which makes merges, rebases and "diff against the base branch" steps fail or produce wrong results. To fix it, `git fetch --unshallow` pulls in the rest of the history for the refs your refspec covers and removes the boundary; `git fetch --deepen=<n>` extends it by n more commits if you only need a bit more. Note that if the clone was shallow it was also single-branch, so you usually have to widen `remote.origin.fetch` back to `+refs/heads/*:refs/remotes/origin/*` before other branches appear.

code

bash · 10 lines
bash
git clone --depth 1 https://example.com/app.git
cd app
git describe --tags
# fatal: No tags can describe '9f1c2ab...'

git fetch --unshallow
git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"
git fetch origin
git describe --tags
# v3.2.0-14-g9f1c2ab

go deeper

for a junior

Recognize the symptoms: git describe failing, blame pointing everything at one commit, or a diff that claims every file changed. Know that git fetch --unshallow is the blunt fix.

for a middle

Explain why ancestry-walking commands fail at the shallow boundary and the difference between --unshallow, --deepen and --depth on fetch, including the single-branch refspec you must widen.

for a senior

Diagnose this in a live pipeline: identify which step needs history, then pick the cheapest bounded fetch rather than unshallowing everything on every run.

for a principal

Set the standard so pipelines do not oscillate between a too-shallow default and a full clone — define which job classes need history and make the fetch depth an explicit, reviewed choice.

## Why things break Git's power comes from walking the commit graph. A shallow repository has an artificial edge: the commits named in `.git/shallow` are treated as if they had no parents. Every operation that would walk past that edge is either truncated or fails. ## The specific failures **History reading.** `git log` stops at the boundary. `git blame` attributes every line that was last touched beyond the boundary to the boundary commit, so ownership answers are simply wrong rather than merely incomplete. **Version derivation.** `git describe` looks backwards for the nearest tag. If the tag is older than the boundary, it fails outright. Build systems that stamp a version from `git describe` therefore break in a shallow CI checkout — a very common pipeline bug. **Merge base.** `git merge-base main HEAD` needs a common ancestor. In a shallow, single-branch clone the other branch may not even be present; even when both tips are present at depth 1, their common ancestor is far below the boundary. Consequences ripple outward: `git merge`, `git rebase`, `git cherry-pick` onto a distant base, and `git diff main...HEAD` (which is defined in terms of the merge base) all misbehave. In CI this shows up as "changed files" lists containing the entire repository. **Bisect.** `git bisect` can only pick candidates from the commits you have, so a regression introduced before the boundary is unfindable. ## Restoring history `git fetch --unshallow` asks the remote for everything missing, then deletes the shallow boundary; the repository becomes a normal complete clone. It needs a remote that actually has the full history, and it cannot be combined with `--depth`. `git fetch --deepen=<n>` extends the existing history by `n` further commits without going all the way. `git fetch --depth=<n>` sets an absolute depth measured from the current tips, which can deepen or (rarely) re-shorten. `--shallow-since=<date>` moves the boundary to a date instead of a count. ## The single-branch trap Because `--depth` implies `--single-branch`, an unshallowed repository is still narrow: `remote.origin.fetch` names one branch, so `--unshallow` gives you the full history *of that branch only*. Other branches appear only after you widen the refspec, for example with `git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"` followed by `git fetch`. In CI this catches people out: they unshallow, still cannot find `origin/main`, and conclude the fetch failed. ## The cost calculation Unshallowing downloads exactly the data the shallow clone was avoiding, and it is often *more* expensive than a full clone would have been, because the server cannot reuse a prepared pack as cheaply. If a pipeline consistently ends up unshallowing, the honest answer is to clone deeply in the first place, or to fetch a bounded amount of history up front — for example `--depth 50` when you only need to diff against recent commits, or `--shallow-since` covering the release window. ## Choosing a middle ground Many jobs need only a *little* history. Fetching the base branch at a bounded depth is usually enough for a merge-base diff, and `--shallow-since` is a good fit for "anything in the last month". Where the job truly needs tags or blame, use a full clone or a partial clone rather than shallow, since a partial clone keeps the whole commit graph and only defers file contents.

  • Why does git diff main...HEAD often list the whole repository in a shallow CI checkout?
    The three-dot form is defined relative to the merge base of `main` and `HEAD`. In a shallow clone the real common ancestor is below the boundary, so Git either fails or falls back to comparing against a boundary commit that shares almost nothing with the branch — producing a diff that looks like every file changed.
  • If a job only needs to diff against the target branch, what is cheaper than --unshallow?
    Fetch a bounded slice instead of everything: clone at a modest `--depth`, then `git fetch --depth=<n> origin <target-branch>` so both tips and a plausible merge base are present. `--shallow-since=<date>` works well when the branches are known to have diverged recently.
  • Can --unshallow be combined with --depth?
    No. `--unshallow` means "fetch everything and remove the boundary", which contradicts a depth limit, and Git rejects the combination. Use `--deepen=<n>` or `--depth=<n>` when you want to extend the history only part of the way.

saying these in an interview costs you the question

  • Claims unshallow is free because objects are already local
  • Thinks blame works normally, just with fewer commits
  • Expects other branches to appear after --unshallow alone
  • Blames the CI runner when git describe fails
  • Treats a failed merge-base as a corrupt repository

context