🪴 GoDeep
Search
← Computer Science

Version control with Git

How Git models history as snapshots in a graph, why branches are cheap pointers, and how merge, rebase and good commits keep history useful.

13 cards · 7 quiz questions · 9 min read

Version control is the memory of a software project. It lets many people change the same code without trampling each other, lets you undo mistakes, and, used well, lets you answer questions like “when did this break, and why did we do it this way?” Git dominates the field, and most of its apparent complexity melts away once you understand the small data model underneath.

Commits are snapshots, not diffs

It is natural to think of a commit as “the changes I made”. Git does not model it that way. A commit is a snapshot: it points to a tree describing the complete state of every tracked file, together with an author, a message, and a pointer to its parent commit.

Every object in Git (file contents, directory trees, commits) is named by a hash of its contents. That has two nice consequences:

  • Deduplication. An unchanged file in a new commit has the same hash, so it is shared, not copied.
  • Integrity. Changing anything in an old commit changes its hash, and therefore the hash of every descendant. History is tamper-evident.

When you run git diff or git show, Git computes the difference between two snapshots on the fly. (Internally, packfiles do use delta compression to save space, but that is a storage optimisation, not the conceptual model.)

History is a graph

Because each commit points to its parent, history forms a directed acyclic graph. Ordinary commits have one parent; merge commits have two or more. When two people work at the same time from the same starting point, the graph forks; a merge joins it back together.

Branches are movable pointers

A branch is not a folder or a copy of the code. It is a tiny reference containing one commit ID. When you commit on main, Git creates the new commit and moves the main pointer to it. HEAD is a further pointer saying which branch you currently have checked out.

This is why Git branches are so cheap to create and delete, and why workflows built on short-lived feature branches are practical.

git switch -c fix-login   # create a pointer and move HEAD to it
git commit -am "Handle expired session tokens"
git switch main
git merge fix-login

Merge versus rebase

When two branches have diverged, there are two ways to combine them.

Merge creates a new commit with two parents. No existing commit changes, and the history truthfully records that work happened in parallel. The cost is a busier graph. If the target branch has no new commits of its own, Git simply fast-forwards the pointer and no merge commit is needed.

Rebase takes your commits and replays them one by one on top of another base, producing new commits with new hashes. The result reads as a straight line, as if you had started from the latest code. The cost is that the original commits are replaced.

The practical rule: rebase your own private work freely, but do not rewrite history that others have already built on.

Reasonable teams differ. Some rebase feature branches before merging to keep main linear; some squash each pull request into one commit; some merge everything to preserve context. Any of these can work. What matters is a consistent policy and never force-pushing over a shared branch without warning.

Pull requests and code review

On most teams, changes reach the main branch through a pull request (or merge request). Mechanically it is a request to merge one branch into another, but its real value is social: it is where automated tests run, where reviewers catch bugs and design problems, and where knowledge spreads. Software Engineering at Google stresses that review is as much about shared understanding and consistency as about finding defects.

Small pull requests get better reviews. A 50-line change gets careful scrutiny; a 2,000-line change tends to get a skim and an approval.

Writing commits for your future self

A good commit is one logical change that builds and passes tests. A good message has a short imperative summary, then a body explaining why:

Retry payment webhook on 503 responses

The provider returns 503 during its nightly maintenance window,
and we were dropping those events. Retry up to 3 times with backoff.

The diff already shows what changed. Only the message can preserve the reasoning, which is exactly what a maintainer needs two years later when a line of code looks strange.

Readable history as a debugging tool

Suppose a test that passed last month now fails, and a few hundred commits have landed since. git bisect performs a binary search: you mark a good commit and a bad one, Git checks out the midpoint, and you report whether it is good or bad. Each step halves the range, so around 1,000 commits need only about ten checks. With git bisect run and a test script, the whole search is automatic.

Bisect only works well if history cooperates. If commits do not build, or one commit mixes a refactor, a feature and a dependency upgrade, the “culprit” it finds tells you little. Small, coherent, well-described commits turn history into a searchable record of decisions, which is the real payoff of version control discipline.

Key takeaways

  • A commit is a snapshot of the whole tree plus parent pointers; history is a graph.
  • Branches are cheap movable pointers, not copies.
  • Merge preserves true history; rebase rewrites for linearity. Do not rewrite shared history.
  • Pull requests are a review and knowledge-sharing process, and small ones work best.
  • Focused commits with explanatory messages make tools like git bisect powerful.

Sources

  • Scott Chacon & Ben Straub — Pro Git book Free official book; chapters 3 and 10 cover branching and Git internals.
  • Titus Winters, Tom Manshreck & Hyrum Wright — Software Engineering at Google book Chapters on version control and code review at scale.
  • Martin Fowler — martinfowler.com website Articles on branching patterns and continuous integration.