Git Basics
Commits, branches, merge vs rebase, and reading a diff.
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