foundations · level 1

Git Basics

Commits, branches, merge vs rebase, and reading a diff.

150 XP

Git Basics

Git tracks changes to files over time. Every time you run git commit, git takes a snapshot of every tracked file and stores it permanently. Nothing in git's history is ever deleted — commits are append-only.

Analogy

Git is like taking a photo of your entire desk every time you finish a task. Each photo captures the position of every pen, paper, and coffee cup — not just what moved. When something breaks, you compare today's photo to yesterday's and see exactly what shifted. Branches are parallel photo rolls — "the desk if I'd gone with Plan A" versus "the desk if I'd gone with Plan B" — and merging is reconciling the two rolls into one final arrangement.

Commits as snapshots

A commit is not a diff. It is a full snapshot of the repository at that moment, plus a pointer to the parent commit before it. What looks like a "change" in git diff is computed on the fly by comparing two snapshots.

Each commit has:

Field What it holds
SHA A 40-character hash that uniquely identifies the commit
Tree A pointer to the root directory snapshot
Parent The commit that came immediately before
Message A human-readable description
Author Name, email, and timestamp

The SHA is not random — it is a SHA-1 hash of the content, the parent SHA, the author, the message, and the timestamp. Change any of those and you get a completely different commit SHA.

Branches as labels

A branch is nothing more than a named pointer to a commit. When you run git checkout -b feature, git creates a text file containing the current commit SHA. When you make a new commit on that branch, git moves the pointer forward.

HEAD is a special pointer that usually points to a branch (not directly to a commit). When HEAD points at a branch, it is called being "on" that branch.

main ──── A ──── B ──── C    ← HEAD points here when on main
                 └──── D     ← feature branch (a pointer to D)

Merge vs rebase

Both integrate changes from one branch into another. They differ in how they write history.

Merge creates a new merge commit with two parents. The history stays exactly as it happened.

main: A ── B ── C ── M      (M has parents C and D)
                 \──/
feature: A ── B ── D

Rebase replays commits from your branch on top of the target branch. Each commit is rewritten (new SHA, new parent), so history looks linear.

Before: main A ── B ── C
                   feature: D ── E

After rebase: main A ── B ── C ── D' ── E'

Rebase rewrites history. Never rebase commits that have already been pushed to a shared branch — other people's clones still have the old SHAs and their next git pull will be confusing.

Reading a diff

git diff shows changes between two snapshots. Each changed file gets a header, then hunks.

diff --git a/app.py b/app.py
index 4b825dc..3e4a1fb 100644
--- a/app.py
+++ b/app.py
@@ -3,6 +3,7 @@ def greet(name):
     return f"Hello, {name}"

+def farewell(name):
+    return f"Goodbye, {name}"
+
 if __name__ == "__main__":
     print(greet("world"))
  • --- is the old version, +++ is the new version.
  • Lines starting with + were added; lines starting with - were removed.
  • The @@ header shows which line numbers the hunk starts at in both versions.

Key commands

git log --oneline --graph    # visual commit graph
git diff HEAD~1              # diff against the parent commit
git diff main..feature       # diff between two branches
git show <sha>               # show one commit's changes
git bisect start             # binary search for a regression