Git Rebase Struggles Conceptualized With Pikmin

Git Rebase Struggles Conceptualized With Pikmin

If you haven’t already, take a look at my first git rebase / Pikmin blog post about how to learn and use git rebase.  That post covers the basics, while this one will conceptualize how to use git rebase when things go bad: conflicts, force pushing, and working with a lot of development churn.

git rebase is a real treat when there are no conflicts, but that’s often not the case when you’re working on a fast-moving development team or contributing to an open source project.  There’s a good chance you will be working on the same files, at the same time or your work lags behind what the open source project is doing.  

This increases the likelihood for conflicts.  Throw in different branches, different merge strategies, and the different complexities of coding in general.  But hopefully, these adorable Pikmin can help you traverse the minefield.

Let’s start a small example so you can see how conflicts can occur and then how to fix them.  In this example, I want to bump the version of a file in the tccutil repo because of a change I’ve made.

It’s a one line change so this is a perfect example to show how merge conflicts can happen and how to fix them.  I add and commit my change.  

I now have a unique commit that does a unique thing (this is the Pikmin in the analogy as you can see in the image below).  

This is a friendly way to help conceptualize commits; it's why I wrote the first blog post--to help conceptualize that:
  • each commit is unique and does a unique thing
  • each pikmin is unique and does a unique thing
  • you can modify each commit to do something else if you need
  • you can modify each pikmin to do something else if you need
  • you can rearrange commits to any order you need
  • you can rearrange pikmin to any order you need

Now back to the example: I haven’t pushed my branch yet, and everything is local to my machine right now.  But it’s been a few weeks since I pulled in any upstream changes, so now I want to rebase my changes into the latest code produced in the tccutil repo.  

In other words, before I make a pull request, I want to make sure I have the latest code produced by someone else.  

You can do a git pull and git merge here, but many repos will have you rebase onto a branch before accepting a PR.  And if you have to do several merges over time, it's just kind of messy.  I have found it much easier to visualize each commit as a consumable piece of code developed by someone.  Rebasing puts your commits right on top of someone else's up-to-date commits.  Because of this, I pretty much always use git rebase when developing.

Since my local repo is out of date with the upstream repo, I:

  • switch to (my local) master branch (this is the branch I want to target my PR against)
  • git pull in the latest changes
  • switch back to my local development branch
  • rebase my branch onto master
git checkout master 
git fetch && git pull
git checkout more-verbose-error
git rebase -i master

I'm then thrown into vim, which shows my single commit, 7783be6.  It also shows the latest commit that was pulled in.

So in the screenshot above, e1db8b2 is what's currently in master as shown below.  So we are rebasing my commit (7783be6--the yellow pikmin) onto the latest master commit, e1db8b2 (the blue pikmin).  

I also have this PR live for you to view: https://github.com/jacobsalmela/tccutil/pull/37

I only have one commit and I want to keep it, so I just :wq my changes in and then the rebase process continues.

But uh-oh!  A conflict!

My yellow pikmin (commit 7783be6) could not be applied on top of master (blue pikmin).  These conflicts happen when the line(s) you edit are the same line(s) as those edited by someone else.  

In other words, this conflict means someone upstream edited the same line in the same file on the branch I’m trying to rebase onto.  Since git doesn't know which piece of code is correct, the rebase halts and asks for human intervention.

Here is that error message broken down a bit:

  • Auto-merging tccutil.py: git is attempting to auto-merge the delta of changes
  • CONFLICT (content): Merge conflict in tccutil.py: There's a conflict in the content of this file (you can have conflicts where someone removed the file that you are editing, which is not the case here)
  • error: could not apply 7783be6...update error and bump version: this shows my commit (the yellow pikmin) and my commit message
  • Resolve all conflicts manually.  Mark them as resolved with git add/rm...: this tells you go look in the file(s) in that commit, fix them, git add them, and the git rebase --continue.  Essentially, you can't do anything here until the conflicts are resolved.

Since git can’t determine which code is correct, it’s up to us humans to figure it out.  To do this, you need to edit the file in question.  git puts in some markers so you know where you need to make changes.

So I open tccutil.py and I start looking for something like this:

<<<<<<<< HEAD
    some code from someone else
========
    my code
>>>>>>>>

This sometimes-confusing looking chunk of code is what you need to fix.  You should see:

  • <<<<<<<< HEAD indicates the start of the upstream code (their changes)
  • ======== indicates that our changes are below
  • >>>>>>>> closes out the beginning marker (nothing else past this marker needs to be modified (unless there is another start marker >>>>>>>)

I opened my file in vim and found the offending lines (116-120).

Line 117 is the upstream change (what's currently in master).  Line 119 is my change.  It's now up to you, the human, to determine the best course of action.  

This conflict is a little easier to see if you have a modern text editor with built-in git functionality, as they often come in with fancy buttons and colors to help you fix the conflict a bit easier:

If you were logged into Github (and had pushed your branch), you would see a Resolve conflicts button,

which shows the same thing you see in your text editor of choice:

Now as I mentioned, it's up to you the user to fix this conflict.  Determining a simple print statement doesn't have any effect on how this code behaves--it's just a message the user sees, so this merge conflict is just about what wording best suits the error message (again, this post is about how to fix stuff like this, so it's helpful when the code itself is simple).  And this is a real repo, with real code, which you'll find linked throughout this article, which also helps so you can see real-life examples.

Whatever method you decide to use (vim, fancy text editor, Github, etc.) you need to make sure to remove all three markers:

  • <<<<<<<< HEAD
  • ========
  • >>>>>>>>

and save your change before adding your file.  

I tend to use the editor at this point because I like how the fancy colors help distinguish what I need to be looking at.  In this instance, I have decided that I liked parts of both pieces of code, so I make a completely new sentence.  

I could have just easily chosen to use their code, or my existing code (either by clicking "Use this" or editing it in vim and deleting everything except my code.

I have saved my file with the change above.  I then enter

git add tccutil.py
git rebase --continue

If there were other files to address, it would move on to the next, but since we just had the one, the rebase process completes.

Now the interesting part is that you have a brand new commit.  You would have a brand commit even if you had left chosen to keep your existing code (i.e. not modifying the message like I did).  This is because you have an entirely new set of commits then what you previously had.  So no matter what, you'll always end up with a new commit.  So even though I used the same code, it still shows up as a new commit (f680727).

This becomes problematic when it comes time to push your code.

If you attempt a regular push, it will reject it (with a deceptive message).  Despite it saying you need to do a git pull, don't.  That will complicate your life considerably.  Instead, you need to do a git push --force, or git push --force-with-lease.  The latter is less likely to cause problems if you are working with other people and other teams.  But in many cases, you'll be on your own branch and you just need to update it before making a PR.  So pushing with --force is fine.  

Not only fine, force pushing is  the only way push your code after a rebase.

This part can be quite scary and turns a lot of people off to the rebase command.  And with good reason: you are essentially re-writing the history of commits.  If you mess up and change or delete some commits and then force push it, those previous commits are just gone (unless you had a backup or someone else's local branch still had them).  

But when you successfully rebase onto a new branch, you can easily test your commit(s) on new code that someone else is pushing.  This is often useful if you are working on a feature, new code is added to development, and you want to keep up-to-date with it.

There are a million and one different strategies for working with git and one workflow doesn't fit all.  This isn't the only way to use rebase, but hopefully you have learned a few tips to help you use it in a way that works for you.

Feel free to leave comments or questions.