Engineering

Mastering Git Rebase: A Comprehensive Guide

Akshay Pilankar
Sakshi Jain

Have you ever found yourself staring at a commit history that looks like this?

main     A---B---C---D---E---F
          \
feature    G---H---I---J---K

If you're thinking, "I wish I could clean this up before merging into the main branch," you're in the right place. Git rebase is the tool you need, and this guide will walk you through mastering it.

Prerequisites

Before we dive in, make sure you have:

  • A basic understanding of Git (commits, branches, merging)
  • Git installed on your system
  • A sample repository to practice with

What is Git Rebase?

Git rebase is a command that allows you to change the base of your branch from one commit to another, making it appear as if you'd created your branch from a different commit. Essentially, it rewrites your commit history to create a more linear, cleaner history.

Here's a visual representation of what happens during a rebase:

Before rebase:

main     A---B---C
          \
feature    D---E---F

After rebase:

main     A---B---C
                  \
feature            D'---E'---F'

Why use Git Rebase?

  1. Linear Commit History: Rebase helps maintain a clean, linear project history, making it easier to follow the project's evolution.
  2. Integrating Changes: It allows for smooth integration of changes from one branch to another.
  3. Cleaner Merge Process: By rebasing before merging, you can avoid unnecessary merge commits, keeping your history cleaner.

Compared to merging, rebasing provides a cleaner history but comes with its own set of precautions, which we'll discuss later.

Rebase vs. Merge

While both rebase and merge integrate changes from one branch into another, they do so in different ways:

  • Merge preserves the branch history but can clutter the commit history with merge commits.
  • Rebase rewrites the project history by creating new commits for each commit in the branch.

Choose merge when you want to preserve the exact history of your branch. Choose rebase when you want to create a linear history.

When to use Git Rebase?

  1. Cleaning up feature branches: Before merging a feature branch back into the main branch, use rebase to tidy up your commit history.
  2. Staying up-to-date with main: While working on a long-running feature branch, periodically rebase onto the main branch to incorporate the latest changes.
  3. Before pull requests: Rebase your branch onto the main branch before creating a pull request.
  4. Collaborative projects: In team projects, rebasing can keep the overall project history clean and easier to understand.

How to perform Git Rebase

Basic rebase:

git checkout feature-branch
git rebase main

This command moves the commits from feature-branch to the tip of main, replaying each commit on top of the main branch's latest commit.

Important note: If conflicts occur during this process, Git will pause and allow you to resolve the conflicts manually. After resolving, stage the files with "git add ." and continue the rebase process "git rebase --continue"

Interactive rebase:

Interactive rebase allows you to interact with each commit before it's applied. You can edit commit messages, squash commits, reorder commits, and more.

git rebase -i HEAD~3

This command will open an editor showing the last three commits:

pick f7f3f6d Change button color
pick 310154eUpdate header text
pick a5f4a0d Add new feature

You can change the word 'pick' to:

  • reword: change the commit message
  • edit: amend this commit
  • squash: meld into previous commit
  • drop: remove the commit

After you have updated the commit instructions, and saved and exited the file, Git will apply your commits accordingly.

Caution: Force push and Protected branches

Important: Rebasing rewrites commit history. If you've already pushed your branch to a remote repository and then rebased locally, you'll encounter a situation where your local and remote histories have diverged. Here are your options:

git push origin feature-branch --force

However, force pushing can be dangerous, especially on shared branches. It's recommended to:

  1. Never rebase or force push to the main branch.
  2. For feature branches, consider these alternatives:
    • Pull and merge from the remote branch before pushing. This preserves remote history but may add merge commits.
    • Use "git pull --rebase followed by a regular push. This maintains a linear history without force pushing.
  3. Set up branch protection rules in your Git repository to prevent force pushes to important branches.

For example, in GitHub, you can set up branch protection rules:

  1. Go to your repository settings
  2. Click on "Branches"
  3. Add a branch protection rule for your main branch
  4. Check "Require pull request reviews before merging" and "Restrict who can push to matching branches"

Best practices

  1. Don't rebase public/shared branches: Rebase rewrites history, which can cause problems for other developers who have based their work on the original branch.
  2. Use "git rebase --abort" if things go wrong: This command will stop the rebase and return your repository to its state before the rebase began.
  3. Communicate with your team: Make sure everyone understands your team's policy on rebasing and force pushing.

