Gwilym’s blog

Hopefully someone finds something useful here

Git rebase like a pro

I felt a lot like I was fumbling in the dark when using git for the first year or so. Tools like learngitbranching were great for getting me going, but I often felt like I didn’t have as much control over my commits and the history as I would like. And when working in a team, merge conflicts become quite common. The race to be the first to merge the PR so you don’t have to deal with the conflict becomes quite heated.

I used to get upset whenever I had to resolve a merge conflict because it would take ages to work out what I would have to fix. Or if I realised that I’d made a mistake about 10 commits ago and don’t want anyone to know. And sometimes you want to develop on someone else’s feature branch rather than off main. What do you do when the feature branch you’ve been working on gets merged?

Learning about interactive rebases felt (and still feels like) a super power.

Note however that rebasing can cause a bit of pain if you’re working with people less familiar with git. But hopefully with these few commands and explanations you’ll be git rebasing like a pro in no time!

The most important step

The most important step before doing a rebase you’re not 100% sure will work fine is put down a temporary branch to jump back to. I’ll repeat that in big so that people skipping through this spot it

Put down a temporary branch before a big rebase

Just doing git branch tmp/before-rebasing before you rebase will save you a big headache if you do something horribly wrong. Yes there are always ways back, but if git decides to garbage collect your commit before you notice you’ll be in a difficult space with potential losses of work.

So, now that you’ve done that, I’ll continue this article.

Contents

This article is quite long, and the subsections and sub-subsections I’ve used for some reason make this a little difficult to navigate. So here are some quick links if you know what you want to do:

1 My PR has a merge conflict

2 My branch’s history makes no sense

3 Someone rebased a branch I was looking at locally

4 I branched of someone else’s branch, now that’s merged and I want my branch off main

What does a rebase actually do?

A rebase is a kind of ‘history rewriting’ command. In essence, it lets you take a group of commits, and apply them to on top of a different commit.

But, as I will show in some of the examples below, it lets you do all sorts of other operations too.

Note I will always run a so called interactive rebase. This causes git to show you what it plans to do, and gives you a chance to change your mind, cancel the rebase etc.

I think the easiest way to show what a rebase does and why you’d use it is to give some examples of when it is useful and how to use it.

1 My PR has a merge conflict

This is probably the easiest and most common time that I would rebase in my day-to-day work. It is also where I would recommend people get their feet wet and practice before moving on to some of the more advanced use cases below.

I’m going to assume your PR is against a branch named main and that the remote location is called origin. Both of these can be swapped for any branches (note that you’ll need to refer to a later section if the branch your PR is against has itself been rebased).

You are currently working on the branch feature in this example of what we’re going to do, and suppose that there is a merge conflict.

feature branch

Normally the way I see people do this is by first merging main into their own branch, and then later merging their branch into main. I argue that this is ugly and also less useful:

  1. It is harder for you because you have to fix every single merge conflict at the same time. What I mean by that is if you have multiple files conflicting, you have to fix them all. And this can get pretty complex if you have made some deep refactor.
  2. It is harder for the reviewer because you could change anything in that merge commit, and it can be very difficult to follow that commit specifically.
  3. Also, looking commit-by-commit at the resulting code requires knowledge of how the system used to be rather than how it is currently.
  4. And finally, aesthetically it makes the code graph look really ugly.

This is what your history will end up looking like with the git merge strategy mentioned above

end result of git merging into your branch

And this with git rebase

end result of git rebasing your branch

Please forgive the strange arrow directions here, not sure what’s going on with the visualiser…

1.1 Put down a temporary branch

Okay, I’ll admit that in this case I would normally not. But if this is the first time you’re doing a git rebase, please do it.

$ git branch tmp/before-rebasing

Remember that git branch creates a branch pointing to your current commit, but doesn’t switch to it. You can go back to this if something goes horribly wrong in the next step.

1.2 Start the interactive rebase

This is the magic of rebasing. I will always rebase interactively. If only to know exactly what git is going to do once it starts.

Firstly, ensure your local pointer to the origin/main is up to date with

$ git fetch

Then, do the rebase

$ git rebase -i origin/main

This will open your currently configured text editor (controlled by the EDITOR environment variable if you want to change it), showing you the list of commits it will replay.

If you’re happy with the list, then save and close the file and git will get to work reapplying your commits one at a time.

1.3 Merge conflict!

You’re doing this to resolve a merge conflict, so you will get one eventually. At this point you have to do something you may not be entirely familiar with. Look at the commit you’re in at the moment. This will be important.

The current state of your files will be as-if you’ve done the commits up to this point on top of main rather than your historical version that you actually did the work in. So when you fix the merge conflict, you have to leave those files in the state you would’ve had it in at this point in your branch (not the end result) had main looked like it does now. This is because after you’ve fixed this conflict, the rebase will continue to run the remaining commits.

Once you’re happy with the changes, you can git add the files you’ve updated to tell git you’ve fixed the conflict.

And then run

$ git rebase --continue

to continue the rebase.

Once that’s all done, push your branch up to origin with

$ git push --force-with-lease

and you’re good to go.

1.4 Help, something went wrong!

It happens sometimes. How to fix it depends on at what point you’re at for your rebase.

1.4.1 The rebase totally failed but it claims to have finished

Good thing you put a temporary branch down at the start. Simply resetting your git branch to that temporary place will fix the issue.

$ git reset --hard tmp/before-rebase

