Saturday, October 13, 2012

Git Rebase inside IntelliJ IDEA

Rebase is one of the most awesome commands in git. Use it to keep the local branch up to date with what's going on in trunk. A rebase a day keeps merge conflicts away!

In the form advocated here, rebase takes your branch's local changes and applies them to the most recent code in the master branch. This makes git look as if you made your changes to the latest code, instead of to older code. Everyone else's changes are incorporated into yours without an explicit merge commit.

Frequent rebasing reduces merge pain by catching conflicts early and in small quantities. Nothing can eliminate merge pain entirely. When conflicts do come up during rebase, git doesn't make it clear what is going on. To help with this, I turn to my favorite IDE, IntelliJ IDEA. Unfortunately IntelliJ doesn't make it clear either.

This post steps through rebasing a working branch to bring in the latest changes from master, inside IntelliJ. The merge-conflicts support in IntelliJ is good, but beware! It is not quite what you expect, so watch out, and read on.

Before starting the rebase:
1. Commit all your changes to the local branch. If you're in the middle of something, commit it anyway. You can tweak that commit history later.
2. Bring the central repo changes into your master branch. (Most people use "git pull.")
3. Check out your local branch.

In IntelliJ, look in the Changes view, at the Log tab, to see the status of your repository. If it looks like this, with commits on both master and your current local branch, then this post is for you:

To start the rebase within IntelliJ, go to the VCS menu; select Git and then Rebase...
In the dialog that appears, uncheck "Interactive." The interactive rebase serves a different purpose. In the "Onto" field, enter the master branch, the branch with other people's changes. Click Rebase.

If there are no conflicts, well, lucky you, you're done. It gets interesting when the "Rebase Suspended" box appears.

Conflicts! Time to resolve them. Note that "(0/0)" here has no meaning. I've seen that piece work in an interactive rebase, but not this one.

Once you click OK, nothing happens. This is confusing. Your git repo is in an intermediate, mid-rebase state, but you can't tell. The secret here is to go to the Changes view, and look at the Local tab. Now we see the files in conflict.

Caution: don't walk away
Don't leave your repo in the middle of a rebase. You'll be confused as heck when you return to it later and aren't on any branch. Either finish the rebase or admit defeat with VCS -> Git -> Abort Rebase.

To resolve the conflict, right-click on the file, choose Git, and then Resolve Conflicts. (Anybody know if there's a handy button for this?)

Now watch out! The "Files Merged with Conflicts" dialog looks familiar from my subversion days, but the column headings are deceptive. "Yours" and "Theirs" don't mean what you think they do. Click on Merge to look closer.

The Merge window appears. Look closely at the "Local Changes" side and the "Changes from Server" side - they're the opposite of what you expect. The Local Changes were made on the master branch, and the Changes from Server were made in your local branch. This is the opposite of what you would expect in a merge, even a git merge. The rebase has the changes backwards.

Technical details
This happens because of the way rebase works in git. Rebase starts from the tip of the "onto" branch, master. Then it applies the localFeatureBranch commits there - so in git's mind, we're starting from master ("Local Changes") and bringing in the localFeatureBranch commits ("Changes from Server"). In git terms, the master tip is ORIG_HEAD and the local branch is MERGE_HEAD. It would help if the column headings in IntelliJ were customized to reflect this.

Now that you know which changes are which, merge them together and then save. You might see this "File Cache Conflict" dialog pop up. The changes IntelliJ is making through git and the changes it's making internally are not quite synchronized. You must choose "Keep Memory Changes" in order to retain your merge!

Repeat this process with any other files in conflict. IntelliJ's conflict resolution automatically adds the file to the git staging area, which is necessary to continue the rebase. Once all conflicting files are merged and in the staging area, continue the rebase. VCS -> Git -> Continue Rebasing.

If that's the last conflict, then nothing happens. You don't get any sort of congratulations, you're just back to work. Flip to the Log tab in the Changes dialog, and see the nice straight line that shows your localFeatureBranch growing out of master. All changes are now incorporated. Good job!

If there was more than one commit on your local branch, then there's a good chance you'll find more conflicts. The "Rebase Suspended" dialog pops again, and you get to resolve more conflicts and Continue Rebasing again.
If at any point you decide this rebase is a disaster, choose VCS -> Git -> Abort Rebasing. 

That's it for rebasing inside of IntelliJ, as of v11 of the IDE and the bundled Git Integration. If I had infinite time available, I'd love to write another plugin just for rebase, because it's a very important and very cryptic operation in Git.

No comments:

Post a Comment