Common pitfalls and how to avoid them

  1. Losing commits during interactive rebase: Always double-check your interactive rebase todo list before confirming.
  2. Dealing with merge conflicts: Resolve conflicts carefully, making sure you understand the changes you're accepting or rejecting.

Example scenario: Cleaning up a feature branch

Let's walk through a practical example. Suppose you have this commit history on your feature branch:

A---B---C---D---E  feature
     \
      F---G---H  main

Where commits D and E are messy work-in-progress commits you want to clean up before merging.

  1. First, make sure you're on your feature branch: git checkout feature
  2. Start an interactive rebase:git rebase -i main
  3. In the editor that opens, you'll see something like:pick B Add new button
    pick C Update styles
    pick D WIP: Start implementing new feature
    pick E WIP: Continue new feature implementation
  4. Change it to:pick B Add new button
    pick C Update styles
    squash D WIP: Start implementing new feature
    squash E WIP: Continue new feature implementation
  5. Save and close the editor. Git will then prompt you to edit the commit message for the squashed commit.
  6. After the rebase is complete, your history will look like:
    1. The feature branch is now based on the latest commit of the main branch (H)
    2. Commits B and C have been recreated (hence the ' notation) on top of H
    3. D' represents the squashed commit combining the original D and E              B'---C'---D'  feature              
                   /
      A---F---G---H  main
  7. If you've already pushed this branch before, you'll need to force push:git push origin feature --forceRemember, always use force push with caution, especially on shared branches.

Troubleshooting

  1. "Unable to rebase: You have unstaged changes.": Commit or stash your changes before rebasing.
  2. "The previous cherry-pick is now empty": This can happen if the changes in a commit have already been applied. You can skip this commit using "git rebase --skip".
  3. "fatal: Not possible to fast-forward, aborting.": This occurs when Git can't automatically merge the branches. You'll need to perform a manual merge or consider a different strategy.

Practice exercise

1. Create a new Git repository:mkdir rebase-practice && cd rebase-practice
git init

2. Create some commits on the main branch:echo "Initial content" > file.txt
git add file.txt
git commit -m "Initial commit"
echo "More content" >> file.txt
git commit -am "Add more content
3. Create and switch to a feature branch:git checkout -b feature4. Make some commits on the feature branch:echo "Feature content" >> file.txt
git commit -am "Add feature content"
echo "More feature content" >> file.txt
git commit -am "Add more feature content"
5. Switch back to main and make a commit:git checkout main
echo "Main branch change" >> file.txt
git commit -am "Change on main"
6. Now, rebase your feature branch onto main:git checkout feature
git rebase main
7. You'll likely encounter a conflict. Git will pause the rebase and show you something like:

8. Observe the current history graph:git log --oneline --graph --all

9. Open file.txt in your text editor. You'll see the conflicting changes marked:Initial content
More content
<<<<<<< HEAD
Main branch change
=======
Feature content
>>>>>>> 69bc124... Add feature content

10. Manually resolve the conflict by editing the file to keep the changes you want. For example:Initial content
More content
Main branch change
Feature content

11. After resolving the conflict, stage the file:git add file.txt

12. Continue the rebase:git rebase --continue

13. Git might open an editor for you to modify the commit message. If you're happy with it, save and close the editor.

14. You might encounter another conflict with the next commit. If so, repeat steps 8-12.

15. Once all conflicts are resolved and the rebase is complete, observe the new linear history:git log --oneline --graph --all

16. If you're satisfied with the result, and if you had previously pushed this branch, you would need to force push:git push origin feature --force(Remember, be cautious with force pushing on shared branches!)

This exercise provides a more realistic scenario where conflicts occur during rebasing. It teaches you how to:

  1. Recognise a rebase conflict
  2. Manually resolve conflicts
  3. Continue a rebase after resolving conflicts

Conclusion

Git rebase is a powerful tool for maintaining a clean and organized project history. By understanding when and how to use it, along with the necessary precautions, you can significantly improve your Git workflow and collaboration with your team. Remember, while rebase can create a cleaner history, it's important to use it wisely, especially on shared branches. Happy rebasing!

Akshay Pilankar
Senior Software Engineer @ Kapstan. Akshay has a passion for full-stack development and excels in crafting scalable solutions with JavaScript. With over 6 years of experience, he drives innovation in every project. Outside of work, Akshay is a passionate musician and gamer.

Simplify your DevEx with a single platform

Schedule a demo