Intuition

Version control solves a deceptively simple problem: how do multiple people change the same codebase without destroying each other’s work? The naive answer - numbered copies in shared folders - collapses at any real scale. Git’s answer is to model the entire history of a project as a directed acyclic graph (DAG) of immutable snapshots, where branching and merging are first-class operations rather than special cases.

Understanding Git means understanding the DAG. Every command - commit, branch, merge, rebase, cherry-pick - is an operation on this graph. Once the data model clicks, the commands stop feeling arbitrary.


Core Idea

The Object Model

Git stores four types of objects, all content-addressed by SHA-1 hash:

ObjectContains
BlobFile contents (no filename, no metadata).
TreeA directory listing - maps names to blobs or other trees.
CommitA tree pointer, parent commit pointer(s), author, timestamp, message.
TagA named pointer to a commit with optional annotation.

A commit is a snapshot, not a diff. Git computes diffs on the fly by comparing trees. This makes operations like checkout and log fast regardless of history length.

The DAG

Commits form a directed acyclic graph where each commit points to its parent(s):

A ← B ← C ← D       (main)
         ↖
          E ← F      (feature)
  • Linear history: each commit has one parent.
  • Branch point: C is the common ancestor of D and F.
  • Merge commit: has two parents, joining divergent lines.

Branches are just movable pointers to commits. Creating a branch is O(1) - it writes 41 bytes (a SHA reference). This cheapness is why Git encourages branching for everything.

Branching Strategies

StrategyHow it worksBest for
Trunk-basedEveryone commits to main; short-lived feature branches (hours, not weeks).CI/CD-heavy teams, small teams.
Git FlowLong-lived develop and main branches; feature, release, and hotfix branches.Versioned releases with formal QA.
GitHub FlowSingle main branch; feature branches + pull requests; deploy on merge.SaaS, continuous deployment.

Note

The best branching strategy is the one your team can actually follow. Complex models create ceremony that teams circumvent under deadline pressure - and circumvented process is worse than no process.

Merge vs Rebase

  • Merge creates a new commit with two parents, preserving the full branch topology.
  • Rebase replays commits onto a new base, producing a linear history but rewriting commit hashes.
# Merge: preserves branch structure
A ← B ← C ← M
         ↖  ↗
          D

# Rebase: linearizes
A ← B ← C ← D'

Warning

Never rebase commits that have been pushed and shared. Rewriting public history forces collaborators to reconcile divergent graphs - a painful, error-prone process.


Example

A typical feature workflow using GitHub Flow:

# 1. Create a branch from main
git checkout -b fix/login-timeout main
 
# 2. Make changes and commit
git add src/auth.py
git commit -m "Increase session timeout to 30 minutes"
 
# 3. Push and open a pull request
git push -u origin fix/login-timeout
gh pr create --title "Fix login timeout" --body "Session was expiring too quickly"
 
# 4. After review, merge (squash for clean history)
gh pr merge --squash
 
# 5. Clean up
git checkout main && git pull
git branch -d fix/login-timeout

Each step maps to a DAG operation: branch creation (new pointer), commit (new node), push (sync with remote graph), merge (join nodes), delete branch (remove pointer - commits remain).