Demystifying git rebase - Phelipe Teles

Demystifying git rebase

8 min.
View source code

git rebase is a command for rewriting history, which sure seems scary at first but you’ll learn to love it.

Why learn git rebase?

As a beginner, I remember being hard to understand what rebase even was. I can’t quite articulate why, but I’m guessing because I didn’t see when I would need to use it.

It turns out you don’t need rebase for most of your daily tasks, you can work just fine knowing the basics, pull-add-commit-push.

git rebase is very similar to git merge, in that you can also use it to get the latest commits in an upstream branch. But, other than that, you’ll also use it to:

  • change the order of commits.
  • join commits.
  • split a commit.
  • edit a commit, e.g. to remove an accidentally committed .env file.
  • edit a commit message.
  • delete a commit.

This is most commonly used to tidy up the commit history. And that’s it, that’s the major reason why I use rebase, to organize my commit history.

In this blog post, I’ll try to explain what rebase is with examples of how I use it daily.

Understanding git rebase

git rebase tries to replay a bunch of commits on top of another branch.

Let’s imagine you’re working on a feature branch and the main branch was updated with some code you want:

[No name]
      E---F---G feature     /A---B---C---D main

You can run git rebase main, with the feature branch checked out, to achieve the following result:

[No name]
              E'---F'---G' feature             /A---B---C---D main

The commits E, F and G are now different. They are different commits, their hash changed, because their parent commit changed.

This is similar to git merge main, but the resulting branch is cleaner because the history is linear: it looks like you never branched off of an old commit of the main branch.

Reasons to be afraid of rebase

Because rebase implies changing the commit identity (their hash), you should never rebase public commit history, meaning a commit history that someone else might have worked on.

To illustrate, let’s imagine a feature branch that you have pushed to remote:

[No name]
              E---F feature             /A---B---C---D main

For some reason, a co-worker decided to collaborate and added one more commit:

[No name]
              E---F---G feature             /A---B---C---D main

But, before he did it, you decided to rebase your branch, maybe because you wanted to improve E’s commit message, and pushed it.

[No name]
              E'---F' feature             /A---B---C---D main

Now, your co-worker will not be able to push their changes, because the G commit was made on top of F, but it’s gone from the remote branch.

git will tell your coworker that his changes were rejected because “remote contains work that you do not have locally. […] You may want to first integrate the remote changes”.

He could use git force --push, but things would only get worse because he would’ve thrown away your work. Now that the damage is done, you should refer him to the RECOVERING FROM UPSTREAM REBASE section available in the git-rebase man page.

Reasons NOT to be afraid of rebase

You shouldn’t be afraid of losing work — if you committed your code, it’s very hard to lose it.

Even if you did mess things up, you can always search a previous known good state of your branch with git reflog feature or git reflog HEAD, and then reset to that state, e.g. git reset --hard feature@{1} or git reset --hard feature@{one.min.ago}. Read the docs for more about git-reflog.

Interactive rebase

You may be wondering now: ok, but how will rebase help me to split a commit?

This is possible with the interactive mode, available with the --interactive option. In this mode, git will open your text editor with a list of every commit that will be replayed on top of another branch:

[No name]
pick df4adc Epick 180a94 Fpick 490b6c G# Rebase 3409df0 onto main (3 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) 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.#

This is known as the rebase todo list. Here, you’re supposed to describe what git should do at each commit in order to help you achieve the desired commit history.

The default command for every commit is “pick”, meaning “replay this commit in this specific order, unchanged”.

This is just a text file, you can edit it however you like. When you’re done, save and close the file.

Now let’s see in practice how to use git rebase -i.

Changing the order of commits

Let’s say you want to reverse the commits order, given this commit history:

[No name]
              E---F---G feature             /A---B---C---D main

After running git rebase -i main, you’ll be met with the following todo list.

[No name]
pick df4adc Epick 180a94 Fpick 490b6c G

Put the commits in reverse order:

[No name]
pick 490b6c Gpick 180a94 Fpick df4adc E

Save and close the file to continue. In case there is no conflicts, your branch will then look like this:

[No name]
              G'---F'---E' feature             /A---B---C---D main

Join commits, without changing commit message

Here’s a more likely scenario: you committed some broken code a while ago and wants to fix that commit.

If the commit you want to fix is the most recent one, just use with git commit --amend. If you don’t want to change commit message you can also use git commit --amend --no-edit.

In case the commit is not the latest one, you’ll need to rebase. If you don’t care about the commit message, you can use the fixup command.

Let’s look at an example, suppose you have the following branch:

[No name]
df4adc Add script180a94 foo490b6c bar

The commit “Add script” has some broken code you want to fix. You implemented the fix and committed it with git commit -m "Fix script":

[No name]
df4adc Add script180a94 foo490b6c bar3409df Fix script

After running git rebase -i, you get the following todo list:

[No name]
pick df4adc Add scriptpick 180a94 foopick 490b6c barpick 3409df Fix script

To merge the two commits, you can move the “Fix script” line up and use the fixup command, instead of pick.

[No name]
pick df4adc Add scriptfixup 3409df Fix scriptpick 180a94 foopick 490b6c bar

The resulting history will look like you never introduced the bug:

[No name]
2d5b33 Add scriptc404e9 foo20ec55 bar

Another way to achieve the same result is to commit with git commit --fixup=df4adc (or git commit --fixup=:/"Add script"). And then run git rebase --interactive --autosquash. This will save you the step of editing the file manually, git will do it automatically.

[No name]
pick df4adc Add scriptfixup 3409df0 fixup! Add scriptpick 180a94 Add environment variablespick 490b6c Add .gitlab-ci.yml

Join commits, but preserve their commit messages

If we want to join multiple commits and preserve its message, we need to use the squash command.

The only difference from using the fixup command is that you’ll get the chance to edit the commit’s message while rebasing:

[No name]
# This is a combination of 2 commits.# This is the 1st commit message:Add script# This is the commit message #2:squash! Fix script# Please enter the commit message for your changes. Lines starting# with '#' will be ignored, and an empty message aborts the commit.## ...

Similar to the --fixup option in git commit, you can also commit with git commit --squash=sha.

Split a commit

To split a commit, you want to use the edit command.

Git will stop at that commit to let you edit it however you want. After you’re done editing, run git rebase --continue.

To split a commit, you can first “undo” it with git reset, then just add changes and commit them differently.

$ # undo the commit, but keep its changes in the working tree$ git reset HEAD^$ git add foo$ git commit -m "One commit"$ git add bar$ git commit -m "Another commit"$ git rebase --continue

Remove a file from a commit

To remove a file, you’ll need to edit a commit as well.

But now, it’s more convenient to use git reset --soft to “undo” the commit, because it’ll keep the changes in the staging area. Now all you need to do is unstage the file you want to remove and commit again.

$ # undo the commit, but keep its changes in the staging area$ git reset --soft HEAD^$ # remove the file from the staging area$ git rm --cached .env$ # reuse the previous commit message$ git commit -c ORIG_HEAD

Edit a commit message

To edit a commit message, use the reword command. git will open your editor to let you edit the message.

Delete a commit

To delete a commit, use the drop command or just delete the line.