Git: Branch off An Unmerged Branch While Committing Often - Disasters and Salvage

Git: Branch off An Unmerged Branch While Committing Often - Disasters and Salvage

Committing often and pushing often has been advocated as good practice when using Git, which saves your latest work on remote even if your hard drive dies right after and which provides a more fine-grained selection when rolling back.

It is also good practice to make each pull request consist of only one commit, so that it is easier to cherry-pick stable features from master to release branch. So what I do is to squash all commits into one right before I send them out for review.

Sometimes, I would branch off the current branch I was working on but was blocked when the project was building/compiling or my reviewers have yet to get back to me. It works out by having multiple local repos in different directories.

The problem comes in when I later make more commits onto the original branch I just branched off, as illustrated below.

Say we rebase from the master branch at commit C0, and we branch off from Feature A at commit C1 to work on Feature B. Later, our reviewers finally got back to us and we continue work on Feature A to create commits C2 and C5. Now Feature A is ready for the pull request if squashed. The problem shows up when some files are touched by both the branch Feature A and Feature B. If we squash Feature A, submit the pull request from Feature A against master, and finally rebase Feature B from master, crazy merge conflicts arise.

The conflict results from the fact that both Feature A and Feature B touch the same files and that we rewrite history by squashing (interactive rebase) all commits of Feature A, including those between C0 and C1. Now S0 is a new commit different from any of Feature B, and the merge conflicts really is about the conflict between S0 and C0-C1.

Resolving this conflict by hand is a nightmare, because Git applies commits one by one chronologically, which means we have to resolve the conflict again and again until the last commit.

I encountered this problem in my last internship. At first, I tried to resolve all conflicts by hand and it was not pretty and took me an hour to do so. This problem kept showing up and I believe there must be some other way.

I did eventually find a solution.

I first squash all commits on Feature B right until the one it branches off Feature A. Then, squash Feature A until C0. Notice that S2 is now dangling as its parent is not in the source tree anymore.  Then, instead of a merge, cherry-pick S2 to apply onto S1, and do a hard reset to have Feature B point to the new commit.

Now approve the pull request to merge S1 to master. After that, when you checkout Feature B and do a rebase from master, since the exact S1 is already in master, no conflicts arise because of Feature A. (there might still be conflicts of course if your teammate pushed the delta on the same files right before you rebase).

The bane of the problem might just be that the Git way of taking snapshots of a branch, in that it allows the coexistence of multiple snapshots (i.e. commits) on a branch, which only comes in handy on fine-grained rollback but also brings more trouble in a more common setting in my opinion. Google uses customized Piper for version control, in which deltas are submitted in the quantum of a change list of files, which minimizes the problem encountered here.