Working Code
Example 1: Creating a behind-the-times branch
git init
echo "v1" > file.txt
git add file.txt
git commit -m "feat: add file"
# Create a feature branch
git checkout -b feature-work
echo "feature work" > feature.txt
git add feature.txt
git commit -m "feat: add feature work"
Add a new commit to main:
git checkout main
echo "main extra work" >> file.txt
git add file.txt
git commit -m "feat: add extra work on main"
Check history:
git log --oneline --graph --all
Output:
* a1b2c3d (HEAD -> main) feat: add extra work on main
| * e4f5g6h (feature-work) feat: add feature work
|/
* f7g8h9i feat: add file
Example 2: Rebasing onto the latest main
git checkout feature-work
git rebase main
Output:
Successfully rebased and updated refs/heads/feature-work.
Check history:
git log --oneline --graph --all
Output:
* j0k1l2m (HEAD -> feature-work) feat: add feature work
* a1b2c3d (main) feat: add extra work on main
* f7g8h9i feat: add file
The history is now linear. The feature commits sit on top of main.
Try It Yourself
merge vs rebase: comparing results
Given the same starting point, here's how merge and rebase differ:
Starting state:
A - B - C (main)
\
D - E (feature)
After merge:
A - B - C - M (main, merge commit)
\ /
D - E (feature)
After rebase:
A - B - C (main)
\
D' - E' (feature, rebased)
Rebase replays the feature commits on top of main. The content is the same but the hashes change (D -> D').
Interactive rebase: tidying up commits
# Create several work-in-progress commits
echo "1" >> work.txt && git add work.txt && git commit -m "wip: work in progress 1"
echo "2" >> work.txt && git add work.txt && git commit -m "wip: work in progress 2"
echo "3" >> work.txt && git add work.txt && git commit -m "feat: feature complete"
Interactively rebase the last 3 commits:
git rebase -i HEAD~3
The editor opens:
pick a1b2c3d wip: work in progress 1
pick e4f5g6h wip: work in progress 2
pick i7j8k9l feat: feature complete
Squash the wip commits together:
pick a1b2c3d wip: work in progress 1
squash e4f5g6h wip: work in progress 2
pick i7j8k9l feat: feature complete
After saving, a message editor opens:
# Combined commit messages
wip: work in progress 1
wip: work in progress 2
Replace with a clean message:
wip: initial implementation
After saving, the 3 commits become 2.
Handling conflicts during rebase
git rebase main # Conflict occurs
Output:
CONFLICT (content): Merge conflict in file.txt
error: could not apply a1b2c3d...
hint: Resolve all conflicts manually...
# Resolve the conflict
nano file.txt # Edit
git add file.txt
git rebase --continue # Move on to the next commit
# Or abort the rebase entirely
git rebase --abort
"Why?" -- rebase vs merge
Both combine branches, but the resulting history differs.
| | merge | rebase | | --------- | -------------------------- | ----------------------------- | | History | Preserves branch structure | Linear and clean | | Safety | Always safe | Risky on shared branches | | Conflicts | Once | Per commit | | Use case | PR merges, collaboration | Cleaning up personal branches |
Golden rule: Never rebase shared branches that have already been pushed (main, develop).
Why? Rebase changes commit hashes. If teammates have already fetched those commits, their history will conflict with yours.
# Safe rebase patterns
# 1. Clean up unpushed local branches
git rebase -i HEAD~5
# 2. Update local feature to latest main (before pushing)
git rebase main
# 3. Merge PRs through GitHub (merge commit or squash merge)
Common Mistakes
Mistake 1: Rebasing a pushed branch
git push origin feature-login # Already pushed
git rebase main # Changes commit hashes
git push # Error!
# error: failed to push some refs...
# Force push (causes problems for teammates!)
git push --force # Never do this on shared branches!
Mistake 2: Rebasing from the wrong branch
# Wrong: rebasing feature onto main FROM main
git checkout main
git rebase feature-login # main's history gets rewritten!
# Correct: rebase from the feature branch
git checkout feature-login
git rebase main
Mistake 3: Panicking during interactive rebase
git rebase -i HEAD~3
# Accidentally saved something wrong in the editor
# You can always abort
git rebase --abort # Fully restores the original state
Deep Dive
Full interactive rebase command list
Commands available for each commit in the editor:
| Command | Short | Description |
| -------- | ----- | --------------------------------------------- |
| pick | p | Keep the commit as-is |
| reword | r | Edit the commit message only |
| edit | e | Edit the commit content |
| squash | s | Merge into previous commit (combine messages) |
| fixup | f | Merge into previous commit (discard message) |
| drop | d | Delete the commit |
| exec | e | Run a shell command between commits |
git pull --rebase: cleaner history
# Default git pull = fetch + merge (creates merge commits)
git pull
# git pull --rebase = fetch + rebase (linear history)
git pull --rebase
# Set as default
git config --global pull.rebase true
If your team prefers clean history, set pull.rebase true.
Squash merge: tidying up when merging PRs
Three ways to merge a PR:
# 1. Merge commit: preserve all commits + merge commit
git merge --no-ff feature-branch
# 2. Squash merge: combine all commits into one on main
git merge --squash feature-branch
git commit -m "feat: integrate feature"
# 3. Rebase merge: linear history on main
git rebase main && git checkout main && git merge feature-branch
GitHub lets you restrict allowed merge methods in PR settings.
- Create a feature branch and add 3 commits.
- Add a new commit to main as well.
- From the feature branch, run
git rebase mainand check history withgit log --oneline --graph --all. - Add 2
wip:commits and 1feat:commit on feature, then usegit rebase -i HEAD~3to squash the wip commits.
Q1. Why should you not rebase a shared branch (one that's already been pushed)?
- A) Rebase is slower than merge
- B) Rebase changes commit hashes, causing history conflicts with others
- C) Rebase deletes files
- D) GitHub doesn't support rebase