Chapter 7 — Rebase and Cherry-pick
Tools to clean up history before sharing it. `rebase -i` lets you reorder, squash and edit commits. `cherry-pick` plucks one commit from anywhere. `--force-with-lease` lets you push rewritten history safely.
What you'll learn
- 1Use `git rebase` to update a feature branch on top of `main`.
- 2Use `git rebase -i` interactively to squash, reorder and reword commits.
- 3Use `git cherry-pick` to copy a single commit between branches.
- 4Use `git push --force-with-lease` for safe history rewrites.
Core Concepts
1) Why rebase?
`merge` preserves true history; `rebase` rewrites for a cleaner line. Compare:
A ─ B ─ C ─ M (merge)
\ D /
→ rebased:
A ─ B ─ C ─ D' (D replayed on top of C)2) Basic rebase
git switch feature
git rebase main # replay feature's commits on top of mainIf conflicts: resolve → `git add` → `git rebase --continue`.
3) Interactive rebase
git rebase -i HEAD~5 # let me edit the last 5 commitsAn editor opens with verbs per commit:
| Verb | Action |
|---|---|
| `pick` | Keep as-is (default) |
| `reword` | Edit the message only |
| `edit` | Stop to amend file/commit |
| `squash` | Merge with previous, combine messages |
| `fixup` | Like squash but drop this commit's message |
| `drop` | Remove the commit |
4) Cherry-pick
git cherry-pick <SHA> # apply that one commit here
git cherry-pick A^..B # rangeGreat for backporting a hotfix from `main` to a release branch.
5) Force-pushing safely
git push --force-with-lease # refuses if remote moved unexpectedlyPrefer over `--force` (which can blow away teammates' commits).
Examples
Example 1 — `ex01_rebase.sh`: linear rebase
**Output**
=== rebase demo ===
feature commits replayed on top of main
result: linear historyKey: rebase makes the history *look* simple even if work was concurrent.
Example 2 — `ex02_squash.sh`: combine 3 commits into 1
**Output**
=== squash demo ===
3 small commits → 1 clean commit
new SHA, same contentKey: `pick / squash / squash` is the common pattern.
Example 3 — `ex03_cherry_pick.sh`: copy one commit
**Output**
=== cherry-pick demo ===
hotfix commit appears on release branchKey: cherry-pick creates *new* commits with new SHAs — no history sharing.
Example 4 — `ex04_force_push.sh`: safe force push
**Output**
=== force-with-lease demo ===
push succeeds: local rewrite mirrors remoteKey: `--force-with-lease` is the polite way to overwrite remote history.
Common mistakes
- **Rebasing public branches** — `main` is shared; never rebase it. Rebase your feature branch instead.
- **`--force` (without `-with-lease`)** — can erase others' commits. Always use `--force-with-lease`.
- **Interactive rebase confusion** — read the verb list slowly. `fixup` is the common-case shortcut.
- **Cherry-pick from the wrong branch** — your `main` may not have the commit yet; fetch first.
- **Stopping mid-rebase and forgetting** — `git rebase --abort` returns you to the pre-rebase state.
Recap
- `merge` keeps truth, `rebase` keeps tidy. Use both wisely.
- `rebase -i` is the workshop where you polish before pushing.
- `cherry-pick` is "give me that one commit, please".
- `--force-with-lease` is the only force-push you should ever type.
Try it
cd src
chmod +x ex0*.sh
./ex01_rebase.sh
./ex02_squash.sh
./ex03_cherry_pick.sh
./ex04_force_push.sh💻 Examples
Runnable examples — see the output yourself.
#!/usr/bin/env bash
set -euo pipefail
REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "Demo User" && git config user.email "demo@example.com"
git config commit.gpgsign false
echo "base" > base.txt
git add . && git commit -q -m "A: initial commit"
# feature branch work
git switch -c feature
echo "feature 1" > feat1.txt
git add . && git commit -q -m "C: add feature 1"
echo "feature 2" > feat2.txt
git add . && git commit -q -m "D: add feature 2"
# main branch at commit
git switch main
echo "main add" > extra.txt
git add . && git commit -q -m "B: main extra work"
echo "=== rebase (before) graph ==="
git log --oneline --graph --all
# feature main using rebase
git switch feature
git rebase main
echo ""
echo "=== rebase (after) graph ( ) ==="
git log --oneline --graph --all
echo ""
echo "=== feature branch file list ==="
ls
rm -rf "$REPO"
=== rebase demo ===
feature commits replayed on top of main
result: linear history#!/usr/bin/env bash
set -euo pipefail
REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "Demo User" && git config user.email "demo@example.com"
git config commit.gpgsign false
echo "" > app.py
git add . && git commit -q -m "init: base"
# work commit (WIP)
echo "stage 1" >> app.py && git add . && git commit -q -m "wip: step 1"
echo "stage 2" >> app.py && git add . && git commit -q -m "wip: step 2"
echo "edit" >> app.py && git add . && git commit -q -m "fix: typo"
echo "stage 3" >> app.py && git add . && git commit -q -m "wip: step 3"
echo "=== squash (before) log (WIP commit 4) ==="
git log --oneline
# git rebase -i using , reset --soft using squash simulation
git reset --soft HEAD~4
git commit -m "feat: implement full feature (squashed 4 commits)
- step 1, 2, 3
- edit "
echo ""
echo "=== squash (after) log (1 using ) ==="
git log --oneline
echo ""
echo "=== final commit message ==="
git log -1 --format="%B"
rm -rf "$REPO"
=== squash demo ===
3 small commits → 1 clean commit
new SHA, same content#!/usr/bin/env bash
set -euo pipefail
REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "Demo User" && git config user.email "demo@example.com"
git config commit.gpgsign false
echo "main code" > main.txt
git add . && git commit -q -m "init"
# develop branch at commit
git switch -c develop
echo "feature A" > feature_a.txt && git add . && git commit -q -m "feat: feature A"
echo "hotfix" > hotfix.txt && git add . && git commit -q -m "fix: critical hotfix"
echo "feature B" > feature_b.txt && git add . && git commit -q -m "feat: feature B"
echo "feature C" > feature_c.txt && git add . && git commit -q -m "feat: feature C"
echo "=== develop branch log ==="
git log --oneline
# main at hotfix commit cherry-pick
git switch main
HOTFIX_HASH=$(git log develop --oneline | grep "critical hotfix" | awk '{print $1}')
echo ""
echo "=== cherry-pick: $HOTFIX_HASH ==="
git cherry-pick "$HOTFIX_HASH"
echo ""
echo "=== main log (hotfix ) ==="
git log --oneline
echo ""
echo "=== main file list (hotfix.txt add) ==="
ls
rm -rf "$REPO"
=== cherry-pick demo ===
hotfix commit appears on release branch#!/usr/bin/env bash
set -euo pipefail
WORKDIR=$(mktemp -d)
git init --bare -b main "$WORKDIR/remote.git" -q
git clone "$WORKDIR/remote.git" "$WORKDIR/repo" -q
cd "$WORKDIR/repo"
git config user.name "Demo User" && git config user.email "demo@example.com"
git config commit.gpgsign false
echo "initial file" > main.txt
git add . && git commit -q -m "init"
git push origin main -q
# feature branch at WIP commit (after) push
git switch -c feature
echo "WIP 1" >> main.txt && git add . && git commit -q -m "wip: step 1"
echo "WIP 2" >> main.txt && git add . && git commit -q -m "wip: step 2"
echo "WIP 3" >> main.txt && git add . && git commit -q -m "wip: step 3"
git push origin feature -q
echo "=== push (before) log (WIP commit 3) ==="
git log --oneline
# squash: WIP 3 → 1
git reset --soft HEAD~3
git commit -m "feat: complete feature"
echo ""
echo "=== squash (after) log (1) ==="
git log --oneline
echo ""
echo "=== --force-with-lease using (before) push ==="
git push --force-with-lease origin feature
echo ""
echo "=== remote feature branch check ==="
git log origin/feature --oneline
rm -rf "$WORKDIR"
=== force-with-lease demo ===
push succeeds: local rewrite mirrors remote📝 Exercises
Try them yourself first, then open the solution to compare.
Problem 1 (hw01.sh)
Goal: On a `feature` branch, make 4 small commits. Use `git rebase -i` to squash the last 3 into the first, then keep the cleaned commit on the branch.
- Filename: hw01.sh
▶Toggle solution
#!/usr/bin/env bash
set -euo pipefail
REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "Demo User" && git config user.email "demo@example.com"
git config commit.gpgsign false
echo "base" > base.txt && git add . && git commit -q -m "init: base"
git switch -c feature/work
echo "work content" > work.txt && git add . && git commit -q -m "feat: add work"
git switch main
echo "hotfix" > hotfix.txt && git add . && git commit -q -m "fix: hotfix"
echo "=== rebase (before) graph ==="
git log --oneline --graph --all
git switch feature/work
git rebase main
echo ""
echo "=== rebase (after) graph () ==="
git log --oneline --graph --all
git switch main
git merge feature/work
echo ""
echo "=== fast-forward merge (after) final log ==="
git log --oneline --graph --all
rm -rf "$REPO"
Problem 2 (hw02.sh)
Goal: Create `main` and `release` branches. Make a hotfix commit on `main`, then cherry-pick it onto `release` without bringing along the rest of `main`'s commits.
- Filename: hw02.sh
▶Toggle solution
#!/usr/bin/env bash
set -euo pipefail
REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "Demo User" && git config user.email "demo@example.com"
git config commit.gpgsign false
echo "main code" > main.txt && git add . && git commit -q -m "init"
git switch -c develop
echo "log feature" > login.py && git add . && git commit -q -m "feat: add login"
echo " " > security.py && git add . && git commit -q -m "fix: security patch"
echo "" > dashboard.py && git add . && git commit -q -m "feat: add dashboard"
echo "=== develop log ==="
git log --oneline
git switch main
SECURITY=$(git log develop --oneline | grep "security patch" | awk '{print $1}')
git cherry-pick "$SECURITY"
echo ""
echo "=== main log (security patch ) ==="
git log --oneline
echo ""
echo "=== main file list ==="
ls
rm -rf "$REPO"
All lecture materials and example code are openly available on GitHub.
View on GitHub ↗