Computing and the Command Line

References and the Commit Graph

A reference is a name that holds a commit ID: branches in refs/heads, tags in refs/tags, remote-tracking branches in refs/remotes, and HEAD, which usually names a branch. Lightweight and annotated tags, pushing tags, and git describe. Packed refs. The commit graph that parents form, with ^ and ~ for walking it and merge-base for where branches split; reachability, which decides what git keeps; dangling commits, git fsck, and when git gc finally deletes them.

  • 10 min
  • 9 steps
  • 3 questions
  • Lesson 41 of 80

In this lesson

  1. Refs are names for commits
  2. HEAD
  3. Tags
  4. Packed refs
  5. The commit graph
  6. Walking the graph
  7. What git keeps
  8. Your turn
  9. So

Refs are names for commits

Commit IDs are unwieldy, so git lets you store one under a name. These names are references, or refs, and in the usual layout each is a small file under .git/refs holding one ID 1:

me@linuxbox:~/garden$ cat .git/refs/heads/main
04fa6fb7dda18fbd9d43b95f1aaa6ed4ed5a8d99
me@linuxbox:~/garden$ git rev-parse main
04fa6fb7dda18fbd9d43b95f1aaa6ed4ed5a8d99

That’s all a branch is, as module 3 said: a file with a commit ID in it 1. Making a branch writes one more file; committing rewrites the file for your current branch. There are three kinds of refs, by folder:

  • refs/heads/: your branches.
  • refs/tags/: tags.
  • refs/remotes/: remote-tracking branches like origin/main (module 4). They’re read-only: git moves them when you fetch or push, never when you commit 1.

git rev-parse turns any name, branch, tag, or HEAD~2, into the full ID.

Pro Git warns against editing ref files directly; the plumbing command git update-ref does it safely, and records the move in the reflog 1.

HEAD is one more ref, in .git/HEAD. Usually it’s a symbolic reference: instead of an ID, it holds the name of another ref 1:

me@linuxbox:~/garden$ cat .git/HEAD
ref: refs/heads/main
me@linuxbox:~/garden$ git symbolic-ref HEAD
refs/heads/main
me@linuxbox:~/garden$ git switch -c fence
Switched to a new branch 'fence'
me@linuxbox:~/garden$ cat .git/HEAD
ref: refs/heads/fence

That’s how git knows which branch to move when you commit: it makes the new commit with HEAD’s commit as the parent, then updates whatever branch HEAD names 1. Switching branches just rewrites this one line (and your files).

In detached HEAD (module 3), .git/HEAD holds a commit ID instead of a branch name 1:

me@linuxbox:~/garden$ git switch --detach HEAD~1
HEAD is now at cc36fee Add beans
me@linuxbox:~/garden$ cat .git/HEAD
cc36feeb19d92e3a7d49b1db9bffe3c78a4a8959

Commits made now have no branch to move, which is why they’re easy to lose.

Quick check

What’s in .git/HEAD while you’re on the main branch?

Tags

A tag names a commit that never moves, such as a release or “the version that worked.” There are two kinds 1:

A lightweight tag is just a ref in refs/tags with a commit ID in it, a branch that doesn’t move:

me@linuxbox:~/garden$ git tag v0.1 HEAD~1
me@linuxbox:~/garden$ cat .git/refs/tags/v0.1
cc36feeb19d92e3a7d49b1db9bffe3c78a4a8959

An annotated tag, made with -a and a message, is a full object, a fourth type alongside blobs, trees, and commits. The ref points to the tag object, which points to the commit and records who tagged it, when, and why 1:

me@linuxbox:~/garden$ git tag -a v1.0 -m "Spring planting plan"
me@linuxbox:~/garden$ cat .git/refs/tags/v1.0
c1a9ca46a1c3581e92a2adcfd39ffe27cd0b4944
me@linuxbox:~/garden$ git cat-file -t v1.0
tag
me@linuxbox:~/garden$ git cat-file -p v1.0
object 04fa6fb7dda18fbd9d43b95f1aaa6ed4ed5a8d99
type commit
tag v1.0
tagger Me <me@example.com> 1791216240 -0500

Spring planting plan

