Robin
Robin

Reputation: 615

Still showing merge conflicts even after I pushed the fixes

I merged my master branch into my uat branch using the git merge --squash master command and I resolved all the conflicts and pushed my changes into the repo. Now, no one has touched the uat branch since I merged but when I run a git merge --squash master in my uat branch, I still see the same conflicts even though I fixed those conflicts and pushed them.

Upvotes: 0

Views: 1144

Answers (1)

torek
torek

Reputation: 489688

This is normal.

The problem is that git merge --squash is what one might call a terminal action on a branch: after doing git merge --squash branch, you should1 delete the branch, git branch -D branch, or at least rename it to a "don't work on this any more" name: git branch -m branch dead-branches/branch for instance.


1"Should" here is a value judgement. There's no technical requirement, and you might have offsetting reasons for keeping the branch: you just need to know what Git is doing and what it means to keep the branch.


Real merges

It's worth looking at what a normal (non-squash) merge does, before considering a squash "merge", to see why I put the word merge in quotes for the --squash case. Git's git merge is about combining work, and the mechanism it uses is to find a common starting point. This is a commit—ideally one single commit, the commit—that is on both branches, closest to the tip of each branch. If we draw them the way I like to, the branch names and commits look like this:

     C--D--E   <-- mainline (HEAD)
    /
A--B   [common starting point]
    \
     F--G--H   <-- branch

where the main line branch that you have checked out—denoted by the word HEAD attached to the branch name mainline—means, in effect, "commit E". Commits A through E are all on that branch, because Git starts at E and works backwards: E's parent is D; D's parent is C; C's parent is B; B's is A. (A is the very first commit ever made so it has no parent: it's a root commit. This is not important here, but worth mentioning since the first commit you ever make in a new repository is one of these root commits, and usually the only one.)

Meanwhile, the other branch branch really means "commit H". Commits A, B, and F through H are all on branch branch.

Note that commit B is on both branches! It's the commit that's "nearest" to the two tip commits, E and H, so it is the merge base.

A true merge starts by finding the merge base, given the tip commits. Git then, in effect, compares the contents saved as the merge-base snapshot—what you'd get if you git checkout <hash-of-B>—to the contents of the current commit E:

git diff --find-renames <hash-of-B> <hash-of-E>   # what we did

It repeats this with the base and your other branch tip:

git diff --find-renames <hash-of-B> <hash-of-H>   # what they did

This is the work that Git should combine. If you changed three files that they didn't change at all, Git should apply your changes to the base. If they changed some files that you didn't change at all, Git should apply their changes to the base. Where you and they both touched the same files, Git should combine those changes, if it can, making sure that if you both made exactly the same change, Git takes only one copy of that change.

Applying the combined changes to the base gives a new snapshot (or has merge conflicts; then Git normally stops and makes you clean up the mess). Assuming all has gone well, Git commits the new snapshot, setting its first parent to your current commit, and its second parent to the other commit:

     C--D--E
    /       \
A--B         I   <-- mainline (HEAD)
    \       /
     F--G--H   <-- branch

Let's redraw this in a less bendy fashion:

A--B--C--D--E--I   <-- mainline (HEAD)
    \         /
     F---G---H   <-- branch

The merge commit I is on only one branch, in this case the main-line branch. Let's check out the side branch branch and do more work on it now:

A--B--C--D--E--I   <-- mainline
    \         /
     F---G---H--J--K   <-- branch (HEAD)

If we git checkout mainline (to attach our HEAD there) and run git merge branch, Git now must find, once again, the best commit that's on both branches. We start at K for the side branch, then look at J, then at H. Meanwhile we start at I for the main-line, then look at both E and H simultaneously. Commit H is in our list of side-branch commits, so it's the one closest to both branches: it's the new merge base.

Git will now combine our changes with their changes. These changes are:

git diff --find-renames H I   # what we did
git diff --find-renames H K   # what they did

Note that Git isn't starting from B this time: the merge base is now H, because of merge commit I.

Assuming all goes well, Git makes a new commit with the combined changes applied to whatever is in H:

A--B--C--D--E--I-----L   <-- mainline (HEAD)
    \         /     /
     F---G---H--J--K   <-- branch

and we are all done.

Squash "merges"

Now let's see what a git merge --squash does, starting with the same thing we had originally:

A--B--C--D--E   <-- mainline (HEAD)
    \
     F---G---H   <-- branch

Git does the same merge-base finding, and the same combining. I like to refer to this as merge as a verb: it's the action of finding a merge base, getting two git diffs, and combining the results.

But the next step, when all has gone well, is different. Instead of making a merge commitmerge as an adjective, modifying commit, meaning a commit with two (or more) parents—Git makes a commit with just one parent. Well, in fact, it stops entirely, as if you ran git merge --no-commit, but when you finish the process by running git commit, that makes an ordinary, non-merge commit I:

A--B--C--D--E--I   <-- mainline (HEAD)
    \
     F---G---H   <-- branch

New commit I has the same effect as combining the three commits F-G-H and applying them to the contents of E: content-wise, it's what you would get if you did a git rebase -i master while on branch and changed the last two pick commands to squash. But it's not a merge commit! It's not a merge-as-an-adjective, even though it did the merge-as-a-verb action.

So, if you now git checkout branch and make more commits and then switch back to the main line, what you have now looks like this:

A--B--C--D--E--I   <-- mainline (HEAD)
    \
     F---G---H--J--K   <-- branch

and if you now ask Git to merge the two branches, Git starts by finding the merge base. The merge base is the first commit that's on both branches, and since I is not a merge commit, that's commit B again.

Note that I has all the changes from F-G-H in its contents (because we made it by merge-as-a-verb-ing), but Git has no idea, so this next merge has to look at B vs H, which means putting in everything from F-G-H again.

If Git was able to resolve all the conflicts on its own last time, it might be able to resolve all the conflicts on its own this time, too. This new merge-as-a-verb, whether or not we use --squash, may be able to do everything automatically. If Git had to stop and get help last time, though, chances are it will have to stop and get help this time too: many of the same conflicts will occur. They may even be worse, depending on what we did when we resolved them to make commit I.

The normal solution is that when we do the git merge --squash and make commit I, we throw away the side branch entirely:

A--B--C--D--E--I   <-- mainline (HEAD)

If we want to make more changes, we make a new side branch:

A--B--C--D--E--I   <-- mainline, branch2 (HEAD)

and then make new commits on it:

A--B--C--D--E--I   <-- mainline
                \
                 J--K   <-- branch2 (HEAD)

Commits F through H are abandoned (and soon-ish, gone for real, especially if we delete the name branch, as this also deletes the reflog for branch; this reduces any remaining commit-protection to just the 30 days for "unreachable" commits in the HEAD reflog). We don't need or want them any more, though: those three old dull snapshots are now represented as commit I's single snapshot instead.

Upvotes: 2

Related Questions