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?

3 Answers 3

24

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
  • 5
    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
14

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
8

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.

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