Version Control System
A simplified Git: commits form a graph, branches are just movable labels pointing into that graph, and merging is "find where two labels diverged and combine what happened since." None of it needs a real diff algorithm to get the class model right.
Requirements
Functional
- Committing snapshots the current set of tracked files under a message, with a pointer back to the commit(s) it came from.
- A branch is a named pointer to a commit; committing on a branch moves that branch's pointer forward.
- Checking out a branch switches the working set to that branch's commit's snapshot.
- Merging branch B into branch A creates a new commit on A with two parents: A's tip and B's tip.
Non-functional
- Finding the common ancestor of two branches for a merge should walk the commit graph from both tips, not scan every commit ever made.
- Creating a branch must be a cheap pointer creation, not a copy of the repository's history.
Design
Commit is an immutable node with a snapshot and a list of parent commits (one parent for
a normal commit, two for a merge) - the entire history is just commits chained by parent
pointers, which is a graph, not a special "history" data structure. Branch is nothing
more than a name and a mutable reference to one Commit; Repository owns the branches
and does the walking.
- 1The caller commits on a named branch - it never touches a Commit object directly.
- 2A new commit is created with the branch's current tip as its single parent.
- 3The branch pointer moves forward to the new commit - the old commit is still reachable, just no longer the tip.
- 4To merge, the caller names two branches; the repository finds where they diverged.
- 5The merge commit is the one place a commit gets two parents instead of one.
Finding a merge base is a graph problem solved with two graph tools: walk back from each tip collecting ancestor commit ids, then intersect - the first commit found in both walks (closest to the tips) is the merge base.
Class diagram
Code
Design decisions
- Commits are immutable and only ever gain new commits pointing back at them - nothing is ever rewritten in place. A commit's parents, snapshot, and id never change once created; "undoing" a commit in a real system means creating a new commit that reverts it, never editing history, which is what keeps every branch's pointer trustworthy without a lock.
- A branch is a pointer, not a copy of history.
Branchholds oneCommitreference; creating a branch is assigning that reference to wherever the source branch currently points, which is why branching is fast regardless of how much history exists behind it. - A merge commit has two parents instead of the model trying to represent one "primary" history. Once a commit can have more than one parent, the commit graph is a DAG rather than a straight line, and every question about history ("what's the common ancestor," "what changed since we diverged") becomes a graph traversal instead of a special case.
- What's missing for a real system: this page models the commit graph and merge-base discovery, not the merge itself - actually combining two snapshots' content into one (line-level diffing, conflict markers) is a separate, much larger algorithm that sits on top of this structure rather than inside it; a real VCS would also need the object store itself to be content-addressed so identical file content across commits is stored once.