| English | 中文 | Interactive demo | Runnable lab |
When a repository has many commits and similar files and .git/objects grows, it is tempting to explain it as “each commit copies the project.” Git’s logical model is a snapshot graph made of commits, trees, and blobs. The physical layer can retain objects as loose objects or reorganize them into pack files with delta compression for similar objects. Physical layout can change while the content visible from the same commit stays unchanged.
This chapter covers how to observe those layers, what repack, gc, and prune each do, and why commit-graph is acceleration metadata rather than part of a business snapshot.
When a team sees object storage grow and wants to run maintenance, split the question into four layers:
logical content: the commit, tree, and blob content shown by git show <commit>
reachability: which objects can still be found through branch, tag, reflog, and other refs
physical layout: loose objects, packs, deltas, and pack indexes
auxiliary metadata: how commit-graph assists traversal of commit history
If main still points to commit C, normal repacking or writing a commit-graph must not change git show C. After deleting a branch ref, whether objects unique to that branch are cleaned also depends on other refs, reflogs, expiry policies, and maintenance timing. “The branch is deleted” does not prove “the objects disappeared immediately,” because it omits the recovery window.
A commit points to a root tree; a tree points to paths and blobs or subtrees; a blob stores file content. Object IDs derive from object type and content, so Git can reuse an object with identical content. Multiple commits commonly share trees and blobs for unchanged files.
At the physical layer, many objects may initially be separate loose files below .git/objects/. git repack can write objects into one or more .pack files with .idx indexes. Similar objects can be expressed as deltas relative to another object. When Git reads an object, it reconstructs the same logical content.
Both statements are true:
A commit is semantically a complete snapshot.
Git can share objects on disk and compress similar objects.
Do not infer that a snapshot requires copying the whole working directory on every commit. Do not infer that a delta in a pack makes historical content incomplete.
First inspect current object statistics and the pack directory:
git count-objects -vH
git rev-parse --git-path objects/pack
git verify-pack -v .git/objects/pack/*.idx
count-objects counts and byte sizes change with Git version, object hash format, compression parameters, file contents, prior automatic maintenance, and machine environment. verify-pack can show objects and delta-chain information for the current pack. They are useful for diagnosing and comparing one controlled experiment. They do not independently prove repository performance or define a fixed threshold for every project.
For a disposable copy, or a maintenance window with a verified backup and no concurrent Git writers, common commands are:
git status --short
git repack -ad
git commit-graph write --reachable
git commit-graph verify
git gc --no-prune
repack -a considers reachable objects for a new pack. -d removes packs superseded by the new pack and redundant loose objects already covered by packs. gc coordinates several repository-maintenance tasks; --no-prune explicitly retains loose-object deletion, making it a narrower observation step in this chapter’s lab. Maintenance for a production or collaborative repository needs an assessment of repository size, disk space, active Git processes, and the team’s release workflow.
gc, prune, and reachabilitygit gc is the higher-level entrypoint for ordinary maintenance and performs several housekeeping tasks. Its default pruning expiry does not immediately delete every unreachable object, leaving time for reflog- and mistake-recovery paths.
git prune is lower-level. The official manual recommends git gc in most cases because it handles pruning with other maintenance tasks. git prune --expire=now immediately processes eligible loose objects; if another Git process is writing the same repository, it increases corruption risk.
Before destructive maintenance, confirm at least:
git prune -n to inspect candidates first.Do not copy the lab’s git prune --expire=now to a collaborative repository. In this chapter’s lab it deletes exactly one blob that the script just wrote with no ref pointing to it, and the entire lab root is disposable.
Commit-graph is a serialized commit-graph file. git commit-graph write --reachable generates the graph from reachable refs, and git commit-graph verify cross-checks its contents against the object database. It can help Git perform some commit-graph walks faster. When changed-path data is enabled, it can also help path-specific history queries.
It does not alter commit, tree, blob, or branch semantics, and it does not replace packs, reflogs, or backups. Seeing a commit-graph file proves only that the auxiliary structure was written. A performance difference needs measurement under a fixed workload, machine, Git version, and configuration.
The lab creates 14 similar logical snapshots and performs physical maintenance in an isolated temporary repository:
cd labs/git-mental-model/10-storage
lab_path=$(bash setup.sh)
bash verify.sh "$lab_path"
sed -n '1,200p' "$lab_path/observations.txt"
diff -u "$lab_path/show-before.txt" "$lab_path/show-after.txt"
sed -n '1,120p' "$lab_path/count-before.txt"
sed -n '1,120p' "$lab_path/count-after.txt"
bash cleanup.sh "$lab_path"
verify.sh asserts real Git behavior:
git show for one stable commit is byte-for-byte identical before and after repack, commit-graph write, gc --no-prune, and pruning the lab orphan.git verify-pack reports at least one delta object.git commit-graph verify succeeds.git prune --expire=now removes only the deliberately unreachable blob, while the retained commit remains showable.The lab disables global Git configuration, signing, and caller hooks. It records real count-objects output without presenting bytes, object counts, or elapsed time as a performance conclusion. cleanup.sh deletes only a marked, structurally complete temporary root.
Run the complete self-test:
bash labs/git-mental-model/10-storage/test.sh
Choose commit C that is still referenced by main:
| Operation | git show C |
Physical object structure | Reachability |
|---|---|---|---|
git repack -ad |
unchanged | packs and deltas may change | C remains reachable from main |
| Write and verify commit-graph | unchanged | auxiliary file is added or updated | C remains reachable |
git gc --no-prune |
unchanged | several maintenance tasks may run | C remains reachable |
| Delete the only ref and wait for relevant expiry conditions | C can lose a reachable path | depends on later maintenance | affected by reflog, time, and configuration |
The changed condition is whether C retains a reachable ref path and whether maintenance has reached an allowed deletion expiry. Do not predict a fixed pack size, delta count, or duration; those values depend on the actual object set and Git’s compression choices.
When automation handles object maintenance, its handoff should include at least:
repository path and HEAD: <path> / <oid>
worktree and refs checked: clean state, branches, tags, reflog consideration
concurrent Git activity checked: <processes or scheduling boundary>
command and expiry parameters: <exact command>
logical invariant: git show <stable-commit> before/after comparison
physical observations: pack/index/commit-graph existence and count-objects output
deletion evidence: exact disposable object or dry-run candidate
This record keeps maintenance traceable: which logical content remained, which physical structures changed, and which deletion actions still have recovery boundaries. An agent must not expand “GC returned successfully” into “repository performance improved” or “all history is safe.”