Welcome back, intrepid developer! In our previous chapters, we mastered the basics of Git, learned how to create branches, and merged our work back into the main line of development. Merging is fantastic for combining divergent lines of work, but sometimes, the commit history can look a bit… messy, full of extra merge commits.
What if there was a way to integrate changes from one branch into another, but make it look like you developed your changes directly on top of the latest version of the target branch? What if you could even tidy up your own commits before sharing them with the world?
That’s precisely what Git’s rebase command allows us to do! In this chapter, we’ll dive deep into the world of rebasing. We’ll learn how it differs from merging, explore its incredible power for creating clean, linear project histories, and discover the magic of interactive rebasing to sculpt your commits into perfect, logical units. Get ready to elevate your Git game and contribute to projects with a pristine commit log!
What is Rebasing? A Fresh Perspective on History
Imagine your project’s commit history as a story. When you merge a branch, it’s like adding a new chapter that explicitly states, “Here’s where two different storylines converged.” This creates a merge commit, showing the point where the histories joined.
Rebase, on the other hand, is like rewriting a segment of your story to make it appear as if your new events happened after all the latest developments in the main plot. Instead of creating a merge commit, rebase takes your commits, places them on top of the target branch’s latest commit, and then reapplies them one by one. The result? A perfectly linear history, as if you always started your work from the most up-to-date version.
Let’s visualize this difference:
Merging vs. Rebasing: A Visual Story
Consider a main branch and a feature branch that diverged.
Scenario 1: git merge
- When you merge
featureintomain, Git creates a new “merge commit” (F). This commit explicitly records thatfeaturewas integrated intomain. This preserves the exact history of when and how branches diverged and merged.
Scenario 2: git rebase
- When you rebase
featureontomain, Git effectively moves thefeaturebranch’s commits (DandE) to come after the latest commit onmain(C). It rewrites the history of yourfeaturebranch commits, giving them new commit IDs. The original commits (D_original,E_original) no longer exist in the branch’s history; new ones (D_rebased,E_rebased) are created. The history becomes a straight line.
Why Choose Rebase?
- Cleaner History: A linear history is often easier to read and understand. It looks like a single, continuous line of development.
- Avoid Unnecessary Merge Commits: If you’re frequently pulling changes from
maininto your feature branch, rebasing can help avoid a “diamond” shape in your history with many small merge commits. - Easier Backporting (Cherry-Picking): A clean, linear history makes it simpler to identify and cherry-pick specific changes if needed.
- Squashing Commits: With interactive rebase, you can combine multiple small, in-progress commits into one logical commit before pushing, making your commit messages more meaningful.
When to Be Cautious with Rebase: The Golden Rule!
The absolute most critical rule when it comes to rebasing is:
DO NOT REBASE COMMITS THAT HAVE ALREADY BEEN PUSHED TO A SHARED (PUBLIC) REPOSITORY.
Why? Because rebasing rewrites history. If you rebase commits that others have already pulled and built their work upon, you’re changing the past that they’ve already seen. When they try to push or pull again, Git will see a completely different history and get very confused, leading to potential data loss or complex conflict resolution for everyone involved.
Think of it this way: rebase is your personal history-editing tool for your local, unshared work. Once your commits are public, they’re set in stone.
Step-by-Step Implementation: Rebasing for a Clean History
Let’s get our hands dirty and see rebase in action. We’ll simulate a common scenario: you’re working on a feature branch, and the main branch has new updates you want to incorporate smoothly.
First, let’s create a new Git repository and some initial commits.
# Initialize a new Git repository
git init my_rebase_project
cd my_rebase_project
# Configure user details (if not already set globally)
git config user.name "Your Name"
git config user.email "your.email@example.com"
# Create an initial commit on the 'main' branch
echo "Initial project setup" > README.md
git add README.md
git commit -m "feat: Initial project setup"
# Create a simple file
echo "function greet() { return 'Hello!'; }" > src/app.js
git add src/app.js
git commit -m "feat: Add basic greeting function"
# You are now on the 'main' branch with two commits.
# Let's verify the log
git log --oneline
You should see output similar to this (commit IDs will differ):
b1a2c3d feat: Add basic greeting function
a0b1c2d feat: Initial project setup
Scenario: Diverging Branches
Now, let’s create a feature branch and make some changes there, while also simulating new changes happening on main.
# Create a new feature branch
git checkout -b feature/login-form
# Make some changes on the feature branch
echo "<!-- Login Form -->" > src/login.html
echo "<input type='text' placeholder='Username'>" >> src/login.html
git add src/login.html
git commit -m "feat: Add basic login form HTML"
echo "<input type='password' placeholder='Password'>" >> src/login.html
git add src/login.html
git commit -m "feat: Add password field to login form"
# Check the log on feature/login-form
git log --oneline
Your feature/login-form branch now has two new commits on top of main.
Now, let’s switch back to main and simulate someone else making changes:
git checkout main
# Make a new commit on the main branch
echo "/** Global utility functions */" > src/utils.js
echo "function logEvent(event) { console.log(event); }" >> src/utils.js
git add src/utils.js
git commit -m "feat: Add global utility logging function"
# Check the log on main
git log --oneline
You now have a situation where feature/login-form is two commits ahead of main from where it branched, and main is one commit ahead of where feature/login-form branched off. They have diverged!
Performing a git rebase
Now, let’s bring the feature/login-form branch up-to-date with main using rebase.
# Switch back to your feature branch
git checkout feature/login-form
# Rebase feature/login-form onto main
git rebase main
Git will now attempt to “replay” your feature/login-form commits one by one on top of the latest main commit. You should see output similar to:
Successfully rebased and updated refs/heads/feature/login-form.
Let’s look at the log now:
git log --oneline
Notice how the commits from feature/login-form now appear after the main branch’s new commit, and there’s no merge commit! The history is perfectly linear. The commit IDs for your feature branch commits will also have changed, as they are technically new commits.
This is a clean, linear history. Excellent!
Resolving Conflicts During Rebase
What happens if changes on main conflict with changes on your feature branch? Git will pause the rebase process and let you resolve them.
Let’s simulate a conflict:
# First, reset our feature branch to before the rebase for a fresh start
git reset --hard HEAD~2 # This assumes the last two commits were from the feature branch
git checkout main
git branch -D feature/login-form # Delete the rebased branch
git checkout -b feature/login-form # Recreate the branch from main's earlier state
# (Re-add feature commits - let's simplify for the example, assume we're back at divergence point)
# For a real scenario, you'd be in a state before your first rebase attempt.
# Let's manually create a conflict for demonstration.
# Create a fresh repository for a clean conflict demo
cd ..
rm -rf my_rebase_project
git init my_rebase_project
cd my_rebase_project
git config user.name "Your Name"
git config user.email "your.email@example.com"
echo "Line 1" > conflict_file.txt
echo "Line 2" >> conflict_file.txt
git add conflict_file.txt
git commit -m "Initial commit with conflict_file"
git checkout -b feature/conflict-example
# Change on feature branch
sed -i '' 's/Line 2/Feature Line 2 Modified/' conflict_file.txt # Use 'sed -i' for macOS/BSD
# For Linux: sed -i 's/Line 2/Feature Line 2 Modified/' conflict_file.txt
git add conflict_file.txt
git commit -m "feat: Modify Line 2 on feature"
git checkout main
# Change on main branch (conflicting with feature)
sed -i '' 's/Line 2/Main Line 2 Modified/' conflict_file.txt # Use 'sed -i' for macOS/BSD
# For Linux: sed -i 's/Line 2/Main Line 2 Modified/' conflict_file.txt
git add conflict_file.txt
git commit -m "feat: Modify Line 2 on main"
git checkout feature/conflict-example
# Now, attempt to rebase and create a conflict
git rebase main
Git will pause and tell you there’s a conflict:
Applying: feat: Modify Line 2 on feature
Using index info to reconstruct a base tree...
M conflict_file.txt
.git/rebase-apply/patch:3: trailing whitespace.
Line 2
error: patch failed: conflict_file.txt:1
error: conflict_file.txt: patch does not apply
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can abort the rebase with "git rebase --abort".
This is familiar territory if you’ve resolved merge conflicts.
Open the conflicted file (
conflict_file.txt):<<<<<<< HEAD Feature Line 2 Modified ======= Main Line 2 Modified >>>>>>> feat: Modify Line 2 on mainResolve the conflict: Edit the file to the desired state. Let’s say we want to keep both, or a combined version:
Line 1 Combined Line 2 from Feature and MainStage the resolved file:
git add conflict_file.txtContinue the rebase:
git rebase --continueIf there are more commits to replay that also have conflicts, Git will pause again. Once all commits are replayed and conflicts resolved, the rebase will complete.
You can always use
git rebase --abortif you get stuck and want to go back to the state before you started the rebase.
Interactive Rebasing: Sculpting Your History (git rebase -i)
This is where rebase becomes truly powerful for cleaning up your local branch history before pushing. git rebase -i (interactive rebase) lets you modify individual commits, combine them, reorder them, or even delete them.
Let’s say you’ve been working on a feature, and you have a series of commits like this:
- “feat: Add login form structure”
- “fix: Typo in login form label”
- “refactor: Adjust CSS for login form”
- “feat: Add password field”
- “fix: Another small typo”
This is a lot of granular commits. For a cleaner history, you might want to combine the typo fixes and the CSS refactor into the main login form commits.
Let’s create a scenario:
# Ensure we are on a clean main
cd ..
rm -rf my_rebase_project
git init my_rebase_project
cd my_rebase_project
git config user.name "Your Name"
git config user.email "your.email@example.com"
echo "Initial project setup" > README.md
git add README.md
git commit -m "feat: Initial setup"
git checkout -b feature/interactive-demo
echo "function validateEmail(email) { /* ... */ }" > src/validation.js
git add src/validation.js
git commit -m "feat: Add email validation function"
echo "function validatePassword(password) { /* ... */ }" >> src/validation.js
git add src/validation.js
git commit -m "feat: Add password validation function"
# Oops, found a bug right after adding password validation
sed -i '' 's/validatePassword/isValidPassword/' src/validation.js
# For Linux: sed -i 's/validatePassword/isValidPassword/' src/validation.js
git add src/validation.js
git commit -m "fix: Rename isValidPassword function"
echo "function validateUsername(username) { /* ... */ }" >> src/validation.js
git add src/validation.js
git commit -m "feat: Add username validation"
# Another small fix
sed -i '' 's/isValidPassword/validatePasswordStrength/' src/validation.js
# For Linux: sed -i 's/isValidPassword/validatePasswordStrength/' src/validation.js
git add src/validation.js
git commit -m "fix: Improve password validation naming"
git log --oneline
You’ll see something like:
e1f2g3h fix: Improve password validation naming
c4d5e6f feat: Add username validation
a7b8c9d fix: Rename isValidPassword function
f0g1h2i feat: Add password validation function
j3k4l5m feat: Add email validation function
... (main commits)
Notice the two “fix” commits. We can squash them into their respective feature commits or into one logical “validation functions” commit.
Starting an Interactive Rebase
To interactively rebase the last N commits, you use git rebase -i HEAD~N. If you want to rebase all commits since your branch diverged from main, you can use git rebase -i main.
Let’s rebase the last 5 commits (the ones from feature/interactive-demo):
git rebase -i HEAD~5
Git will open your default text editor (like Vim, Nano, or VS Code). You’ll see a file listing your commits in reverse chronological order (oldest first), along with instructions:
pick j3k4l5m feat: Add email validation function
pick f0g1h2i feat: Add password validation function
pick a7b8c9d fix: Rename isValidPassword function
pick c4d5e6f feat: Add username validation
pick e1f2g3h fix: Improve password validation naming
# Rebase 8a5f3d4..e1f2g3h onto 8a5f3d4 (5 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) for each commit
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, tdo <label> = like 'label', but only if empty
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit and edit its message (or use the one from <commit> or <shortref>)
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here, that commit will be dropped from the series.
#
# However, if you remove everything, the rebase will be aborted.
#
Common Interactive Rebase Commands
pick(p): Use the commit as is. This is the default.reword(r): Use the commit, but stop to edit its commit message.edit(e): Use the commit, but stop to amend its contents (e.g., add/remove files, change code) and/or message.squash(s): Use the commit, but merge its changes into the previous commit. Git will then open an editor to combine the commit messages.fixup(f): Likesquash, but it discards this commit’s log message, keeping only the previous commit’s message. Useful for small fixes you don’t want to explicitly mention.drop(d): Remove the commit entirely.
Our Goal: Clean Up the Validation Commits
Let’s modify the editor content to achieve a cleaner history:
- Squash
fix: Rename isValidPassword functionintofeat: Add password validation function. - Squash
fix: Improve password validation namingintofeat: Add username validation(or combine it into the password one if it’s related). For simplicity, let’s squash it into the previous feature commit.
So, we’ll change the file to:
pick j3k4l5m feat: Add email validation function
pick f0g1h2i feat: Add password validation function
fixup a7b8c9d fix: Rename isValidPassword function # Use 'fixup' to discard its message
pick c4d5e6f feat: Add username validation
fixup e1f2g3h fix: Improve password validation naming # Use 'fixup' to discard its message
# ... (rest of the comments)
Important: The fixup and squash commands always apply to the commit above them in the list.
Save and close the editor. Git will then proceed with the rebase.
If you used squash, Git will open another editor for you to combine the commit messages. If you used fixup, it will silently combine them.
After the rebase completes, check your git log --oneline:
git log --oneline
You should now see a much cleaner history, with the “fix” commits integrated into their logical feature commits. The number of commits will be reduced, and their IDs will be new.
This is incredibly powerful for maintaining a clean, understandable project history, especially before creating a Pull Request or Merge Request for your team to review.
Mini-Challenge: Consolidate Your Work
You’ve been working on a new data-fetching module. You started with a basic setup, then added error handling, then refactored a small part, and finally added a loading state. You now have four commits:
feat: Initial data fetching module setupfeat: Add error handling to data fetchingrefactor: Minor cleanup in data fetching logicfeat: Implement loading state for data fetching
Challenge: Use git rebase -i to consolidate these four commits into two logical commits:
- One commit for the initial setup and error handling.
- One commit for the loading state and refactoring.
Steps:
- Create a new Git repository or a new branch in your existing one.
- Make the four commits as described above.
- Start an interactive rebase to target these four commits.
- Use
squashorfixupto combine them as requested. - Edit the commit messages to be clear and concise for the two new commits.
Hint: Remember that squash and fixup apply to the commit above them in the interactive rebase editor. You might need to reorder commits slightly in the editor to get the desired grouping.
What to observe/learn: After completing the challenge, use git log --oneline to verify that your history now reflects only two commits, each with a clear, combined message, representing the consolidated work. This demonstrates how you can group related changes for better readability.
Common Pitfalls & Troubleshooting
- Rebasing Public/Shared Branches: We’ve covered this, but it’s worth reiterating: Never rebase commits that have already been pushed to a public or shared remote repository. Doing so will cause headaches for your collaborators. If you must, the only way is to
git push --force-with-lease(orgit push --force), but only if you are absolutely certain no one else has pulled your old commits, or if you’ve communicated clearly with your team and they are prepared to handle the rewritten history. This is an advanced, risky operation. - Forgetting
git addduring Conflict Resolution: Just like withgit merge, if Git pauses for a conflict during rebase, you must resolve the conflict in the file(s) and thengit addthe resolved file(s) to the staging area before runninggit rebase --continue. Forgettinggit addwill result in Git complaining that you haven’t marked conflicts as resolved. - Getting Lost in
rebase -i: The interactive rebase editor can be intimidating. If you find yourself confused or make a mistake in the editor, simply close it without saving changes (e.g.,:q!in Vim) or usegit rebase --abortto return to the state before the rebase started. - Losing Commits: While rebase rewrites history, Git’s
reflogis your safety net. If you accidentally drop a commit or mess up a rebase,git reflogwill show you a history of where your HEAD has been, allowing you to find the commit hash of your previous state and reset back to it (git reset --hard <commit_hash>). - Reordering Commits with Dependencies: Be careful when reordering commits in
rebase -i. If commit B depends on changes introduced in commit A, and you reorder them so B comes before A, Git will likely encounter conflicts or errors because B’s changes won’t have their prerequisites.
Summary
Congratulations! You’ve unlocked the power of git rebase. You now understand:
- The fundamental difference between
git mergeandgit rebaseand their impact on commit history. - How
git rebasecreates a clean, linear history by replaying your commits on top of a target branch. - How to resolve conflicts that arise during a rebase operation.
- The immense power of
git rebase -i(interactive rebase) for sculpting your local commit history: squashing, fixing up, reordering, editing, and dropping commits to create meaningful, atomic changes. - The crucial “golden rule” of rebasing: Never rebase commits that have already been shared with others.
- Common pitfalls and troubleshooting techniques when working with rebase.
Rebasing is a powerful tool that, when used correctly, can significantly improve the clarity and maintainability of your project’s commit history. It’s a hallmark of professional Git workflows, especially for preparing feature branches for review.
In the next chapter, we’ll build on this knowledge to explore advanced branching strategies and team workflows, integrating these techniques into a collaborative development environment.
References
- Git Official Documentation: git-rebase
- GitHub Docs: About Git rebase
- Atlassian Git Tutorial: Rebasing vs Merging
- GitLab Docs: Rebase and resolve merge conflicts
This page is AI-assisted and reviewed. It references official documentation and recognized resources where relevant.