← Back to Git series
🔀
Git & GitHub Deep Dive
interactive rebase · squash · cherry-pick · force-with-lease

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.

rebasecherry-picksquash
Duration
1-2 hours
Level
📊 Intermediate
Prerequisite
🎯 Chapter 6
Outcome
Polish history and push safely with --force-with-lease

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:

text
A ─ B ─ C ─ M (merge)
     \ D /

→ rebased:
A ─ B ─ C ─ D'        (D replayed on top of C)

2) Basic rebase

bash
git switch feature
git rebase main         # replay feature's commits on top of main

If conflicts: resolve → `git add` → `git rebase --continue`.

3) Interactive rebase

bash
git rebase -i HEAD~5    # let me edit the last 5 commits

An editor opens with verbs per commit:

VerbAction
`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

bash
git cherry-pick <SHA>           # apply that one commit here
git cherry-pick A^..B           # range

Great for backporting a hotfix from `main` to a release branch.

5) Force-pushing safely

bash
git push --force-with-lease     # refuses if remote moved unexpectedly

Prefer over `--force` (which can blow away teammates' commits).

Examples

Example 1 — `ex01_rebase.sh`: linear rebase

**Output**

text
=== rebase demo ===
feature commits replayed on top of main
result: linear history

Key: rebase makes the history *look* simple even if work was concurrent.

Example 2 — `ex02_squash.sh`: combine 3 commits into 1

**Output**

text
=== squash demo ===
3 small commits → 1 clean commit
new SHA, same content

Key: `pick / squash / squash` is the common pattern.

Example 3 — `ex03_cherry_pick.sh`: copy one commit

**Output**

text
=== cherry-pick demo ===
hotfix commit appears on release branch

Key: cherry-pick creates *new* commits with new SHAs — no history sharing.

Example 4 — `ex04_force_push.sh`: safe force push

**Output**

text
=== force-with-lease demo ===
push succeeds: local rewrite mirrors remote

Key: `--force-with-lease` is the polite way to overwrite remote history.

Common mistakes

  1. **Rebasing public branches** — `main` is shared; never rebase it. Rebase your feature branch instead.
  2. **`--force` (without `-with-lease`)** — can erase others' commits. Always use `--force-with-lease`.
  3. **Interactive rebase confusion** — read the verb list slowly. `fixup` is the common-case shortcut.
  4. **Cherry-pick from the wrong branch** — your `main` may not have the commit yet; fetch first.
  5. **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

bash
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.

ex01_rebase.shlinear rebase
CODE
#!/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"
▶ Output
=== rebase demo ===
feature commits replayed on top of main
result: linear history
ex02_squash.shcombine 3 commits into 1
CODE
#!/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"
▶ Output
=== squash demo ===
3 small commits → 1 clean commit
new SHA, same content
ex03_cherry_pick.shcopy one commit
CODE
#!/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"
▶ Output
=== cherry-pick demo ===
hotfix commit appears on release branch
ex04_force_push.shsafe force push
CODE
#!/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"
▶ Output
=== force-with-lease demo ===
push succeeds: local rewrite mirrors remote

📝 Exercises

Try them yourself first, then open the solution to compare.

Exercise 1

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.

Requirements
  • Filename: hw01.sh
Toggle solution
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"
Exercise 2

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.

Requirements
  • Filename: hw02.sh
Toggle solution
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"
Example code / lecture materials

All lecture materials and example code are openly available on GitHub.

View on GitHub ↗