Pull Request Merge Strategies: The Great Debate

December 10th 2014 Nicola Paolucci in Git

When a piece of work is complete, tested and ready to be merged back into your main line of development, your team has some policy choices to make. What are your merge strategy options? In this article I'll explain the possibilities and then provide some notes on how we do it at Atlassian. Hopefully at the end you'll have the tools to decide what works best for your team.

What are some possible merge strategies? There is a plethora of choices:

  • Explicit, non fast-forward merge
  • Implicit via rebase or fast-forward merge
  • Squash on merge

I'll use master as the mainline branch in this article, but you can replace it with develop, staging, next, etc. Go nuts.

Oh: and stay till the end because I have some goodies (scrolling down right now is not allowed!).

Explicit merges (aka non fast-forward)

This option is the least surprising and most straight-forward. And sometimes default and straight-forward is the way to go. When moving complete feature branches to master using explicit merges, git creates a merge commit which records the event.

In the resulting commit git stores the two parents involved in the merge. That commit will unify the changes between the two branches using a recursive 3-way merge (unless you specify a different merge strategy).

What is a merge?

Technically, a merge commit is a regular commit which just happens to have two parent commits. Let me show you: if you select the sha-1 of a merge commit and inspect its contents using cat-file -p, you get:

    $ git cat-file -p 127196

    tree e8a13dd4283eb2635c42079fa77c3480bd153c97
    parent 8bd43d673fcf1239e36ec33cbc8d22806461e757
    parent 38f8f7b1afc81f74ee2c8d93c359b19636b9d6b1
    author Nicola Paolucci <xxxxx@atlassian.com> 1412938811 +0200
    committer Nicola Paolucci <xxxxx@atlassian.com> 1412938811 +0200

    Merge branch 'test-branch'

Some teams avoid explicit merges because that they create clutter in the linear history of the project. But this argument generally stems from an unfamiliarity with branching workflows. For example, the "noise" issue is easily solved by learning one or two tricks like using git log --first-parent and the like.

Implicit merge via rebase or fast-forward merge

Another way to move complete work from a branch to master is to use rebase or a fast-forward merge.

One of the uses of rebase is precisely to replay commits–one by one–on top of a specific branch. Note that this operation rewrites all the ids (sha-1) of those commits.

This happens because when git computes the unique id of a commit it takes into account the parent commit. If the parent commit changes, the sha-1 of the replayed commit changes too.

What is a rebase?

Used this way, one can indeed apply some commits to master without creating a merge commit. This procedure completely loses the context of where those commits come from, unfortunately.

Using a fast-forward merge to move code to master has some similarities to the above. Have a look:

What is a fast-forward merge?

A fast-forward merge can only happen if in master there are no more recent commits than the commits of the feature branch. In this case master's HEAD can easily be moved to the latest commit of the feature branch. And the merge can complete without an explicit merge commit: it literally just fast-forwards the branch label to the new commit.

Differently than rebase, a fast-forward merge will not change the commit ids (sha-1), but it will still lose the context of those commits as part of an earlier feature branch.

Squash on merge, generally without explicit merge

A third way to move changes is to squash all feature branch's commits into a single commit before performing a fast-forward merge or rebase. This keeps the mainline branch history linear and clean. It isolates the entire feature in a single commit. But it loses insight and details on how the feature branch developed throughout. So... trade-offs.

What is squash on merge?

In this scenario you might be compelled to keep the original, unsquashed, feature branch around for historical reasons. If you use explicit merges this need does not arise because the explicit merge commit allows you to reconstruct what was in the feature branch and its entire evolution.

Stash–our enterprise git repository manager–allows teams to choose their merge strategies for pull requests. A pull request is a light-weight code review facilitated by the great paradigm shift to feature based development. Tweaking a simple parameter you can get "squash on merge" in your project, as you can get --ff-only and several others–with --no-ff being the default.

Conclusion: Our policy and why we chose it

What's the merge policy at Atlassian? At Atlassian we lean strongly towards using explicit merges. The reason is very simple: explicit merges provide great traceability and context on the features being merged.

A local history clean-up rebase before sharing a feature branch for review is absolutely encouraged, but this does not change the policy at all. It augments it. For more on this see a piece I wrote a while ago on "merge vs rebase workflows".

I'll stop here for now, I hope you found these explanations useful and if interested in these topics take a second to follow me @durdn and the awesome @atlassiandev team for more git rocking. Oh! and subscribe our RSS feed!  

You might also enjoy our ebook, "Hello World! A new grad's guide to coding as a team" – a collection of essays designed to help new programmers succeed in a team setting. Grab it for yourself, your team, or the new computer science graduate in your life. Even seasoned coders might learn a thing or two.

Read it online now

Click here to download for your Kindle