Pro Git recommends annotated tags for anything you’ll share 1; lightweight ones suit private, temporary labels. git tag lists tags, and you can use a tag anywhere you’d use a commit ID: git switch --detach v1.0, git diff v1.0 main. git show-ref lists every ref with its ID:

me@linuxbox:~/garden$ git show-ref
04fa6fb7dda18fbd9d43b95f1aaa6ed4ed5a8d99 refs/heads/fence
04fa6fb7dda18fbd9d43b95f1aaa6ed4ed5a8d99 refs/heads/main
cc36feeb19d92e3a7d49b1db9bffe3c78a4a8959 refs/tags/v0.1
c1a9ca46a1c3581e92a2adcfd39ffe27cd0b4944 refs/tags/v1.0

Two things to know about tags in practice:

  • git push doesn’t send tags. Push one with git push origin v1.0, or all of them with git push origin --tags 1.
  • git describe names the current commit relative to the newest annotated tag before it 2:

    me@linuxbox:~/garden$ git describe
    v1.0-2-g9cc8b1b
    

    That reads: two commits after v1.0, at commit 9cc8b1b (the g stands for git) 2. Scripts use it to stamp a version on a build.

Packed refs

One file per ref gets slow with thousands of tags, so git gc (or git pack-refs) moves them into a single file, .git/packed-refs 1:

me@linuxbox:~/garden$ git pack-refs --all
me@linuxbox:~/garden$ ls .git/refs/heads .git/refs/tags
.git/refs/heads:

.git/refs/tags:
me@linuxbox:~/garden$ cat .git/packed-refs
# pack-refs with: peeled fully-peeled sorted 
e96af3cfd72c5017c6495d7f45f4a3066cbd3a12 refs/heads/fence
05c397d6f8cad48fa356c67233c2c244d0c6e918 refs/heads/main
cc36feeb19d92e3a7d49b1db9bffe3c78a4a8959 refs/tags/v0.1
c1a9ca46a1c3581e92a2adcfd39ffe27cd0b4944 refs/tags/v1.0
^04fa6fb7dda18fbd9d43b95f1aaa6ed4ed5a8d99

The ^ line under v1.0 is the commit that annotated tag points to 1. When a ref changes later, git writes a fresh file under refs/ again, and it checks there first, then in packed-refs 1. That’s another reason to use git commands rather than reading .git/refs yourself: a branch can exist without a file there.

Git 3.0 plans a bigger change: a storage format called reftable, which doesn’t keep each ref as its own file, becomes the default for new repositories 3. The commands don’t change; only what’s in .git does.

Left, what .git holds: .git/HEAD holds ref: refs/heads/main; refs/heads/main holds the commit ID 05c397d6f8ca...; refs/tags/v0.1 holds cc36feeb19d9..., a lightweight tag; refs/tags/v1.0 holds c1a9ca46a1c3..., a tag object; refs/remotes/origin/main is a read-only bookmark. The tag object c1a9ca4 contains object 04fa6fb... type commit, tag v1.0, tagger Me, and the message Spring planting plan. Right, the commit graph: main points at 05c3, Merge branch 'fence', which has two parents, HEAD^1 9cc8 Add tomatoes and HEAD^2 e96a Plan the fence. Below 9cc8 is HEAD~2, c933 Add squash, then 04fa Add garlic, the merge base of main and fence, which e96a also points to. Off to the side, 29df points to the merge commit but nothing points to it: it's dangling, no name reaches it. Key: ^n is the nth parent of a merge; ~n is first parent, n times; git fsck --no-reflogs finds dangling commits.
Names point at commits; parents link commits into a graph; whatever no name reaches is garbage, eventually. Credit: StudyCorner diagram · CC BY 4.0 · Source

The commit graph

Each commit names its parents, so commits link into a graph, running from each commit back to the first. A normal commit has one parent; a merge commit has two (or more) 1:

