32

I'm currently working for a company that uses VSTS for managing git code. Microsoft's "recommended" way of merging a branch is to do a "squash merge", meaning that all commits for that branch get squashed into one new commit incorporating all of the changes.

The trouble is, what if I do some changes in one branch for one backlog item, then immediately want to start doing changes in another branch for another backlog item, and those changes depend on the first branch's set of changes?

I can create a branch for that backlog item and base it on the first branch. So far, so good. However, when it comes time to create a pull request for me second branch, the first branch has already been merged into master and because it's been done as a squash merge, git flags up a bunch of conflicts. This is because git doesn't see the original commits that the second branch was based off of, it just sees the one big squash merge and so in order to merge the second branch in to master it tries to replay all the first branch's commits in top of the squash merge, causing lots of conflicts.

So my question is, is there any way to get around this (other than just never basing one feature branch off another, which limits my workflow) or does squash merging just break git's merging algorithm?

4 Answers 4

25

With Git, commits

  • are immutable,
  • and form a directed acyclic graph.

Squashing does not combine commits. Instead, it records a new commit with the changes from multiple other commits. Rebasing is similar, but doesn't combine commits. Recording a new commit with the same changes as an existing commit is called history rewriting. But as existing commits are immutable, this should be understood as “writing an alternative history.”

Merging tries to combine the changes of two commit's histories (branches) starting from a common ancestor commit.

So let's look at your history:

                                 F  feature2
                                /
               1---2---3---4---5    feature1 (old)
              /
-o---o---o---A---o---o---S          master

A is the common ancestor, 1–5 the original feature branch, F the new feature branch, and S the squashed commit that contains the same changes as 1–5. As you can see, the common ancestor of F and S is A. As far as git is concerned, there is no relation between S and 1–5. So merging master with S on one side and feature2 with 1–5 on the other will conflict. Resolving these conflicts is not difficult, but it's unnecessary, tedious work.

Because of these constraints, there are two approaches for dealing with merging/squashing:

  • Either you use history rewriting, in which case you will get multiple commits that represent the same changes. You would then rebase the second feature branch onto the squashed commit:

                                     F  feature2 (old)
                                    /
                   1---2---3---4---5    feature1 (old)
                  /
    -o---o---o---A---o---o---S          master
                              \
                               F'       feature2
    
  • Or you don't use history rewriting, in which case you might get extra merge commits:

                                     F  feature2
                                    /
                   1---2---3---4---5    feature1 (old)
                  /                 \
    -o---o---o---A---o---o-----------M  master
    

    When feature2 and master are merged, the common ancestor will be commit 5.

In both cases will you have some merging effort. This effort doesn't depend very much on which of the above two strategies you choose. But make sure that

  • branches are short-lived, to limit how far they can drift from the master branch, and that
  • you regularly merge master into your feature branch, or rebase the feature branch on master to keep the branches in sync.

When working in a team, it is helpful to coordinate who is currently working on what. This helps to keep the number of features under development small, and can reduce the number of merge conflicts.

5
  • 6
    Your answer doesn't seem to tackle what happens if you first squash-merge feature1 onto master, then want to merge feature2 later. In that case, wouldn't the first approach result in conflicts as git tries to re-apply the feature1 commits on top of the squashed commit, whereas the second would allow git to determine that those commits don't need to be merged?
    – Jez
    Feb 9, 2018 at 17:11
  • 1
    @Jez That's exactly what happens when you squash a PR. I recently had to manually rewrite a PR on an OSS project (by git cloneing the repo and copying my changed files over!) because I branched from a branch and then the maintainer squashed the first branch. At my job they also do squash merges. This means I can't work on a feature b that depends on feature a until feature a is merged. Feb 9, 2018 at 18:35
  • 4
    And is that not a really annoying breakage of something that would otherwise work, as git is designed to? See, I see various organizations like Microsoft and Github actually recommending these squash merges and they seem dumb to me.
    – Jez
    Feb 10, 2018 at 10:47
  • 1
    @Jez In the original scenario, yes you will get conflicts when merging feature2 into master because merging the commits 1–5 will conflict with the same changes in S. The solution is to rebase feature2 (solution 1) or not use squashing/rebasing at all (solution 2).
    – amon
    Feb 10, 2018 at 11:44
  • Whether squash merges are a good fit for you depends on what you want to record in the version control history. If the feature branches have many WIP commits, then squashing puts a single big commit with the complete feature on the master branch. If you prefer to preserve all intermediary commits of the feature branch, then use rebasing or merging.
    – amon
    Feb 10, 2018 at 11:45
16

