Rebases in Git and why you shouldn’t be afraid of them

marcinwosinek

Marcin Wosinek

Posted on December 7, 2022

Rebases in Git and why you shouldn’t be afraid of them

Beginners often experience several points of confusion about rebases when using Git:

  • what they are
  • why people do them
  • why pushes might fail after successful rebase

In this article, I’ll provide answers to those questions. In the meantime, TL;DR: rebases are a way of making your crude commit history into something you’ll want to share with the rest of your team.

How to do a rebase

To perform a rebase, you run git rebase <commit-reference>. Commit reference can be anything—for example:

  • branch name,
  • tag, or
  • commit id.

A rebase is a fairly complicated operation, so let’s walk through various aspects of it.

Simple rebase

In its simplest form, rebasing takes changes from one place (one base), and moves them to another. It changes the spot where the history branched off. So with the tree alias I wrote about previously, we start with a branch graph like this:

$ git tree
* 293b722 (HEAD -> test) add test.txt file
| * abc01e7 (origin/main, origin/HEAD, main) Add lorem ipsum to readme
|/
* edd3504 Add readme
Enter fullscreen mode Exit fullscreen mode

and we move our branch to start from a different place—the top of the main branch:

$ git rebase main
Successfully rebased and updated refs/heads/test.
Enter fullscreen mode Exit fullscreen mode

and as a result, we get a tree like this:

$ git tree
* fe4254e (HEAD -> test) add test.txt file
* abc01e7 (origin/main, origin/HEAD, main) Add lorem ipsum to readme
* edd3504 Add readme
Enter fullscreen mode Exit fullscreen mode

Our test branch used to start at edd3504 Add readme, and now it’s starting at abc01e7 Add lorem ipsum to readme.

Rebase in progress

Even a simple rebase may require additional input from your side. For example, you might have some conflict to resolve:

$ git rebase main
Auto-merging test.txt
CONFLICT (add/add): Merge conflict in test.txt
error: could not apply 293b722... add test.txt file
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply 293b722... add test.txt file
Enter fullscreen mode Exit fullscreen mode

When checking the status in such a situation, you will get an instruction from Git on how to proceed:

$ git status
interactive rebase in progress; onto a03989b
Last command done (1 command done):
   pick 293b722 add test.txt file
No commands remaining.
You are currently rebasing branch 'test' on 'a03989b'.
  (fix conflicts and then run "git rebase --continue")
  (use "git rebase --skip" to skip this patch)
  (use "git rebase --abort" to check out the original branch)

Unmerged paths:
  (use "git restore --staged <file>..." to unstage)
  (use "git add <file>..." to mark resolution)
        both added:     test.txt

no changes added to commit (use "git add" and/or "git commit -a")
Enter fullscreen mode Exit fullscreen mode

You need to edit the file to resolve the conflict. When you are done with it, you add the change to staging:

$ git add test.txt
(no output)
Enter fullscreen mode Exit fullscreen mode

and you continue with the rebase:

$ git rebase --continue
[detached HEAD b81cad4] add test.txt file
 1 file changed, 1 insertion(+)
Successfully rebased and updated refs/heads/test.
Enter fullscreen mode Exit fullscreen mode

Subsequently, Git will continue with the rebase: it can finish smoothly as in my case, or you may encounter more conflicts to resolve manually.

Rebase interactive

A more advanced option for rebase is rebase interactive. It allows you to make very precise changes on the branch—not just move it from one place to another. Let’s take a look at a few examples below.

Reword commits

The simplest thing you can do is just change the commit message. The good thing is that there is no chance of conflict because the files are left the same.

Squashing commits

You can choose a few commits and turn them into one. It preserves the file's changes—it just collapses a chain of changes into one commit. It requires writing a new commit message for the new commit, but on its own it will not cause conflicts.

Editing commits

You can edit the file changes in the commit too. This allows you to make sure the commits contain all the relevant changes. If there are changes to the same places later in the branch, then you will need to resolve conflicts manually.

Removing commits

