gitconceptsversion-control

Git Concepts — Visual Guide

Git Concepts — Visual Guide

Merge

A B C D E M main HEAD main HEAD feature $ git merge feature ⬦ two parents

Initial State

Two branches co-exist: main (blue) has commits A→B→C; feature (green) diverged from B and added D→E.

Step 1 of 3

Key Points

  • Merge preserves the complete history of both branches — nothing is rewritten
  • A merge commit has two parents, recording exactly where the branches joined
  • Fast-forward merge: if no divergence, Git just moves the branch pointer — no merge commit needed
  • Use merge when you want to keep a clear record of feature branch lifetimes
  • Conflicts are resolved once, at merge time, then committed

Commands

# Switch to target branch and merge
git switch main
git merge feature

# Force a merge commit (no fast-forward)
git merge —no-ff feature

# Abort a conflicted merge
git merge —abort

Rebase

BEFORE REBASE A B C D E F E' F' main HEAD feature feature HEAD orphaned $ git rebase main

Initial State

feature branch (E, F) diverged from B. main has since moved to D. We want to replay feature commits on top of D.

Step 1 of 4

Key Points

  • Rebase replays your commits onto a new base — commits get new hashes (E→E’, F→F’)
  • History becomes linear — easier to read with git log
  • Golden rule: never rebase shared/public branches — rewriting history breaks everyone else’s clone
  • Conflicts are resolved per-commit, one at a time, as each is replayed
  • Old commits (E, F) become unreachable and are eventually garbage-collected

Commands

# Rebase feature onto latest main
git switch feature
git rebase main

# Continue after resolving conflicts
git rebase —continue

# Abort and return to original state
git rebase —abort

# Then fast-forward merge (clean history)
git switch main
git merge —ff-only feature

Squash

BASE W1 wip: init W2 fix typo W3 more work W4 cleanup W5 final fix feat: done 5 messy WIP commits 1 clean commit $ git rebase -i HEAD~5

Before Squash

5 WIP commits clutter the branch history with trivial messages that add noise to the project log.

Step 1 of 3

Key Points

  • Squash combines multiple commits into one — useful before merging a PR to keep history clean
  • git rebase -i HEAD~N opens an editor where you mark commits as squash or fixup
  • git merge --squash feature merges all feature changes as one unstaged diff, then you commit
  • Like rebase, squash rewrites history — only do it on your local/feature branches before merge
  • Squash = cleaner git log, but you lose granular commit-level rollback ability

Commands

# Interactive rebase — squash last 5 commits
git rebase -i HEAD~5
# In editor: change ‘pick’ → ‘squash’ or ‘s’

# Squash entire branch into one commit on main
git switch main
git merge —squash feature
git commit -m “feat: complete feature”

Cherry-Pick

A B C D E F G H D' main HEAD feature HEAD 🍒 pick this feature HEAD

Identify the Commit

Commit D on main contains a critical bug fix. We need it on the feature branch without merging all of main.

Step 1 of 3

Key Points

  • Cherry-pick copies the changes from a specific commit and applies them to the current branch
  • The new commit D’ gets a new hash but contains identical changes to D
  • The original commit D stays unchanged on main — it is not moved or deleted
  • Common use: apply a hotfix to both main and a release branch simultaneously
  • Duplicate commits can cause confusion on future merges — use sparingly

Commands

# Find the commit hash you want
git log —oneline main
a1b2c3d D: fix critical null pointer

# Cherry-pick onto current branch
git switch feature
git cherry-pick a1b2c3d

# Pick multiple commits
git cherry-pick a1b2c3d..f6e5d4c

# Abort if conflicts arise
git cherry-pick —abort

Fetch

REMOTE (origin) LOCAL REPO R1 R2 R3 R4 R5 origin/main R1 R2 R3 main HEAD R4 R5 origin/main git fetch 2 new commits on remote

Remote Is Ahead

Remote has 2 new commits (R4, R5) that don’t exist locally yet. Your local main is behind.

Step 1 of 3

Key Points

  • fetch downloads remote commits into origin/main but does NOT move your local main
  • It is always safe — fetch never modifies your working tree or local branches
  • pull = fetch + merge (or fetch + rebase with --rebase)
  • After fetching, use git log origin/main to inspect what arrived before integrating
  • Best practice: fetch first, review, then merge/rebase deliberately

Commands

# Download without touching local branches
git fetch origin

# Inspect what arrived
git log origin/main —oneline

# Integrate via merge
git merge origin/main

# Or: fetch + rebase in one step
git pull —rebase

# See divergence at a glance
git status
Your branch is behind ‘origin/main’ by 2 commits

Reset

--soft --mixed (default) --hard A B C HEAD HEAD Working Directory ✓ changes kept Staging / Index ✓ changes kept HEAD (Repository) ↩ moved to A at C A B C HEAD HEAD Working Directory ✓ changes kept Staging / Index unstaged ↩ cleared HEAD (Repository) at C ↩ moved to A A B C HEAD HEAD Working Directory changes present ✕ DISCARDED cannot recover Staging / Index staged changes ✕ DISCARDED HEAD (Repository) at C ↩ moved to A

Before Reset

HEAD is at commit C. We run git reset A in three different modes — each one affects a different number of layers.

Step 1 of 2

Key Points

  • —soft: moves HEAD only. Your staged changes and working files are untouched — useful to re-commit with a better message
  • —mixed (default): moves HEAD and clears the index. Changes return to unstaged in your working directory
  • —hard: moves HEAD, clears index AND discards all working directory changes — this is destructive and irreversible
  • All three modes rewrite local history — never reset commits already pushed to a shared branch
  • Recovery: git reflog can find lost commits after soft/mixed. Hard resets may be unrecoverable

Commands

# Move HEAD back 2 commits, keep staged
git reset —soft HEAD~2

# Move HEAD back, unstage changes (default)
git reset HEAD2
git reset —mixed HEAD
2

# ⚠ Discard EVERYTHING — cannot undo
git reset —hard HEAD~2

# Find lost commits after reset
git reflog

Stash

Working Directory Stash Stack git stash git stash pop app.ts modified styles.css modified api.ts modified ✓ Clean safe to switch branches stash@{0} WIP on feature: app.ts, styles.css, api.ts empty ⚠ uncommitted changes 1 entry saved ⚠ changes restored

Dirty Working Directory

You have uncommitted changes in 3 files but need to switch branches urgently. You can’t commit yet — stash saves the day.

Step 1 of 4

Key Points

  • Stash saves your dirty working directory and index to a stack, then reverts to a clean HEAD state
  • The stash is a stack — multiple stashes can be saved, stash@{0} is always the most recent
  • git stash pop applies the top stash AND removes it; git stash apply keeps the stash entry
  • Stash works across branches — stash on feature, switch to main, pop on main (with care)
  • Untracked files are NOT stashed by default — use -u flag to include them

Commands

# Save dirty state, clean working dir
git stash
git stash push -m “WIP: login form”

# Include untracked files
git stash -u

# List all stashes
git stash list
stash@{0}: WIP on feature: login form

# Apply and remove top stash
git stash pop

# Apply without removing
git stash apply stash@{0}

Enjoyed this post?

Get the next one in your inbox — only when I ship something worth reading.

Newsletter form not configured.

Or follow on Substack for the newsletter.

Comments via GitHub Discussions

Comments not configured. Set GISCUS env vars to enable.