INFRASTRUCTURE2026-04-13📖 5 min read

Git Rebase: A Practical Operational Guide

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 main

This 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 main

With 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 true

Since 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~3

Running this opens an editor where you specify operations per commit. Main commands:

CommandShortAction
pickpKeep the commit as-is
rewordrChange the commit message
editeModify the commit content
squashsMerge into the previous commit (with message edit)
fixupfMerge into the previous commit (discard message)
dropdDelete 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 feedback

This 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 validation

Save 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 tests

Placing 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 tests

On 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 --continue

This 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 main

When 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 true

When to Use Rebase vs. Merge

There isn't a blanket answer. After iterating across many projects, the criteria I've landed on:

Aspectrebasemerge
Commit historyLinear and readableBranches and merges are recorded
Merge commitsNot createdCreated
Conflict resolutionResolve per commitResolve all at once
Use on shared branchesAvoid as a ruleSafe to use
Historical accuracyMay diverge from actual development processDevelopment 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/main

In 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 handling

Since squash opens a message-editing screen, I write the consolidated message:

Implement user profile feature - Create profile page UI - Implement API connection and error handling

Establishing 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-ff to 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-profile

The 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-profile

If 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 --abort

Simply 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-5

For 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 true

Useful 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 feature

This 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 main

git 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 main

Without 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 --abort

If 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/main

If 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 with reflog
  • Troubleshooting: --abort for safe abort, --exec for test runs, reflog for 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.