Squash merging breaks the merging algorithm for any branches that contain any commits that were removed by the squash. Put another way, rebases are viral. If you rebase one branch, you need to rebase any other branches that depended on that branch. If you use rerere, any merge conflicts you solved manually in your local repo won't need to be solved manually again, but that doesn't help with conflicts solved by other people.

That's why our unwritten rule here is it's okay to squash as long as no one else ever depended on your feature branch, which is the case maybe 90% of the time. Otherwise, a straight merge helps everyone avoid issues.

1
  • One way to have both a squashed commit in master history and a feature branch intact it to make a separate squash-only branch. Suppose you have a feature-xyz branch. You can create a feature-xyz-squashed branch starting at the same commit as feature-xyz branch, git cherry-pick the commits from feature-xyz to feature-xyz-squashed, squash them there, and merge feature-xyz-squashed to master. You should not merge feature-xyz then. Sometimes the above makes sense (e.g. you don't want to include commits with a password snuck in), but it's a workaround, hardly a best practice.
    – 9000
    Feb 9, 2018 at 19:52
9

One solution here that hasn't been spelled out explicitly is to use the --onto flag of the rebase feature.

I'll copy the scenario from the other great answer:

                                 F  feature2
                                /
               1---2---3---4---5    feature1 (old)
              /
 -o---o---o---A---o---o---S          master

Okay so this is the scenario after we squash-merged feature1 into master. S is a new commit that contains the combined effect of commits 1 through 5 applied to A.

What you then want to do is:

git checkout feature2

git rebase --onto master feature1

What does this do? It takes those commits that feature2 has that feature1 doesn't have, then creates a new commit that applies those same changes, but to the head of master instead.

That scenario is depicted in that other answer from @amon but they didn't describe how you get there.

Of course that doesn't work if feature1 got deleted in the meantime. In that case, use git cherry-picking to grab commit F.

1

TL;DR: after the squash merge, you should merge main into feature1, then feature1 into feature2, to establish the common ancestors to avoid conflicts.

These squash merge policies exist to make sure the commits of development branches are garbage collected and removed from the repo when the development is finished, if you do a merge commit to main, then the commits of the development branch is retained forever. Your local policy can determine if this is desired or not.

When you merge in Git, it looks at the two commits and goes back in the commit graph until it finds the nearest common ancestor and then diffs both branches against that common ancestor when doing a merge.

If you do a squash merge, the merge commit does not have the source branch as it's parent, so Git can't select the appropriate common ancestor.

Consider the following case, where you developed feature1, and you made a pull request, then you started working on feature2. In the meantime main got new commits and you also did some code-review changes from feature1. So the commit graph looks like this:

              o--o--o feature2
             /
         o--B--o feature1
        /
-o--o--A--o--M main

When you merge feature1 into main, the common ancestor will be commit A. In the diff between A and main there are unrelated developments. In the diff between A and feature1, there is the development of the feature1.

I you squash merge, you just create a new commit on main:

              o--o--o feature2
             /
         o--B--o feature 1
        /
-o--o--A--o--M--F1 main

So when you merge feature2 onto main, Git again finds only A as a common ancestor. Then In the diff between A and main, you have the unrelated developments on main, plus the feature1's changes. In the diff between A and feature2, you have diff of feature1 until B, and the diff of feature2.

Since both diffs contain changes from feature1, you may see conflicts.

To avoid this you need to help Git to find the appropriate common ancestor.

In order to do that first merge main to feature1 before doing the squash merge, to create the merge commit C:

              o--o--o feature2
             /
         B--o--o--C feature 1
        /        /
-o--o--A--o--M--. main

This updates feature1 with changes from main. It's generally a good practice to update your branch before merging and run your tests to see if you new code integrates with the existing codebase well.

Then do the squash merge of feature1 on main, to create the commit D:

              o--o--o feature2
             /
         B--o--o--C feature 1
        /        /
-o--o--A--o--M--.--D main

Then merge main to feature1 again. In this case the common ancestor of commits C and D is M, the diff between M and C and M and D is identical, so this should be an empty merge commit E:

              o--o--o feature2
             /
         B--o--o--C--E feature 1
        /        /  /
-o--o--A--o--M--.--D main

And finally merge feature1 onto feature2. The common ancestor between E and feature2 is B. So the diff from B and E is the changes from main and the changes of feature1. The diff from B and feature2 is the changes that belong to feature2 so the commits are different so no conflict is expected. This creates commit F and now you can delete feature1:

              o--o--o--F feature2
             /        /
         B--o--o--C--E
        /        /  /
-o--o--A--o--M--.--D main

And now feature2 is up-to-date with main. The common ancestor between D and feature2 is D, so the squash merge of feature2 should be straightforward.

Then you can delete feature2 branch too, and the garbage collection should clean up both branches completely then.

Not the answer you're looking for? Browse other questions tagged or ask your own question.