Another operation is removing commits from the branch—the commit and its changes. If there were additional modifications to the same code later in the branch, removing the commit will cause some conflicts. The most basic use case is when you realize that some changes are not needed, and you want to remove them from the history. Besides that, I often use it when I realize my branch is getting too big, and I intend to make it more focused and merge it sooner.

Reordering commits

You can reorder commits as well. There are situations in which it can make sense, but it gets complicated quickly if you try to reorder a commit that changes the same area of code.

File interface

As you can see, interactive rebasing requires a lot of subtle input. Git gets this input in the form of a text file. When I run git rebase main -i, I get my editor with the following content:

pick a03989b add test.txt

# Rebase abc01e7..a03989b onto abc01e7 (1 command)
#
# 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 [-C | -c] <commit> = like "squash" but keep only the previous
#                   commit's log message, unless -C is used, in which case
#                   keep only this commit's message; -c is same as -C but
#                   opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# 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, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# .     create a merge commit using the original merge commit's
# .     message (or the oneline, if no original merge commit was
# .     specified); use -c <commit> to reword the commit message
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
Enter fullscreen mode Exit fullscreen mode

When you save the file and exit the editor, Git will follow the instructions you provided there. It’s an interface that can feel awkward at first, but one can get used to it pretty quickly. The only “gotcha” is that you need to know your default editor, and know how to use it. I’ve written a bit more about it in a section of another article.

Practical tip

While doing interactive rebasing, try to do one thing at the time. Git can manage to do the following in one rebase:

  • change the branch starting point
  • reorder some commits
  • squash others
  • etc.

However, you’ll probably get lost in all the conflicts that will happen all at once. It’s easier to do one thing at the time and keep the following order:

  • remove superfluous commits first,
  • squash related commits, and then
  • move the branch to another base.

In this way, you reduce the number of commits that have to be moved around—thereby reducing the number of conflicts you must resolve.

History changes

Rebasing changes the history of the repository. This means that the same changes will appear as different commits on different branches. It’s not a mistake, but it can be confusing when you see the following logs for the first time:

$ git push origin test
To github.com:how-to-js/git.md.git
 ! [rejected]       test -> test (non-fast-forward)
error: failed to push some refs to 'github.com:how-to-js/git.md.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details
Enter fullscreen mode Exit fullscreen mode

non-fast-forward means there is no direct route way for one point in the repository history toward another. In this case, you need to integrate remote and local changes yourself. There are some subtleties to getting branches in sync, and it’s a topic for another article.

Depending on the workflow in your project, it can be forbidden or required to override the history of some branches. The workflow I use prohibits history changes to the master/main branch, and it requires rebases of all the other branches. There are no right or wrong approaches to rebasing: each policy has its pros and cons, but it’s likely that your team will insist that you maintain consistency with the selected approach.

Possible side effects

Besides the intrinsic complexity of rebases, they introduce some challenges in team environments:

local-only branches

When your work is on your computer only, it’s always safe to rebase or change the history in any other way. It’s a good practice because it always helps you to clean up your work before others can see it. If you started some work a month ago and since then, you’ve made changes to main regularly—it’s not worth preserving information with commits such as Merge remote-tracking branch 'origin/main' into test. Instead, you can rebase regularly and keep history as a straight line.

Branches that were uploaded to remote

Once you share your branch, any change to its history can break stuff. For example, your continuous integration (CI) will remember the results of a test run of a commit, but the commit will be gone. I’ve met people who are very cautious about changing a history that was already shared, so I guess there are teams where this might be discouraged.

Remote branches that other works on

The most complicated case is to change the history of a branch that other people are working on. For that, I would recommend:

  • making sure everybody is up-to-date when the change happens—that there are no two people introducing changes to the branch at the same time;
  • getting everyone updated to the new, rebased branch before they start the new work in it.

This is a delicate situation because it breaks a lot of the automated conflict resolution Git employs. It’s good to be careful in such cases and make sure everybody knows what’s happening.

Keep on learning

If you are interested in learning more about branches in Git, a great (and pretty) resource is available at Learn Git Branching. If you are interested in learning more about Git, sign up here to get updates about my Git-focused content.

💖 💪 🙅 🚩
marcinwosinek
Marcin Wosinek

Posted on December 7, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related