Git & GitHub track

Lesson 07 · Lessons

rebase & cherry-pick

Interactive rebase, squashing, cherry-picking and force-with-lease

Lessons6 min readFree lesson

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

  1. rebase from shared branch (main) — team member's history gets messed up; Always rebase only on local feature branches.
  2. --force after rebase (without lease) — Even if a team member pushes it, it is forced to be overwritten.
  3. After cherry-picking, merge the original branch — The same change is made twice, resulting in conflict or duplicate history.
  4. Edit exit error during interactive rebase — Cancel with git rebase --abort.
  5. 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:

  1. Try inserting a new commit in the middle of a commit using the git rebase -i HEAD~4 to edit command.
  2. If a conflict occurs during cherry-picking, resolve it and then run git cherry-pick --continue.

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/ex01_rebase.shShell
#!/usr/bin/env bash
set -euo pipefail

REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "For practice" && git config user.email "[email protected]"
    git config commit.gpgsign false

echo "base" > base.txt
git add . && git commit -q -m "A: initial commit"

#Feature branch operations
git switch -c feature
echo "function 1" > feat1.txt
git add . && git commit -q -m "C: add feature 1"
echo "function 2" > feat2.txt
git add . && git commit -q -m "D: add feature 2"

#Separate commits to the main branch
git switch main
echo "Add main" > extra.txt
git add . && git commit -q -m "B: main extra work"

echo "=== Graph before rebase ==="
git log --oneline --graph --all

#Rebase feature onto main
git switch feature
git rebase main

echo ""
echo "=== Graph after rebase (linear history) ==="
git log --oneline --graph --all

echo ""
echo "=== Feature branch file list ==="
ls

rm -rf "$REPO"
src/ex02_squash.shShell
#!/usr/bin/env bash
set -euo pipefail

REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "For practice" && git config user.email "[email protected]"
    git config commit.gpgsign false

echo "bass" > app.py
git add . && git commit -q -m "init: base"

#Multiple work commits (WIP)
echo "Step 1" >> app.py && git add . && git commit -q -m "wip: step 1"
echo "Step 2" >> app.py && git add . && git commit -q -m "wip: step 2"
echo "typo correction" >> app.py && git add . && git commit -q -m "fix: typo"
echo "Step 3" >> app.py && git add . && git commit -q -m "wip: step 3"

echo "=== Pre-squash log (4 WIP commits) ==="
git log --oneline

#git rebase -i is interactive, so simulate squash with reset --soft
git reset --soft HEAD~4
git commit -m "feat: implement full feature (squashed 4 commits)

- Implement steps 1, 2, 3
- Including typo corrections"

echo ""
echo "=== Log after squash (combined into 1) ==="
git log --oneline

echo ""
echo "=== Final commit message ==="
git log -1 --format="%B"

rm -rf "$REPO"
src/ex03_cherry_pick.shShell
#!/usr/bin/env bash
set -euo pipefail

REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "For practice" && git config user.email "[email protected]"
    git config commit.gpgsign false

echo "main code" > main.txt
git add . && git commit -q -m "init"

#Multiple commits in the develop branch
git switch -c develop
echo "Function 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 "Function B" > feature_b.txt && git add . && git commit -q -m "feat: feature B"
echo "Function C" > feature_c.txt && git add . && git commit -q -m "feat: feature C"

echo "=== develop branch log ==="
git log --oneline

#Cherry-pick only hotfix commits from main
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 (only contains hotfix) ==="
git log --oneline

echo ""
echo "=== main file list (only hotfix.txt added) ==="
ls

rm -rf "$REPO"
src/ex04_force_push.shShell
#!/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 "For practice" && git config user.email "[email protected]"
    git config commit.gpgsign false

echo "initial file" > main.txt
git add . && git commit -q -m "init"
git push origin main -q

#After committing the WIP in the feature branch, push it
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 "=== Log before push (3 WIP commits) ==="
git log --oneline

#squash: 3 WIP → 1
git reset --soft HEAD~3
git commit -m "feat: complete feature"

echo ""
echo "=== Log after squash (1) ==="
git log --oneline

echo ""
echo "=== Push safely with --force-with-lease ==="
git push --force-with-lease origin feature

echo ""
echo "=== Check remote feature branch ==="
git log origin/feature --oneline

rm -rf "$WORKDIR"

Assignment

Problem 1

Write a script to create a linear history by rebasing the feature branch onto main.

  • File name: hw01.sh
  • Requirements:
  1. Initialize temporary storage and commit base.txt.
  2. Commit work.txt in the feature/work branch.
  3. Return to main and commit hotfix.txt (branch branch status).
  4. Run git rebase main from feature/work.
  5. Confirm that the history is linear with git log --oneline --graph --all.
  6. Fast-forward merge with git switch main && git merge feature/work.
  7. Print the final log (it should be straight).

Problem 2

Write a script that cherry-picks only specific commits from the develop branch into main.

  • File name: hw02.sh
  • Requirements:
  1. Initialize temporary storage.
  2. Make commits in the develop branch in the following order:
    • "feat: add login"
    • "fix: security patch"
    • "feat: add dashboard"
  3. Cherry-pick only the "fix: security patch" commit from main.
  4. Print main history with git log --oneline (only security patch must be present).
  5. Print the file list with ls.

Check the correct answer

After writing it yourself, compare it with the reference solution below.

Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

hw01.shShell
#!/usr/bin/env bash
set -euo pipefail

REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "For practice" && git config user.email "[email protected]"
    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 "=== Graph before rebase ==="
git log --oneline --graph --all

git switch feature/work
git rebase main

echo ""
echo "=== Graph after rebase (linear) ==="
git log --oneline --graph --all

git switch main
git merge feature/work

echo ""
echo "=== Final log after fast-forward merge ==="
git log --oneline --graph --all

rm -rf "$REPO"
hw02.shShell
#!/usr/bin/env bash
set -euo pipefail

REPO=$(mktemp -d)
cd "$REPO"
git init -q -b main
git config user.name "For practice" && git config user.email "[email protected]"
    git config commit.gpgsign false

echo "main code" > main.txt && git add . && git commit -q -m "init"

git switch -c develop
echo "Login function" > login.py    && git add . && git commit -q -m "feat: add login"
echo "security patch" > security.py  && git add . && git commit -q -m "fix: security patch"
echo "dashboard" > 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 only) ==="
git log --oneline

echo ""
echo "=== main file list ==="
ls

rm -rf "$REPO"

Learn this lesson with a mentor

Work through the Git & GitHub track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now