# Reoccurring conflicts after git squash merge

In many projects, branches are being merged with a squash option. This can be helpful to keep the commit history clean. But we need to be mindful when merging a branch that is a parent to some other branch.

- URL: https://sudolabs.com/insights/reoccurring-conflicts-after-git-squash-merge
- Published: 2022-08-02
- Updated: 2024-01-16
- Author: Tomáš Ondrej, Software Engineer
- Categories: Tech
- Language: English

## Example

Let’s take the following git repository structure

![](https://a.storyblok.com/f/117609/407x466/1c2166ab97/repo_structure.png)

There are 3 branches: `main` (blue), `B1` (green), and `B2` (orange). `B2` has been created from `B1`. Letters `A`, …, `F` represent each commit. Rectangles next to the branch tips are visualizing a file state at that specific commit.

Now, the work on branch `B1` is done and we want to squash merge it with the `main` branch.

```
$ git checkout main
$ git merge --squash b1
Auto-merging f1
CONFLICT (content): Merge conflict in f1.txt
Squash commit -- not updating HEAD
Automatic merge failed; fix conflicts and then commit the result.
```

Git could not merge the branch automatically because there’s a conflict in file `f1.txt` which was modified both in `main` and `B1`. We resolve the conflict and finish the merge. After that we get the following structure:

![](https://a.storyblok.com/f/117609/407x481/ec8b344bc7/repo_structure_after_merge.png)

There are 2 branches left - `main` and `B2`. Now the work in `B2` is finished and we want to merge it into the `main` branch so we follow the same procedure:

```
$ git checkout main
$ git merge --squash b2
Auto-merging f1
CONFLICT (content): Merge conflict in f1.txt
Automatic merge failed; fix conflicts and then commit the result.
```

The auto-merge could not happen because of the conflict in the file `f1.txt`. Why? The only change we introduced in branch `B2` was creating a new file `f2.txt`. Why do we have to resolve the same conflict that has already been resolved when merging `B1` into `main`? To find out why let’s take a look at how git merge works.

There are 2 different types of merges: **three-way** and **fast forward**. In our example, git will use three-way merge because there isn’t a linear path to the target branch.

_Linear vs non-linear path from B1 to main_

![](https://a.storyblok.com/f/117609/562x322/529f33e8bb/linear_vs_nonlinear_paths.png)

**Linear path:** there’s no commit after `B1` has been created, so we can just fast-forward the main tip to point to the commit B1.

**Non-linear path:** there’s a commit after `B1` has been created, so git will use a three-way merge.

Now that we know what merge strategy has been used, let’s take a closer look at the three-way merge.

## Three-way merge

When we try to merge branch `B1` into `main`, git will backtrack both branches to find a **shared commit** that exists in both of them. In our case, it’s commit `A`. Such commit is called a **merge-base**.

The merge-base can be found with `git merge-base <commit> <commit>` command.

```
$ git log --graph --oneline --all # print commit graph

* ccfe592 (b1) C
* 0e14aa5 B
| * 17866d5 (HEAD -> main) D
|/
* c4049e1 A

$ git merge-base main b1 # We can also use branch names because a branch name is a reference to a commit
c4049e14c05ba0a88c13ef062eab18280efb1d02
```

![](https://a.storyblok.com/f/117609/222x282/53283132e1/simple_repo_structure.png)

Once the merge-base is found (commit `A`), git needs to find what has been changed on `main` and `B1` separately. It starts by extracting the merge-base commit itself to a temporary area. Now that Git has the merge-base extracted, `git diff` will find what has changed on the `main` branch between snapshot `D` and `A`. That’s the first change-set.

Git now has to run a second `git diff` to find what has changed on `B1`: the difference between snapshot `C` and `A`.

Then, it combines these two sets of changes and applies them to what’s in snapshot `A` (the merge-base), and makes a new commit from the result. After that, the merge is completed.

## A new merge-base prevents recurring conflicts

Now that we know how git finds changes in a three-way merge, let’s compare merge-base after a **squash merge** and after a **merge commit**. We will print a commit graph for each merge type.

```
# commit graph after squash                  # commit graph after merging b1
# merging b1                                 # using a merge commit

                                             *   c713bc7 (HEAD -> main) G
                                             |\
* 5673bd4 (HEAD -> main) G                   * | 17866d5 D
* 17866d5 D                                  | | * 1e9afd8 (b2) F
| * 1e9afd8 (b2) F                           | | * 0960141 E
| * 0960141 E                                | |/
| * ccfe592 (b1) C                           | * ccfe592 (b1) C  <-- merge-base
| * 0e14aa5 B                                | * 0e14aa5 B
|/                                           |/
* c4049e1 A   <-- merge-base                 * c4049e1 A

$ git merge-base main b2                     $ git merge-base main b2
c4049e14c05ba0a88c13ef062eab18280...         ccfe59230f2e5c275ab9c095b1b77d7e...
```

We see that the merge-base has not been changed after the squash merge. When doing a squash merge, we take all the commits of `B1` and squash them to a new commit (`G`) which will be applied to the `main` branch. This commit has no common history with the branch `B2`. So during the merge of `B2`, the merge-base will be the same as for `B1` - commit `A`.

If we execute `git diff` between commit `A` and commit `F`, we see changes to file `f1.txt`. If we execute `git diff` for the commits from the main branch, namely `A` and `G`, there are also changes to file `f1.txt`, which means there is a conflict.

That’s the reason why we had to resolve the conflict for the file which hasn’t even been touched in the branch. Because there wasn’t a new merge base that would exclude the already merged changes from the merge execution.

On the other hand, an ordinary merge will preserve the commit history via a **merge commit**. It means that the history of the branch `B1` is now part of `main`. When we try to merge `B2`, the merge-base will be commit `C` because it is part of both branches. If we compare commit `C` and `F`, the only change is a new file `f2.txt`

```
$ git diff ccfe592 1e9afd8   # Find diff between commits C and F
diff --git a/f2 b/f2
new file mode 100644         # The only change is a new file f2.txt
index 0000000..4bf0f1f
--- /dev/null
+++ b/f2.txt
@@ -0,0 +1,2 @@
+E
+F
```

## Summary

Git merges in two ways: fast-forward and three-way. Git uses the three-way merge if a commit graph is not linear. The three-way merge uses a merge base (a common ancestor) to calculate changes for each of the 2 branches we want to merge together. If we use `git merge —squash`, `git rebase` or `git cherry-pick`, the commit history of a source branch will not be kept.
