git merge --squash contains extra messages

Viewed 337

When running git merge --squash the commit message contains the commit messages of all the commits I'm squashing, great. However, for some reason it actually contains far more than just the commits I'm squashing.

Here is my workflow:

  1. Various feature branches merge, normally, into development.
  2. When a version is ready to go, I do a git merge --squash from development into staging.
  3. I do a second commit, in staging, with the version number, and then do a normal merge from staging back into development. (I occasionally also do hot fixes directly on staging for urgent one-commit bugs, and do a normal merge from staging to development)

The problem is step 2. Every time I try to do a squash commit, the commit message includes all the commits going back at least half a year (from when I branched development off master), despite my having already merged them in previous squash commits. I manually prune off all but the relevant ones but at least twice I've forgotten to do this and ended up with an absurdly unwieldy commit message.

What I've seen from the documentation seems to imply that git merge --squash is not supposed to do this and is supposed to intelligently guess which commits I'm squashing. I haven't been able to find an explanation of how it does this so I could figure out what's gone wrong/how to fix it.

How do I fix this? If it can't be fixed directly, is there an alternate workflow I could use that would give me one branch with a full history and one branch with consolidated commits that have references back to where they came from?

Adding further detail:

we'll call common parent 0
feature branch one: 0-A - B - (merge to development, delete branch)
feature branch two: (development from point D) - E- F (merge to development, delete branch)


development: 0-A - B ------------------------------D- E - F-------------
                  |(source, NOT parent)           /(merge from staging) \                            
staging:     0 --- 1st squash merge(C) - D(version tag) -  2nd squash merge(G) - H (version tag)

That second squash merge has commits A, B, C, D, E, F instead of just E, F recorded in it

1 Answers

Original answer (see update below):

git merge --squash does the same history analysis that a regular git merge without --squash does. To do that, it needs to know the history. But the --squash merges that you did in the past erased important parts of that history.

I fact, you are very lucky if an unwieldy commit message is your only problem. I would expect a lot of merge conflicts in areas that were touched by different feature branches that you have squash-merged.

Generally, do not use git merge --squash unless you know the consequences. It's primary purpose (IMO) is really only to reduce a sequence of messy commits to a single commit. It is totally unusable (again IMO) if you are using feature branches anyway. Just keep your feature branches clean and use regular git merge.


Update:

I can reproduce your observation with this history:

0--A--B--M--E--F  <-- development
 \      /
  C----D--G--H    <-- staging

C is a squash-merge of commit B, i.e. the changes between 0 and B, and G is a squash-merge of commit F, i.e. the changes between D and F.

Let D be the HEAD commit. At this point, when you git merge F (with or without --squash), Git determines a merge base, which is D.

Now, without --squash, Git would find that this is a fast-forward situation and advance the branch to F.

But since you requested --squash, it stops before making a commit, but after it has updated the worktree. It also lays down a commit message consisting of the output of git log F ^HEAD, that is of all commits that are reachable from F, but excluding commits reachable from HEAD. These are A, B, M, E, and F, as you can see in the graph; and that explains why you see so many "unnecessary" commits in your log message.

A possible fix is to generate the squash-merge G' while you are on a temporary branch at M. Then you cherry-pick G' to staging:

           G'     <-- temporary
          /
0--A--B--M--E--F  <-- development
 \      /
  C----D--G--H    <-- staging

This should work for you because the trees of D and M should be identical.

Related