Introduction
A Git commit references zero, one, or several parent commits. The whole history is a directed acyclic graph (DAG) of these parent links. Branches and tags are simply labels on nodes of this graph.
Anatomy of a parent link
git cat-file -p HEAD
# tree 9f1a...
# parent b2c3...
# parent d4e5... (only on merge commits)
# author Ada ...
# committer Ada ...
One parent: linear history. Two parents: a merge. Zero parents: a root commit. Three or more: an octopus merge.
Walking the graph
git log --oneline --graph --all
git log --first-parent # follow only first parents
git log --topo-order
git log --date-order
--first-parent on main is great for seeing the "trunk" of history without merge-side noise.
Ancestry queries
git merge-base A B # most recent common ancestor
git merge-base --is-ancestor X Y # exit 0 if X is ancestor of Y
git rev-list A..B --count # commits in B not in A
Range syntax
A..B: commits reachable from B but not A.A...B: symmetric difference.^A B: same asA..B.B --not A: same asA..B.
git log main..feature
git log main...feature --left-right --oneline
The commit-graph file
Since Git 2.18, Git can write a commit-graph file that pre-computes ancestry data for huge repos:
git commit-graph write --reachable
git config --global core.commitGraph true
git config --global gc.writeCommitGraph true
This dramatically speeds up git log --graph, git merge-base, and reachability checks.
Octopus merges
A single merge commit can have many parents:
git merge feature1 feature2 feature3
Octopus merges only succeed without conflicts. They are useful for integration branches in some workflows but rarely needed.
Visualizing the graph
git log --oneline --graph --decorate --all
gitk --all
git log --pretty=format:'%h %p %s'
%p prints parent SHAs.
Generation numbers
The commit-graph file stores a generation number per commit: 1 for root commits, max(parent generations) + 1 otherwise. Generation numbers turn many ancestry queries into O(log n) operations. Modern Git (2.27+) writes corrected commit dates and generation v2 numbers when the commit-graph is enabled:
git commit-graph write --reachable --changed-paths
git config gc.writeCommitGraph true
git config core.commitGraph true
git log --oneline --graph -n 5
--changed-paths additionally records a Bloom filter per commit listing changed paths, dramatically speeding up path-limited git log queries.
Common mistakes
Picturing branches as separate stacks of commits; they are labels on nodes that may share ancestors. Using HEAD~3 on a merge commit and being surprised: ~ always follows the first parent. Use ^2, ^3 to jump to other parents. Forgetting that rebase rewrites graph topology by creating new commits with new parent pointers; old commits live on in the reflog. Finally, thinking the commit graph stores diffs; it stores trees and parents only. Diffs are computed on demand by comparing trees.