12/08/2018, 13:38

Why Git rebase

Lots of things have changed and more easier over the years. Nowadays, the easy way to fix this set of things is with the Pull Request workflow, which is essentially the Integration Manager workflow Use github or bitbucket that makes the Pull Request workflow easy Delegate a person as ...

Lots of things have changed and more easier over the years. Nowadays, the easy way to fix this set of things is with the Pull Request workflow, which is essentially the Integration Manager workflow

  • Use github or bitbucket that makes the Pull Request workflow easy
  • Delegate a person as integration manager, who will pull or comment on the pull request
  • Require contributors to rebase their own pull request branch before pulling if there are conflicts.

Merge Workflow

The merge flow consists of

git commit -m "something"
git pull   # this does a merge from origin and may add a merge commit
git push   # Push back both my commit and the (possible) merge commit

Note that you normally are forced to do the pull unless you're the only committer and you committed the last commit.

When avoid merge workflow?

As we saw in Avoiding Git Disasters, the multiple-committer merge workflow has very specific perils due to the fact that every committer for a time has responsibility for what the other committers have committed.

These are the problems with the merge workflow:

  • It has the potential for disaster, as that merge and merge commit have to be handled correctly by every committer. That said, most committers will have no trouble with it and will not mess it up. But if you have lots of committers, and they don't all understand Git, or they are using a GUI that hides the actual results from them, watch out.
  • Your history becomes a mess. It has all kinds of inexplicable merge commits (which you typically don't look inside to see what's there) and the history (gitk) becomes useless.
  • Debugging using git bisect is confused massively due to the merge commits.

When Is the Merge Workflow ok?

  • The merge workflow will do you no damage at all if you
  • You don't care much about reading your history.

What is Rebasing?

  • A branch is a separate line of work. You can expose branches in the public repository (a public branch) or they may never get off of your machine (a topical branch).
  • A public branch is one that more than one person pulls from.
  • A topical branch (or feature branch) is a private branch that you alone are using, and will not exposed in the public repository
  • A tracking branch is a local branch that knows where its remote is, and that can push to and pull from that remote.

Imagine you are working on that radical new feature. It’s going to be brilliant but it takes a while. You’ve been working on that for a couple of days now, maybe weeks.

Your feature branch is already six commits ahead of master. You’ve been a good developer and crafted meaningful semantical commits. But there’s the thing: you are slowly realizing that this beast will still take some more time before it’s really ready to be merged back into master.

m1-m2-m3-m4 (master)
     
      f1-f2-f3-f4-f5-f6(feature)

What you also realize is that some parts are actually less coupled to the new feature. They could land in master earlier. Unfortunately the part that you want to port back into master earlier is in a commit somewhere in the middle of your six commits. Even worse, it also contains a change that relies on a previous commits of your feature branch. One could argue that you should have made that two commits in the first place, but then nobody is perfect.

m1-m2-m3-m4 (master)
         
          f1-f2-f3-f4-f5-f6(feature)
                 ^
                 |
            mixed commit

At the time that you crafted the commit, you didn’t foresee that you might come into a situation where you want to gradually bring the feature into master. Heck! You wouldn’t have guessed that this whole thing could take us so long.

What you need is a way to go back in history, open up the commit and split it into two commits so that you can seperate out all the things that are safe to be ported back into master by now.

Speaking in terms of a graph, we want to have it like this.

m1-m2-m3-m4 (master)
         
          f1-f2-f3a-f3b-f4-f5-f6(feature)

With the work splitted into two commits, we could just cherry-pick the precious bits into master.

Turns out, git comes with a powerful command git rebase -i which lets us do exactly that. It lets us change the history. Changing the history can be problematic and as a rule of thumb should be avoided as soon as the history is shared with others. In our case though, we are just changing history of our local feature branch.

Ok, let’s take a closer look at what exactly happened in commit f3. Turns out we modified two files: userService.js and wishlistService.js. Let’s say that the changes to userService.js could go straight back into master wheras the changes to wishlistService.js could not. Because wishlistService.js does not even exist in master. It was introduced in commit f1.

Thi repo will use for this exercise to make it easier to follow, each commit message is prefixed with the pseudo SHAs used in the graphs above. What follows is the branch graph as printed by git before we start to split the commit f3.

Now the first thing we want to do is to checkout our feature branch with git checkout feature. To get started with the rebase we run git rebase -i master.

Now what follows is that git opens a temporary file in the configured editor.

alt

This file is meant to provide you some options for the rebase and it comes with a little cheat sheet (the blue text). For each commit we could choose between the actions pick, reword, edit, squash, fixup and exec. Each action can also be referred to by its short form p, r, e, s, f and e. It’s out of the scope of this article to describe each and every option so let’s focus on our specific task. We want to choose the edit option for our f3 commit hence we change the contents to look like that.

alt

Now we save the file (in Vim <ESC> followed by :wq, followed by <RETURN>). The next thing we notice is that git stops the rebase at the commit for which we choose the edit option.

alt

What this means is that git started to apply f1, f2 and f3 as if it was a regular rebase but then stopped after applying f3. In fact, we can prove that if we just look at the log at the point where we stopped.

alt

To split our commit f3 into two commits, all we have to do at this point is to reset gits pointer to the previous commit (f2) while keeping the working directory the same as it is right now. This is exactly what the mixed mode of git reset does. Since mixed is the default mode of git reset we can just write git reset head~1. Let’s do that and also run git status right after it to see what happend.

alt

The git status tells us that both our userService.js and our wishlistService.js are modified. If we run git diff we can see that those are exactly the changes of our f3 commit.

alt

If we look at the log again at this point we see that the f3 is gone though.

alt

We are now at the point that we have the changes of our previous f3 commit ready to be commited wheras the original f3 commit itself is gone. Keep in mind though that we are still in the middle of a rebase. Our f4, f5 and f6 commits are not lost, they’ll be back in a moment.

Let’s make two new commits: Let’s start with the commit for the changes made to the userService.js which are fine to get picked into master. Run git add userService.js followed by git commit -m "f3a: add updateUser method".

Great! Let’s create another commit for the changes made to wishlistService.js. Run git add wishlistService.js followed by git commit -m "f3b: add addItems method".

alt

This is exactly what we wanted except our commits f4, f5 and f6 are still missing. This is because we are still in the middle of the interactive rebase and we need to tell git to continue with the rebase. This is done with the command git rebase --continue.

Let’s check out the log again.

alt

And that’s it. We now have the history we wanted. The previous f3 commit is now splitted into two commits f3a and f3b. The only thing left to do is to cherry-pick the f3a commit over to the master branch.

To finish the last step we first switch to the master branch. We do this with git checkout master. Now we can pick the f3a commit with the cherry-pick command. We can refer to the commit by its SHA key which is bd47ee1 in this case.

alt

We now have the f3a commit sitting on top of latest master. Exactly what we wanted!

alt

Conclusion

The fundamental idea of rebasing is that you make sure that your commits go on top of the "public" branch, that you "rebase" them so that instead of being related to some commit way back when you started working on this feature, they get reworked a little so they go on top of what's there now.

Resources

  • A Rebase-based Workflow
  • Pro Git book: Maintaining a Project
  • The Case for Git Rebase
  • A Git Workflow for Agile teams
0