Git Rebase: A Practical Operational Guide

A systematic walkthrough of git rebase — from the basics to interactive mode, and when to choose rebase over merge — with the practical know-how you need in real team development. Clean up your commit history and raise team development quality.
代表 / エンジニア
Working in a team on Git, have you ever felt "the commit history is a bit tangled — I'd like to tidy it up before opening this pull request"? I've created review-unfriendly histories plenty of times, and that's where I came to appreciate git rebase. This article lays out the practical, real-world know-how for using git rebase.
What Is git rebase?
git rebase is an operation that "re-attaches" the commits of one branch onto the tip of another. Where merge creates a merge commit at the convergence point, rebase restructures the commit history linearly.
At first, the abstract description of "rewriting history" didn't clearly tell me when to use it. Actually using it in team development is what made its value tangible.
The basic syntax:
# Rebase the feature branch onto the tip of main
git checkout feature
git rebase mainThis relocates the feature branch's commits to sit after main's latest commit. The result is a clean history that looks as if development had started from main's current state.
Comparing History Before and After Rebase
Words alone don't convey this well. Let's look at a concrete history change.
Before rebase:
C---D---E (feature)
/
A---B---F---G (main)The feature branch diverged from main at commit B. Main has since gained commits F and G.
After rebase:
C'---D'---E' (feature)
/
A---B---F---G (main)Feature's commits C, D, E are re-attached after main's G and recreated as C', D', E'. The prime symbols indicate the contents are the same but the commit hashes are different. That "hash changes" property directly connects to the cautions discussed later.
Using Rebase with git pull
An everyday scenario: rebase during git pull. A normal git pull is fetch plus merge; adding --rebase replaces the merge with a rebase.
# Normal pull (merge)
git pull origin main
# Pull with rebase
git pull --rebase origin mainWith a normal pull, when both local and remote have new commits, a needless merge commit is generated. With --rebase, you bring in remote changes while keeping a linear history.
If specifying --rebase every time is tedious, you can make it the default:
# Use rebase by default on pull
git config --global pull.rebase trueSince adopting this setting, the needless merge commits stopped, and personally my stress level dropped noticeably.
Using git rebase -i (Interactive Mode)
The one that really pays off in practice is git rebase -i — interactive mode. It enables flexible history editing: reordering commits, combining them, revising messages.
# Interactively edit the last 3 commits
git rebase -i HEAD~3Running this opens an editor where you specify operations per commit. Main commands:
| Command | Short | Action |
|---|---|---|
| pick | p | Keep the commit as-is |
| reword | r | Change the commit message |
| edit | e | Modify the commit content |
| squash | s | Merge into the previous commit (with message edit) |
| fixup | f | Merge into the previous commit (discard message) |
| drop | d | Delete the commit |
What I use most are squash and fixup. The pattern "commit freely during development, then consolidate into meaningful units via rebase -i before opening the PR" is widely adopted. You may be wrestling with the same fine-grained-commits problem.
Example: Combining Commits
# Edit in the editor as follows
pick a1b2c3d Implement user authentication
fixup d4e5f6g typo fix
fixup h7i8j9k Apply review feedbackThis combines three commits into one, keeping only the first commit's message.
Example: Fixing a Commit Message
Committing in a rush often results in sloppy messages. With reword, you can change just the message while leaving the content intact.
# Edit in the editor as follows
reword a1b2c3d fix
pick d4e5f6g Add validationSave and close the editor, and a second editor opens for the commit whose message you reworded. Rewrite it to something meaningful, like "Fix display of user input validation errors."
Example: Reordering Commits
Rearranging lines in the editor changes the order commits are applied.
# Original order
pick a1b2c3d Add API endpoint
pick d4e5f6g Add tests
pick h7i8j9k Add API validation
# After reordering (validation right after API addition)
pick a1b2c3d Add API endpoint
pick h7i8j9k Add API validation
pick d4e5f6g Add testsPlacing related changes adjacent makes the story easier to follow during review. Caution: if there are dependencies between commits, you may trigger conflicts.
Example: Modifying Commit Content with edit
Using edit lets you modify the contents of a past commit. Useful when "I should have included this file change in that commit."
# Edit in the editor as follows
edit a1b2c3d Implement user authentication
pick d4e5f6g Add testsOn save, rebase stops at the specified commit:
# Modify the file
vim src/auth.ts
# Stage the change and amend the commit
git add src/auth.ts
git commit --amend --no-edit
# Continue rebase
git rebase --continueThis operation is powerful but can conflict with subsequent commits — keep that in mind.
Efficient Commit Cleanup with --autosquash
During development you'll often create "fix-up commits intended to be combined later." Using the --fixup option when committing lets rebase auto-reorder and fixup later.
# Normal commit
git commit -m "Implement login feature"
# Create a fixup commit targeting the above
git commit --fixup=<hash of the login-feature commit>
# Automatically apply fixups with autosquash
git rebase -i --autosquash mainWhen the editor opens, fixup commits are automatically positioned right after the target commit, with the command already set to fixup. It eliminates manual reordering — a recommended feature for anyone using fixup commits daily.
You can also enable --autosquash by default:
git config --global rebase.autosquash trueWhen to Use Rebase vs. Merge
There isn't a blanket answer. After iterating across many projects, the criteria I've landed on:
| Aspect | rebase | merge |
|---|---|---|
| Commit history | Linear and readable | Branches and merges are recorded |
| Merge commits | Not created | Created |
| Conflict resolution | Resolve per commit | Resolve all at once |
| Use on shared branches | Avoid as a rule | Safe to use |
| Historical accuracy | May diverge from actual development process | Development process preserved |
Recommended usage:
- When rebase is appropriate: Bringing a local feature branch up to main's latest, tidying commits before opening a pull request
- When merge is appropriate: Integrating into shared branches, release branch operations, when you want the merge history preserved explicitly
Looking back, I started out doing everything with merge. Layering in rebase appropriately significantly improved review efficiency.
A Concrete Operational Flow
Here's the flow I actually use on real projects — from feature branch development through pull request creation.
# 1. Create feature branch from latest main
git checkout main
git pull --rebase origin main
git checkout -b feature/user-profile
# 2. Commit freely during development (with the plan to consolidate later)
git commit -m "Create profile page UI"
git commit -m "WIP: API connection"
git commit -m "API connection complete"
git commit -m "typo fix"
git commit -m "Apply review feedback: add error handling"
# 3. Incorporate latest main
git fetch origin
git rebase origin/main
# 4. Clean up the commits
git rebase -i origin/mainIn step 4's interactive rebase, I consolidate the five commits into two meaningful ones:
pick a1b2c3d Create profile page UI
squash d4e5f6g WIP: API connection
squash h7i8j9k API connection complete
squash l0m1n2o typo fix
squash p3q4r5s Apply review feedback: add error handlingSince squash opens a message-editing screen, I write the consolidated message:
Implement user profile feature
- Create profile page UI
- Implement API connection and error handlingEstablishing Team Rules
The rebase-vs-merge split is something the whole team needs to agree on. Documenting policies like these up front prevents confusion between members.
- feature branch → main: Rebase to catch up to latest, then merge via pull request (use
--no-ffto keep the merge commit) - Direct push to main: Prohibited
- Rebase of already-pushed branches: Prohibited as a rule. If unavoidable, notify the team in advance
- Commit cleanup: Recommend interactive rebase before opening pull requests
These policies are ideally decided at project kickoff. Trying to introduce them later is harder — each member's habits are already entrenched, and unifying them is difficult.
Things to Watch Out for with Rebase
Git rebase is a powerful tool, but misuse can cause real confusion in a team. Use this checklist:
Pre-rebase safety checklist:
- The target is a local-only branch
- You are not attempting to rebase commits already pushed to remote
- Work-in-progress changes are stashed or committed
- You understand how to handle conflicts when they occur
Especially important is the principle: do not rebase commits that have been pushed. Because rebase changes commit hashes, rebasing commits that others have already fetched creates history inconsistencies.
Recovering After Rebasing an Already-Pushed Branch
You should avoid this in principle, but knowing the recovery path provides peace of mind.
# A force push is required (normal push will be rejected)
git push --force-with-lease origin feature/user-profileThe key here is using --force-with-lease rather than --force. --force-with-lease refuses the push when the remote branch has moved beyond your awareness (e.g., another teammate pushed new commits). --force overwrites unconditionally and risks wiping out others' changes.
# Dangerous: may overwrite others' changes
git push --force origin feature/user-profile
# Safe: refuses the push if unexpected changes exist on remote
git push --force-with-lease origin feature/user-profileIf you want to return to the pre-rebase state, use git reflog:
# Find the pre-rebase state in reflog
git reflog
# Example output
# a1b2c3d HEAD@{0}: rebase (finish): returning to refs/heads/feature
# ...
# f8e9d0c HEAD@{5}: commit: Apply review feedback
# Reset to the pre-rebase state
git reset --hard HEAD@{5}git reflog displays the operation history Git records. Commits you thought were "gone" after rebase or reset can often be recovered via reflog. I've been saved by it more than once.
Resolving Conflicts During Rebase
When conflicts occur, follow this procedure:
# After resolving the conflict
git add <resolved file>
git rebase --continue
# To abort the rebase
git rebase --abortSimply knowing you can --abort back to the original state lowers the psychological hurdle to using rebase.
Conflict resolution during rebase differs slightly from during merge. Because rebase re-applies commits one at a time, if multiple commits have conflicts, you resolve each one as it comes up.
# Conflict during rebase
Auto-merging src/components/Header.tsx
CONFLICT (content): Merge conflict in src/components/Header.tsx
# 1. Check conflicted files
git status
# 2. Open the file and resolve the conflict
vim src/components/Header.tsx
# 3. Review the conflict markers and fix to the appropriate content
# <<<<<<< HEAD
# main branch content
# =======
# feature branch content
# >>>>>>> commit message
# 4. Stage the resolved file
git add src/components/Header.tsx
# 5. Continue the rebase
git rebase --continue
# 6. If the next commit also conflicts, repeat steps 1-5For branches with many commits, you may end up repeatedly resolving conflicts. Enabling git rerere (reuse recorded resolution) makes Git remember once-resolved conflict patterns, automating resolution when the same conflict recurs.
# Enable rerere
git config --global rerere.enabled trueUseful Options Related to Rebase
Beyond the basics, here are some options that are handy in practice.
--onto: Flexibly Specify the Destination
A normal rebase attaches the current branch to the tip of the specified branch; --onto enables more flexible control.
# Rebase only the feature commits that diverged from develop onto main
git rebase --onto main develop featureThis is useful when feature diverged from develop and you want to port only those changes onto main.
# before
E---F---G (feature)
/
C---D (develop)
/
A---B (main)
# after: git rebase --onto main develop feature
E'---F'---G' (feature)
/
A---B (main)--keep-base: Tidy Commits Without Changing the Divergence Point
--keep-base is handy when you want to do an interactive rebase without changing the branch's divergence point.
# Clean up commits without changing the divergence point from main
git rebase -i --keep-base maingit rebase -i HEAD~n achieves something similar, but when you want to target "all commits since diverging from main," you don't need to count commits.
--update-refs: Update Related Branches Too
A relatively new option available in Git 2.38+. When rebasing stacked branches (a chain of branches), it automatically updates the references of related branches.
# Rebase a chain of branches together
git rebase --update-refs mainWithout this option, after rebasing a parent branch, you had to manually re-attach child branches. --update-refs removes that chore.
Troubleshooting
Using rebase, you sometimes hit unexpected situations. Common cases and their handling:
Wanting to Back Out Mid-Rebase
# Abort the rebase completely and return to the pre-rebase state
git rebase --abortIf conflict resolution has become overly complex, or you want to reconsider the rebase approach entirely, --abort without hesitation.
Tests Fail After Rebase
Because rebase re-applies each commit in sequence, intermediate commits may leave the build broken. The --exec option runs a specified command after each commit's application.
# Run tests after each commit's application
git rebase -i main --exec "npm test"Rebase stops at the commit whose tests fail, making it easy to pinpoint where the problem appeared.
Accidentally Completed a Rebase
Even after rebase completes, git reflog lets you return to the previous state.
# Check operation history
git reflog
# Return to pre-rebase state (replace HEAD@{n} with the appropriate number)
git reset --hard HEAD@{n}git reflog is Git's safety net. Beyond rebase, keep it in mind as the last line of defense when you want a do-over.
Mass Conflicts Erupt
Rebasing a long-neglected branch onto main's latest often produces a flood of conflicts. To prevent this, cultivate the habit of incorporating main's changes regularly.
# Habit: incorporate main at the start of each day
git fetch origin
git rebase origin/mainIf conflicts become truly overwhelming, using merge instead of rebase is also a valid option. Through experience I learned that flexibly judging by the situation — rather than being driven by the tool — matters.
Summary
Git rebase is an important tool for improving commit history readability and raising team development quality. Interactive mode enables flexible commit cleanup, and appropriately separating rebase from merge makes project operation more refined.
Recapping what this article covered:
- Basic rebase: Attach the feature branch to main's latest for a linear history
- Interactive rebase: Use squash, fixup, reword, edit, etc., to organize commits into meaningful units
- --autosquash: Combined with fixup commits, streamlines commit cleanup
- Split vs. merge: Rebase for local cleanup, merge for shared-branch integration
- Safe operation: Don't rebase pushed commits; use
--force-with-lease; recover withreflog - Troubleshooting:
--abortfor safe abort,--execfor test runs,reflogfor do-overs
You don't need to master everything at once. Start with commit cleanup on local branches, and gradually integrate it into operations — that's probably the smoothest path.
At aduce, we support development process improvement including Git operations and the buildout of team development practices. If you need help reviewing your development flow or with technical challenges, please feel free to reach out via Contact.