TL;DR
Which commits end up in your local repo is decided by four levers:
- Refspecs — which refs (branches/tags) you asked for
- Tag policy — all tags / no tags / only reachable tags
- Server-side packing — clone-bundles & large packs may include extra objects you didn’t explicitly ask for
- History shape & modes — reachability, shallow depth, partial clone
Change any of these and you will change which commits appear locally.
- Refspec: the primary switch
A refspec tells Git which refs to fetch and where to store them locally.
# .git/config (examples)
[remote "origin"]
fetch = +refs/heads/*:refs/remotes/origin/* # all branches (common default)
# (optional) fetch = +refs/tags/*:refs/tags/* # all tags, if present
+refs/heads/*→ you requested every branch tip and enough history to reach them → many commits.+refs/heads/foo:refs/remotes/origin/foo→ only one branch.git fetch origin tag vX→ only that tag and the history needed to reach it.repo sync -cnarrows branches (heads) to the current branch, but not tags.
Inspect what you’re asking for:
git config --get-all remote.origin.fetch
- Tags: all / none / only reachable
There are three behaviors when fetching tags:
- All tags
git fetch --tagsorgit config remote.origin.tagOpt --tags
→ fetches everyrefs/tags/*and the commits they point to. - No tags
git fetch --no-tagsorgit config remote.origin.tagOpt --no-tags
→ fetches no tags (you can still fetch a specific tag by name). - Default = auto-follow reachable tags
If you fetch some history and a tag points into that history, Git brings that tag object too.
This does not add extra commits; you already fetched the commits, the tag is just an extra pointer.
Check your current tag policy:
git config --get-all remote.origin.tagOpt || echo "auto-follow (default)"
- “I never asked for this” — why unrelated commits still show up
Two common reasons:
- Clone-bundle & pack reuse (AOSP/
repo)
repo sync often downloads a clone bundle first (a large prebuilt pack), then does a normal fetch.
Servers also send big packs that reuse deltas across many refs. Both can include extra objects that you didn’t request explicitly (for compression/latency wins).
- Those commits may exist in
.git/objectswithout any local ref pointing at them. - That’s why this commit/object can be present even when no branch/tag contains it:
git cat-file -e <sha>^{commit} && echo present || echo absent
- Skip it when you need tighter control:
repo sync --no-clone-bundle ...
- You fetched too many refs
If your refspec is the default +refs/heads/*, you literally fetched all branches.
Auto-follow then adds the tags reachable from those branches. Result: lots of unexpected tags/commits.
- Reachability, precisely
A commit X is included if any local ref (branch or tag) can reach X by following parent links.
Useful checks:
# Is X contained in Y's history?
git merge-base --is-ancestor X Y && echo YES || echo NO
# Who contains X?
git branch --contains X
git branch -r --contains X
git tag --contains X
# Count commits (different scopes)
git rev-list --count HEAD # current branch
git rev-list --count --branches # all local branches
git rev-list --count --remotes # all remote-tracking branches
git rev-list --count --all # everything reachable by any local ref
- Shallow & partial modes
- Shallow clone (
--depth=1)
Only grabs the tip(s) you asked for with minimal ancestry → dramatically fewer commits.
Trade-off: limited history operations (e.g., some merges/rebases). - Partial clone (
--filter=blob:none)
Fetches all commits/trees but defers blobs until first use.
Commit count does not shrink, but bandwidth/IO often does.
- AOSP
repospecifics
-c / --current-branchaffects heads only, not tags.- Unless disabled,
repobehaves close to:- fetch all heads (
+refs/heads/*) - fetch all tags (
+refs/tags/*) - optionally download a clone bundle
- fetch all heads (
- Therefore you can see commits unrelated to your target tag/branch because:
- your refspec asked for them (all heads / all tags), or
- the bundle/pack included them.
Dial it down:
# Smaller: avoid bundle + tags
repo sync -c --no-clone-bundle --no-tags platform/bionic
- Minimal, reproducible recipes
- Only the
android-16.0.0_r2snapshot (no history)
git init bionic-min && cd bionic-min
git remote add aosp https://android.googlesource.com/platform/bionic
git fetch --no-tags --depth=1 aosp tag android-16.0.0_r2
git checkout -q FETCH_HEAD
- Only
android-16.0.0_r2history (full), no extra tags
git init bionic-r2 && cd bionic-r2
git remote add aosp https://android.googlesource.com/platform/bionic
git fetch --no-tags aosp tag android-16.0.0_r2
git checkout -q FETCH_HEAD
- Only r2 history plus reachable tags (not all tags)
# Keep fetch scope narrow; do NOT use +refs/heads/*.
git init bionic-r2-tags && cd bionic-r2-tags
git remote add aosp https://android.googlesource.com/platform/bionic
# Step 1: get r2 history without tags
git fetch --no-tags aosp tag android-16.0.0_r2
git checkout -q FETCH_HEAD
# Step 2: auto-follow reachable tags
git fetch aosp # no --tags / --no-tags → default auto-follow
- Restrict a repo to a single ref long-term
# Replace “all branches” with a single refspec
git config --unset-all remote.origin.fetch
git config --add remote.origin.fetch '+refs/tags/android-16.0.0_r2:refs/tags/android-16.0.0_r2'
# Optional: never auto-fetch tags
git config remote.origin.tagOpt --no-tags
- Why your experiments looked the way they did
- Default (bundle + all heads and usually tags) had the most commits/objects.
--no-clone-bundlereduced commits/objects (no preloaded extras).--no-clone-bundle --no-tagsreduced further (no tag-only histories).- You still saw “mystery commits” with
--no-tagsbecause bundles/packs can include extra objects that no ref points to. They exist, but they’re not reachable from your branches/tags.
Bottom line
- Refspecs define the scope; tag policy controls tag pointers; bundles/packs may add extra objects; history modes (shallow/partial) change what’s transferred.
- If you want predictable, minimal results: narrow your refspecs, disable tags/bundle, and use shallow/partial when appropriate.
- If you want correctness with some savings: restrict refspecs to what you actually need, keep default auto-follow to get only reachable tags, and avoid fetching all heads.
