Git Rebase #
In large organizations, development teams frequently work on the same codebase, which leads to common conflicts in pull requests (PRs). Understanding Git’s rebase
command in depth—and knowing how to wield advanced options like squash
, drop
, and rebasing onto specific commits—can help developers keep PRs manageable and the commit history clean. This article explores advanced rebase
techniques, with examples of how to rebase, squash, and drop commits to streamline PR management and resolve conflicts effectively.
Why #
The git rebase
command rewrites commit history by moving or combining commits in a linear, sequential fashion, rather than creating a branching structure. For teams handling multiple PRs and branches, using rebase
can help:
- Keep history clean:
Rebase
avoids the merging branches, making the commit history appear as a single, linear sequence. - Resolve conflicts early: Frequent rebasing helps identify and resolve conflicts before they become larger issues at merge time.
- Improve PR review quality: A clean history with squashed or logically organized commits allows reviewers to understand the changes without the clutter of unnecessary commits.
Use rebase
Safely
#
Because rebase
rewrites commit history, it’s best to approach it with caution. Here are a few steps to prepare for complex rebase operations:
-
Create a backup branch: Always create a backup of your branch before starting a complex rebase.
git checkout feature-branch git branch backup-feature-branch
-
Stash uncommitted changes: Ensure your working directory is clean to avoid losing uncommitted work.
git stash
Example Scenario #
Let’s look at a common example where a feature branch needs to be rebased due to changes in main
. The example includes rebasing onto the latest main branch commit, then rebasing back to an earlier commit for testing.
Scenario Outline #
- The development branch was originally based on an older commit
C
inmain
. - Several commits were added to
dev
, now at commitF
. - Another team merged a PR into
main
, advancing it to commitG
. - After rebasing
dev
onto commitG
, issues were found during testing, and now we need to rebase back to commitC
to isolate the issue.
This example will show how to rebase both forward and backward to a specific commit.
1. Developing #
Initial Setup and Dev Branch Creation
We start with the main
branch on commit C
and create a dev branch based on it.
Create a dev branch starting from commit C
:
git checkout main
git checkout -b dev
Let’s add several commits on the dev
, reaching commit F
.
Now the commit history looks like this:
main: A --- B --- C
\
dev: C --- F
2. Main branch updated #
main
Advances to a New Commit Due to Another Team’s PR
While you were working on dev
, another team completed their work and merged a PR into main
, bringing it to a new commit G
.
main: A --- B --- C --- D --- E --- G
\
dev: C --- F
Your dev
is now at commit F
based on commit A
, but main
has been updated to commit G
.
3. Rebase to main #
Rebase dev
onto the Latest main
(Commit G)
To keep your branch up to date with the latest changes in main
, you rebase your dev
onto G
.
- Pull
main
branch
git checkout main
git pull
- Start the rebase process to rebase onto
main
’s latest commit (G
):
git checkout dev
git rebase main
- If there are conflicts, Git will prompt you to resolve them. After fixing any conflicts, complete the rebase with:
git rebase --continue
After rebasing, your commit history now looks like this:
main: A --- B --- C --- D --- E --- G
\
dev: G --- F'
Your development branch (dev
) is now rebased onto the latest main
, but testing reveals an issue potentially related to the changes introduced by the PR in G
.
4. Rebase to older commits #
Rebase the Dev Branch Back to Commit A to Isolate the Issue
To determine whether the issue is related to the changes in G
, you decide to rebase dev
back onto A
, isolating it from any recent changes in main
.
To rebase dev
back onto commit C
:
Use rebase --onto
to move the base of feature-branch
back to C
, omitting changes in G
.
git rebase --onto <commit-C-hash> <commit-G-hash>
Here’s how this command works:
--onto <commit-C-hash>
: This tells Git to move the base of the branch back toC
.<commit-G-hash>
: Specifies where to start moving commits from, effectively ignoring any changes introduced inG
.
Git will now replay your dev
commits on top of C
, as if G
and subsequent changes in main
didn’t happen.
Now, your branch history looks like this:
main: A --- B --- C --- D --- E --- G
\
dev: C --- F''
You can now test the branch to confirm whether the issue was introduced by G
or originated elsewhere.
Advanced Rebase Workflow #
Combining Rebase, Squash, and Drop for a Clean History
In large PRs, you may want to combine these techniques to keep history clean and manageable. Here’s a recommended workflow:
- Rebase onto the latest
main
to stay current with the latest code. - Squash commits to combine related changes into single, meaningful commits.
- Drop irrelevant commits to remove unnecessary work-in-progress saves.
- Rebase onto specific commits as needed to isolate or manage conflicts.
For a comprehensive cleanup, use an interactive rebase to combine all steps:
git rebase -i HEAD~<number-of-commits>
In the interactive rebase editor, you can squash, drop, and reorder commits to organize history logically.
Summary #
-
Rebase feature branch onto the latest main:
git rebase main
-
Rebase feature branch back to an older commit if issues arise:
git rebase --onto <old-commit-hash> main
Using these rebase techniques helps you manage PRs in large organizations, allowing you to isolate issues, reduce conflicts, and keep your commit history clean. With a deep understanding of rebase
, squashing, and dropping commits, developers can avoid merge conflicts, keep history readable, and streamline the development process for smoother collaboration.