me@linuxbox:~/garden$ git log --oneline --graph
*   05c397d Merge branch 'fence'
|\  
| * e96af3c Plan the fence
* | 9cc8b1b Add tomatoes
* | c93309a Add squash
|/  
* 04fa6fb Add garlic
* cc36fee Add beans
* fc026ad Plan the beds
me@linuxbox:~/garden$ git cat-file -p HEAD
tree ac1941be7fbfb846b2f5220b16ee809da8d36d82
parent 9cc8b1b2e72adde0d0145a0b9e403d3fec3ca7ad
parent e96af3cfd72c5017c6495d7f45f4a3066cbd3a12
author Me <me@example.com> 1791216480 -0500
committer Me <me@example.com> 1791216480 -0500

Merge branch 'fence'

The first parent is the branch you were on (main), the second the branch you merged in (fence) 1. Arrows only point backward: a commit knows its parents, never its children. That’s why git log starts from a ref and walks back.

Walking the graph

Two suffixes walk it 4:

  • ^n means the nth parent. HEAD^ (same as HEAD^1) is the first parent; HEAD^2 is a merge’s second parent.
  • ~n means go back n generations, following first parents. HEAD~2 is the same as HEAD^^.
me@linuxbox:~/garden$ git log --oneline -1 HEAD^1
9cc8b1b Add tomatoes
me@linuxbox:~/garden$ git log --oneline -1 HEAD^2
e96af3c Plan the fence
me@linuxbox:~/garden$ git log --oneline -1 HEAD~2
c93309a Add squash

HEAD^ and HEAD~ both mean the first parent. The numbers differ: ^2 picks a second parent, which only merges have, while ~2 goes back two generations. Don’t confuse either with HEAD@{2} (module 5), which is where HEAD was two moves ago, from the reflog.

git merge-base finds where two branches split, their newest common ancestor. It’s the base a three-way merge compares against (module 3):

me@linuxbox:~/garden$ git merge-base main~1 fence
04fa6fb7dda18fbd9d43b95f1aaa6ed4ed5a8d99

The double-dot range from module 5, main..fence, is defined by the graph too: commits reachable from fence but not from main 1.

Quick check

HEAD is a merge commit. Which names the commit from the branch that was merged in?

What git keeps

A commit is reachable if you can get to it by starting at some ref and following parents. git log shows what’s reachable from HEAD; git log --all from every ref. Reachability is also what git uses to decide what to keep.

Delete a branch whose commits aren’t anywhere else, and they become unreachable. git branch -D prints the ID on its way out, which is the quickest way back:

me@linuxbox:~/garden$ git branch -D idea
Deleted branch idea (was 29df69d).

git branch idea 29df69d would restore it. If you’ve lost that line, the reflog (module 5) still has the commit, and git fsck, which checks the repository’s integrity, can list commits that nothing points to 1. Its --no-reflogs option doesn’t count reflog entries as a reason to keep a commit, so it finds commits that used to be on a branch 5:

me@linuxbox:~/garden$ git fsck --no-reflogs
dangling commit 29df69daf037e8a2a5ad25bdeec66990b0de8270

A dangling commit is one that’s in the database but that nothing uses 5. It isn’t deleted right away. Only git gc removes objects, and it tries hard not to remove anything still referenced by a branch, tag, remote-tracking branch, the index, or a reflog entry; unreferenced objects also get a grace period, two weeks by default 6. Reflog entries for unreachable commits last 30 days (module 5). Together, that gives you weeks to notice a mistake.

The flip side: anything reachable is kept forever. A large file or a password committed once stays in every clone’s history even after you delete it in a later commit 1. That’s why module 2 said to change a leaked secret rather than just delete the file.

Quick check

You deleted a branch with git branch -D and its commits aren’t on any other branch. What happens to them?

Your turn

Exercises

  1. In a practice repository, cat .git/HEAD, then switch branches and look again. Then try git switch --detach HEAD~1.
  2. Make a lightweight tag and an annotated tag. Compare git cat-file -t on each, and git cat-file -p on the annotated one.
  3. Make two more commits and run git describe. What do the parts mean?
  4. Run git pack-refs --all, then look at .git/refs/heads and .git/packed-refs. Does git branch still list your branches?
  5. Make a merge commit, then use git log --oneline -1 with HEAD^1, HEAD^2, and HEAD~2.
  6. Make a commit on a throwaway branch, delete it with git branch -D, find the commit with git fsck --no-reflogs, and bring the branch back.
