Version Control – How to Merge Bug Fixes from Trunk in Old Branches

branchinggitsvn

We are now in the process of switching from svn to git (after a year spent on convincing people, yay!) in my company.

So far, this is all for the better but there is one little thing that we currently have in our workflow that I can't find a good equivalent too.

Currently, all our developers work in master. Once every quarter, we branch master into X.x branch, which will later on become our latest release. This means that our svn repo looks like this:

  • trunk
  • branches
    • 3.8
    • 4.1
    • 4.2

We don't really use tags.

Once in a while, there is an urgent bug fix that is discovered.

The current way for us to do it is:

  • Fix it in master
  • SVN merge the relevant range of commits back into the relevant branches (Maybe our latest release, maybe more).

We are in the space industry, so our branches are long lived and the customer upgrade process is rather long, which is why we have worked like that so far.

Now, how would be a good equivalent in git?

So far, I have tried to fix the bug in a branch spawned from master, and then merge this branch back in master and in 4.3 (for example).
Thing is, the hotfix branch contains the master history, and all the commits between master and 4.3 get merged as well, which we don't want to.

Things I could think of so far:

  • I have looked at the very successful Git Workflow method, and their solution is to fix the bug in the release branch and merge it back instead of the other way around. This could work in our case, but would be rather cumbersome because it would require us to already know the oldest branch we want to fix the bug in before actually fixing the bug.
  • Another solution is to fix in master, and then cherry pick the commits (as we do for the svn merge today). The annoying thing here is that in this case we lose history of what has been merged back where, since cherry picks look like new commits and we lose the relationship.

So what is the "good" way to do it? Should we fix commits in history, or cherry pick and manually keep track of what has been merged, or even something else?

If have little production experience with git, so I am sure I may have missed something.

Best Answer

The Git Flow assumes you only have a single supported release, with the master branch always pointing to the latest release. Since you support multiple releases simultaneously, you cannot copy that workflow 1:1. Nvie's Git Flow is a very good example of a branching strategy, but you must adapt it to your needs. Most importantly, you will have multiple active release branches.

When you identify a bug, you will have to do some testing to determine all affected versions. It is not sufficient to write the fix first, then merge it back into the release branches as a hotfix. Usually, you'll end up with some continuous range of affected versions. Very old versions might not contain the bug, newer versions might have gotten that bug fixed accidentally. You will need to verify the bug on each version so that you can verify it is actually gone after the fix. If you can express the bug as an automated testcase, it's pretty straightforward to find the problematic commit via git bisect, or to run the test for each release:

for release in 3.8 4.1 4.2
do
  git checkout $release
  if ./testcase >release-$release.log
  then echo "$release ok"
  else echo "$release AFFECTED"
  fi
done

Now, you used to write the fix on trunk/master. This is problematic because the buggy part may have changed between versions, so a patch will not usually apply to an older version. In particular, code in master might depend on any features available in master, which might not have been present in older versions. It therefore makes a lot of sense that a Git commit references its whole history, not just the change set. When merging back, it will pull in all the history it depends on.

Using cherry-pick or rebase ignores this history, and records a new commit with the same changeset, but a different history. As pointed out, this will not work if your codebase has diverged.

The “correct” solution is to write the fix as a hotfix on the oldest affected release. Then, you merge oldest release into the second-oldest release. Usually, a newer release would contain all commits of an older release, so this is OK. If things have changed, you now have a chance to manually resolve the merge conflict. Then you continue merging each release into the next-younger release until you are done. This maintains proper history and avoids a lot of unnecessary work, while only requiring effort that has to be expended anyway. In particular, this incremental merging gets you closer to the current development state in small steps.

As a diagram:

| o normal commit |
| x hotfix        |
| ⇘ merging       |

3.8 --o-----------------x
       \                 ⇘
4.1     o--o--o-----------x'
               \           ⇘
4.2             o--o--o-----x''
                       \     ⇘
develop                 o--o--x'''--o--o