If you were a terrible person and didn’t put a temporary branch down, all is not lost (but it is definitely slightly harder). Provided you haven’t pushed your changes yet, you can jump to the old location according to where it is in origin, with

$ git reset --hard origin/feature

If you’ve pushed, you may be able to get an old commit hash for the end of your old feature branch from somewhere, and then you can do

$ git reset --hard <commit hash>

And if you’ve done none of those things, take this as a lesson to read the text in big writing. Or look up the git reflog and have a go at working with that.

1.4.2 The rebase is still in progress but I don’t want to do it any more

This is a nice easy fix.

$ git rebase --abort

will take you back to where you started.

1.4.3 I thought I wanted this commit, but I don’t really

Running

$ git rebase --skip

allows you to completely skip this commit and continue with the next ones. This comes up occasionally when you fix a bug in some existing code because your new feature is exercising it but someone beat you to the fix. Then it makes no sense to keep this commit, so skip it.

2 My branch’s history makes no sense

This is probably my favourite usage of git rebasing. Not only is it super useful in making the reviewer’s job easier, but it also hides your silly mistakes which will make you look like a l33t haxxor.

I do this if I want to

  1. Change a commit message for a commit a little bit back in the history
  2. Split some commits into more pieces
  3. Combine multiple commits into one
  4. Reorder some commits to makes more sense to a reviewer
  5. Pretend I wrote the tests first

2.1 Put down a temporary branch

You bored of me saying this already? I’ve had more complaints about people not doing this step then anything else with git rebasing. So hopefully this’ll stop that

Something along the lines of the following will save you.

$ git branch tmp/before-rebasing

2.2 Start the interactive rebase

In this case, we’re not going to be rebasing to the tip of main (although you could decide to do that if you want). So here we will instead find the commit hash of the oldest commit in your branch. I like to find it using git log and scrolling back but you can do this any way you want.

Lets say my commit hash is deadbeef, use the command

git rebase -i deadbeef

and this time we’re actually going to change the contents of the file a little.

You’ll see something along the lines of this in the file.

# 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.
#
# Note that empty commits are commented out

Pretty much the rest of this section will be what these do, ordered from most useful to least.

2.2.1 pick

You’ll see a lot of these by default. One for every commit hash in your branch. Reorder them in any order, and importantly remove any for commits you don’t want any more.

2.2.2 fixup

If you did a commit, and then the next commit is just fixing a typo in something in the previous commit (or you have a commit that fixes a typo in a commit some time in the past), you can follow up your old commit with this fixup.

As an example, suppose it looked something like this

pick deadbeef1 Add an article about the C preprocessor
pick deadbeef2 Add an article about PAL video processing
pick deadbeef3 Fix typo in C preprocessor article

Change the file to the following before rebasing and the end result is your commit adding the article about the C preprocessor will also contain the typo fix.

pick deadbeef1 Add an article about the C preprocessor
fixup deadbeef3 Fix typo in C preprocessor article
pick deadbeef2 Add an article about PAL video processing

The difference between fixup and squash is that squash awkwardly adds the commit message of the squashed commit to the commit message of the commit it is being squashed into. So I use fixup much more often then squash.

2.2.3 edit

Maybe you want to edit an entire commit. This is super useful if you accidentally commit something you didn’t mean to. For example, maybe you stuck a debugger statement in a js file somewhere while you were developing it and accidentally committed it.

Putting edit before that commit will allow you to remove the debugger line once git gets to that point in the rebase. There is something very important to do here though (and a lot of people get this wrong the first time).

Once you’ve fixed up that commit, git add the files you’ve modified and then run git rebase --continue. DO NOT COMMIT THE CHANGES. The git rebase --continue will do the commit with the old commit message for you.

2.2.4 Removing the line entirely

If you don’t want the commit any more, just don’t include the line with that commit in the rebase command list. Then when git does the rebase, that commit won’t be included.

2.3 After rebasing

Once you’re done, if you have already pushed your branch you’ll have to force push it to origin like the merge conflict example above.

$ git push --force-with-lease

And now it looks like you never make any mistakes. I’m sure your colleagues will be incredibly impressed.

2.4 Help, something went wrong!

The same advice applies as from the previous section

3 Someone rebased a branch I was looking at locally

This can be annoying if you’ve not seen it before. You run git pull and you get some strange errors or it is telling you it needs to commit something.

If you have done some of your own work on this branch too, then put down a temporary branch first, and then after this do step 4 on that temporary branch.

Afterwards, git fetch to update your local copy and then git reset --hard origin/branchname gets you back in place.

4 I branched of someone else’s branch, now that’s merged and I want my branch off main

This is a long title, but I couldn’t come up with a more descriptive name. This works very similarly to problem #1 but with a slight modification.

Just running git rebase origin/main will cause the rebase to pick up not only your changes but also all of the changes in the branch you branched off. And git really doesn’t like applying a diff twice.

Firstly, put down a temporary branch to go back to in case things go horribly wrong with git branch tmp/before-rebase.

So, you need to find the point at which you branched off (for example by using git log or similar), and then run

$ git rebase -i <commit hash> --onto=origin/main

At this point, everything will go ahead like in #1. And of course you could also apply the steps in #2 to change what your branch history looks like.

Conclusion

Hopefully with these tips you’ll be rebasing like a pro. This was a difficult article to write because I wasn’t sure how to organise it. However, hopefully once you’ve mastered these techniques, you’ll really understand not only the power of git, but also how helpful having a clean history in your project is.