Answers
  1. ref: refs/heads/<branch>, changing as you switch. Detached, it’s a bare 40-character ID.
  2. The lightweight tag’s type is commit, since it points straight at one; the annotated tag’s is tag, and -p shows object, type, tag, tagger, and the message.
  3. Like v1.0-2-g9cc8b1b: the newest annotated tag, the number of commits since, and g plus the current commit’s short ID.
  4. The folders are empty and the refs are in packed-refs, but git branch lists them all as before.
  5. HEAD^1 is the last commit on your branch before the merge, HEAD^2 the merged branch’s tip, and HEAD~2 two commits back along your own branch.
  6. dangling commit <id>; then git branch <name> <id>.

So

A ref is a name holding a commit ID: branches in refs/heads, tags in refs/tags, remote-tracking branches in refs/remotes, sometimes packed into .git/packed-refs. HEAD usually names a branch (ref: refs/heads/main) and holds a bare ID when detached. Lightweight tags are plain refs; annotated tags are objects with a tagger and message, pushed explicitly with git push origin <tag>. Parents link commits into a graph: ^n picks a parent, ~n walks back first parents, and git merge-base finds where branches split. Git keeps everything reachable from a ref or the reflog; dangling commits linger for weeks before git gc removes them.

Lesson complete

Nice work.

1day streak
0/1today's goal
–correct

Up next · 12 min

Your Dotfiles in Git

Next lesson
Sources for this lesson
  1. 1
    Scott Chacon, Ben Straub. Pro Git, 2nd edition. Apress; free online at git-scm.com. 2014. verifiedFree CC BY-NC-SA 3.0 book, maintained online. Ch. 1: version control; Git's 2005 origin when the Linux kernel lost free use of BitKeeper; snapshots, not differences (unchanged files stored once); nearly every operation local; integrity through 40-character SHA-1 checksums; the three states (modified, staged, committed) and three areas (working tree, staging area or index, .git directory); first-time setup with system/global/local config levels, user.name and user.email baked into commits, core.editor, git config --list --show-origin. Ch. 2: git init, status (and -s), add, diff and diff --staged, commit (-m, -a), .gitignore, log options, amending, undoing, remotes, tags, aliases. Ch. 3: branches as movable pointers, HEAD, merging and conflicts, remote branches, rebasing and its rule. Ch. 7: reset demystified, stashing, revision selection. Ch. 8: core.autocrlf true on Windows, input on Linux and macOS. Ch. 10: objects (blob, tree, commit) and references.
  2. 2
    git-describe documentation. Git project (git-scm.com). verifiedFinds the most recent tag reachable from a commit and suffixes the number of additional commits and the abbreviated object name; the 'g' prefix stands for git. Without --all or --tags, only annotated tags are used.
  3. 3
    Upcoming breaking changes (BreakingChanges). Git project (git-scm.com). verifiedPlanned for Git 3.0, which has no release date yet: the default hash function for new repositories changes from sha1 to sha256 (SHA-1 deprecated by NIST in 2011; SHAttered 2017 produced two PDFs with the same hash), and the default reference storage format changes from 'files' to 'reftable', which does not use filesystem paths to encode reference names. No plan to deprecate sha1 repositories.
  4. 4
    gitrevisions: specifying revisions and ranges. Git project (git-scm.com). verified<rev>^<n> is the nth parent (^ alone is ^1); <rev>~<n> is the nth-generation ancestor following first parents, so rev~3 = rev^^^ = rev^1^1^1; ^0 is the commit itself.
  5. 5
    git-fsck documentation. Git project (git-scm.com). verifiedVerifies connectivity and validity of objects. --no-reflogs doesn't count commits referenced only by a reflog entry as reachable, to find commits that used to be in a ref; 'dangling' means present but never directly used; --lost-found writes dangling objects to .git/lost-found.
  6. 6
    git-gc documentation. Git project (git-scm.com). verifiedHousekeeping: packs objects and refs and prunes unreachable loose objects older than gc.pruneExpire (default 2 weeks ago). Keeps objects referenced by branches, tags, the index, remote-tracking branches, reflogs, and anything else under refs/.