09 · Internals¶
← Overview · Prev: Hooks · Next: Resources →
Understanding the object model makes every command less magical.
The object database¶
Everything git stores is a content-addressed object, named by a hash of its content
(SHA-1 by default; SHA-256 in repos created with --object-format=sha256).
| Object | Contains |
|---|---|
| blob | The bytes of one file (no name, no permissions) |
| tree | A directory: list of (mode, name, hash) entries pointing at blobs and other trees |
| commit | Pointer to a root tree, parent commit hash(es), author + committer (name, email, timestamp), message |
| tag (annotated) | Pointer to an object, tagger, message, optional signature |
A commit is a snapshot of the whole tree, not a diff. Diffs are computed on demand. Identical content is stored once, so unchanged files cost nothing per commit.
Objects live in .git/objects/xx/yyyy… as zlib-compressed data ("loose" objects) or inside packfiles
(.git/objects/pack/) after git gc, where similar objects are delta-compressed.
Inspect objects (don't decompress by hand)¶
git cat-file -t <sha> # type: commit | tree | blob | tag
git cat-file -p <sha> # pretty-print contents
git cat-file -p HEAD^{tree} # the root tree of HEAD
git ls-tree -r HEAD # all files in HEAD with their blob hashes
git rev-parse HEAD # resolve a name to a full SHA
git count-objects -vH # loose/packed sizes
git verify-pack -v .git/objects/pack/*.idx | head
Refs: names for commits¶
- Branches:
.git/refs/heads/<name>— a file containing a commit SHA. A branch is just a movable pointer. - Remote-tracking:
.git/refs/remotes/origin/<name>— where the remote's branch was at your last fetch. - Tags:
.git/refs/tags/<name>. HEAD: usuallyref: refs/heads/<current-branch>; when it holds a raw SHA you are in detached HEAD.- Special refs:
ORIG_HEAD(where HEAD was before a risky operation),MERGE_HEAD,FETCH_HEAD,REBASE_HEAD.
Relative names: HEAD~1 (first-parent grandparent chain), HEAD^2 (second parent of a merge),
main@{yesterday}, @{u} (upstream), A..B (in B not A), A...B (in either, not both).
Anatomy of .git¶
.git/
├── HEAD current branch pointer
├── config repo-local config
├── index the staging area (binary)
├── objects/ the object database
├── refs/ heads/, remotes/, tags/
├── logs/ reflogs
├── hooks/ hook scripts (samples end in .sample)
├── info/exclude personal ignore rules
└── packed-refs refs compacted by gc
A bare repo has this content at its top level and no working tree.
The three trees¶
| Area | Holds | Commands that move data |
|---|---|---|
| Working tree | Files on disk | edit; git restore (from index/HEAD) |
| Index (staging area) | The proposed next commit | git add, git restore --staged |
| HEAD (last commit) | The last committed snapshot | git commit |
git status compares HEAD↔index (staged) and index↔working tree (unstaged).
git reset moves the branch pointer (--soft), then the index (--mixed), then the working tree (--hard).
Garbage collection and the reflog¶
Unreachable objects (e.g. commits dropped by a rebase) stay until they expire from the reflog (default
~90 days for reachable-in-reflog, 30 for unreachable) and git gc prunes them. That's why
reflog recovery works.
git gc # pack objects, prune expired ones
git fsck # verify integrity, list dangling objects
Plumbing vs porcelain¶
Porcelain commands (commit, checkout, merge) are the friendly interface. Plumbing commands
(hash-object, write-tree, commit-tree, update-ref) are the building blocks. Scripts should prefer
stable, machine-readable forms: git status --porcelain, git for-each-ref, git rev-list.
Hand-building a commit shows how simple the model is:
blob=$(echo 'hello' | git hash-object -w --stdin)
git update-index --add --cacheinfo 100644,$blob,hello.txt
tree=$(git write-tree)
commit=$(echo 'first' | git commit-tree $tree)
git update-ref refs/heads/demo $commit
Reading: https://matthew-brett.github.io/curious-git/index.html · https://jwiegley.github.io/git-from-the-bottom-up