While merge merges the history of two branches, rebase merges the commits elsewhere.
Make history linear. cherry-pick selects only specific commits from other branches and applies them to the current branch.
These two tools help you organize your commit history and selectively pull in only the changes you need.
Learning Objectives
- You can create a linear history by changing the branch base with
git rebase. - You can squash, reword, and drop commits with
git rebase -i(interactive). - With
git cherry-pick, you can apply a specific commit to the current branch. - You can resolve conflicts during rebase/cherry-pick and continue (
--continue). - You can safely force push using
--force-with-lease.
Key concepts
1) git rebase
before rebase:
main: A---B
\
feature: C---D
after: git rebase main (run on feature branch)
main: A---B
\
feature: C'--D' (new hash)
git switch feature
git rebase main #Concatenate features onto main
2) Interactive Rebase
git rebase -i HEAD~3 #Edit the last 3 commits
#In the editor:
pick a1b2c3d feat: first
squash b2c3d4e fix: typo #← pick → squash (merge into previous commit)
reword c3d4e5f feat: second #← Edit message only
drop d4e5f6a wip: temp #← Delete commit
| command | Meaning |
|---|---|
pick |
stay the same |
squash/s |
Merge to previous commit (merge messages as well) |
fixup/f |
Merge to previous commit (discard message) |
reword/r |
Edit message only |
drop/d |
delete commit |
edit/e |
stop commit and edit |
3) git cherry-pick
git cherry-pick abc1234 #Apply one specific commit
git cherry-pick abc1234 def5678 #multiple commits
git cherry-pick abc1234..def5678 #range (excluding abc, including def)
git cherry-pick --no-commit abc1234 #Staging only, no commit
4) force-with-lease
git push --force #Ignore remote status and force push ⚠️
git push --force-with-lease #Reject if remote HEAD is not as expected ✅
Since the local branch hash changes after rebase, force is required when pushing.
If a push is made between team members, --force-with-lease rejects it and prevents overwriting.
See as example
Example 1 — ex01_rebase.sh: Rebase feature branch onto main
#!/usr/bin/env bash
REPO=$(mktemp -d); cd "$REPO"
git init -q && git config user.name "For practice" && git config user.email "[email protected]"
echo "base" > base.txt && git add . && git commit -q -m "A: base"
git switch -c feature
echo "feat" > feat.txt && git add . && git commit -q -m "C: feature"
git switch main
echo "main extra" > extra.txt && git add . && git commit -q -m "B: main extra"
echo "=== Graph before rebase ==="
git log --oneline --graph --all
git switch feature
git rebase main
echo ""
echo "=== Graph after rebase (linear) ==="
git log --oneline --graph --all
Execution result
=== Before rebase ===
* B: main extra (main)
| * C: feature (feature)
|/
* A: base
=== After rebase (linear) ===
* C': feature (feature)
* B: main extra (main)
* A: base
Bottom line: rebase creates a new commit (changes the hash), creating a linear history.
Example 2 — ex02_squash.sh: Merge commits with interactive rebase
#!/usr/bin/env bash
REPO=$(mktemp -d); cd "$REPO"
git init -q && git config user.name "For practice" && git config user.email "[email protected]"
echo "base" > f.txt && git add . && git commit -q -m "init"
echo "v1" >> f.txt && git add . && git commit -q -m "feat: step 1"
echo "v2" >> f.txt && git add . && git commit -q -m "fix: typo"
echo "v3" >> f.txt && git add . && git commit -q -m "fix: another typo"
echo "=== Log before squash ==="
git log --oneline
#Simulate squash non-interactively
git reset --soft HEAD~3
git commit -m "feat: implement feature (squashed 3 commits)"
echo ""
echo "=== Log after squash ==="
git log --oneline
rm -rf "$REPO"
Execution result
=== Log before squash ===
d4 fix: another typo
c3 fix: typo
b2 feat: step 1
a1 init
=== Log after squash ===
e5 feat: implement feature (squashed 3 commits)
a1 init
Bottom line: git rebase -i's squash combines multiple WIP commits into one meaningful commit.
Example 3 — ex03_cherry_pick.sh: Fetch only specific commits from another branch
#!/usr/bin/env bash
REPO=$(mktemp -d); cd "$REPO"
git init -q && git config user.name "For practice" && git config user.email "[email protected]"
echo "main" > main.txt && git add . && git commit -q -m "init"
git switch -c develop
echo "Function A" > a.txt && git add . && git commit -q -m "feat: feature A"
echo "Function B" > b.txt && git add . && git commit -q -m "feat: feature B"
echo "Function C" > c.txt && git add . && git commit -q -m "feat: feature C"
#Cherry-pick only commit B from main
git switch main
HASH_B=$(git log develop --oneline | grep "feature B" | awk '{print $1}')
git cherry-pick "$HASH_B"
echo "=== main log after cherry-pick ==="
git log --oneline
echo ""
echo "=== List of files in main (including only B) ==="
ls
rm -rf "$REPO"
Execution result
=== main log ===
b2' feat: feature B (commit imported by cherry-pick)
a1 init
=== main file list ===
b.txt main.txt
Key point: cherry-pick selects and applies only the desired commit, not the entire branch.
Example 4 — ex04_force_push.sh: Safely push with force-with-lease after rebase
#!/usr/bin/env bash
WORKDIR=$(mktemp -d)
git init --bare "$WORKDIR/remote.git" -q
git clone "$WORKDIR/remote.git" "$WORKDIR/repo" -q
cd "$WORKDIR/repo"
git config user.name "For practice" && git config user.email "[email protected]"
echo "init" > f.txt && git add . && git commit -q -m "init"
git push origin main -q
git switch -c feature
echo "WIP 1" >> f.txt && git add . && git commit -q -m "wip: step 1"
echo "WIP 2" >> f.txt && git add . && git commit -q -m "wip: step 2"
git push origin feature -q
echo "=== Log before push ==="
git log --oneline
#squash (reset --soft method)
git reset --soft HEAD~2
git commit -m "feat: complete feature"
echo ""
echo "=== Log after squash ==="
git log --oneline
echo ""
echo "=== Push safely with force-with-lease ==="
git push --force-with-lease origin feature
rm -rf "$WORKDIR"
Execution result
=== Log before push ===
wip: step 2
wip: step 1
init
=== Log after squash ===
feat: complete feature
init
=== push with force-with-lease ===
(success)
Bottom line: --force-with-lease only allows pushes when the remote branch is in the expected state.
Seeing from a different perspective
| concept | Metaphor |
|---|---|
| rebase | Moving — moving the same load to a new location |
| squash | Combining multiple drafts into one final version |
| cherry-pick | Choose only the dishes you want from the buffet |
| --force-with-lease | Overwrite only when "last seen by me" |
Common Mistakes
- rebase from shared branch (main) — team member's history gets messed up; Always rebase only on local feature branches.
--forceafter rebase (without lease) — Even if a team member pushes it, it is forced to be overwritten.- After cherry-picking, merge the original branch — The same change is made twice, resulting in conflict or duplicate history.
- Edit exit error during interactive rebase — Cancel with
git rebase --abort. - Remote push before squash — The hash changes, requiring a force push.
Cleanup
- Create a linear history by moving the base of the feature to the latest main with
git rebase <branch>. - You can squash·drop·reword N commits with
git rebase -i HEAD~N. - With
git cherry-pick <hash>, only specific commits from other branches are applied to the current branch. - After rebase, push must use
--force-with-lease.
Try it yourself
cd 07_rebase-and_cherry-pick/src
chmod +x *.sh
./ex01_rebase.sh
./ex02_squash.sh
./ex03_cherry_pick.sh
./ex04_force_push.sh
Application:
- Try inserting a new commit in the middle of a commit using the
git rebase -i HEAD~4toeditcommand. - If a conflict occurs during cherry-picking, resolve it and then run
git cherry-pick --continue.