0

I've recently merged a branch into the master branch. There were some conflicts but I dealt with them and now both VS Code and GitHub Desktop are showing the branch as being fully merged. However, not all of the changes I've made in the branch are reflected in the master branch.

This SO post recommends doing a rebase however I get a message saying Current branch master is up to date.

Any ideas on where I can go from here?

Jossy
  • 589
  • 2
  • 12
  • 36
  • 1
    Are you saying that these "missing changes" are missing at GitHub itself? Or are they missing just on your computer? Or are they missing in both places? If so, are these "changes" that you may have nullified when you resolved the conflicts? – matt Oct 01 '20 at 01:56
  • 1
    Well, git merge doesn't lose changes on its own. How did you "deal with" the conflicts? – jingx Oct 01 '20 at 04:43

1 Answers1

0

There's no such thing as a partial merge, although one could use that phrase to describe an in-progress or incomplete merge. But in Git, merging actually takes place in Git's index. Until you (or Git) commit the merge, it's just in-progress. When you (or Git) make the merge commit, you have a completed merge: a merge commit, which is a commit with two or more parents. Every commit holds a snapshot of all the files that Git knew about at the time whoever made the commit, made it; and a merge commit is no different. The git commit command builds the new commit from the files that are in Git's index. So once the merge is all set up in Git's index, you or Git run git commit, and Git builds the merge commit from the files that are in Git's index. Those are what is in the merge commit, as its snapshot.

However, not all of the changes I've made in the branch are reflected in the master branch.

That is, if you compare an earlier commit in master to the snapshot in the merge commit, you don't see what you wanted to see. That's certainly possible: the merge commit will contain the files that were in Git's index when you made the merge commit, and if those files weren't the files that you wanted it to contain, the merge commit's snapshot won't be what you wanted it to be.

The question of how that came about is important and useful (to avoid repeating the problem later) but, despite the comments, that's not what you asked:

Any ideas on where I can go from here?

You have two options:

  1. Remove the merge. This is possible, but not always easy. It's not very difficult if you haven't sent the merge commit on to any other Git repository. If you have, that's when it gets hard.

  2. Add new commits. This is easy! But, coming up with what you want to have in those commits may not be so easy.

To remove the merge, you can use git rebase; rebasing normally drops merge commits, and you can use this to good effect here. The question to which you linked is quite old—it dates back to when Git version 1.7.4 was new, in early 2011—but those methods are still supported. But this brings us back to the general problem of removing a commit that you've given out to other people: you have to convince all of them to remove that commit too.

To correct the problem and add a new commit, you need to come up with the new commit's content. The easiest way to do that is probably just to re-perform the merge, but correctly this time.

You can re-perform the merge by creating a new branch name at the commit where you want the new merge to go. This requires a bit of understanding of how Git's commit graph works, but suppose you currently have this:

...---J---M--N--O   <-- master
         /
...--K--L   <-- feature

that's the result of merging feature into master, producing merge commit M, and then having several more commits added to master (N and O) since then, building on the incorrect merge. Let's draw the above in a different way, that represents the same graph:

...-----J
         \
          M--N--O   <-- master
         /
...--K--L   <-- feature

We just need to find, now, the hash ID of commit J, and create a new branch name pointing to that commit. Let's use the name redo. We will run:

git checkout -b redo <hash-of-J>

which gives us this drawing:

...-----J   <-- redo (HEAD)
         \
          M--N--O   <-- master
         /
...--K--L   <-- feature

We can now run git merge feature, or, if we no longer have the name feature to locate commit L, we can find its hash ID and run git merge hash.

Git will grind away for a bit and produce the same merge conflicts as last time. This time, as you resolve the merge conflicts and supply a set of files for Git to put into the new commit, be careful to produce the files that you really do want. This usually includes running tests on them,1 examining Git's own merge results, and so on. Try not to rely on the ours or theirs flags: git checkout --ours means thow away their changes, for instance, and even -X ours or -X theirs at the git merge command isn't necessarily right.

Once you do have the correct merge, you can run git merge --continue or git commit2 to make the new merge commit:

...-----J-----M2   <-- redo (HEAD)
         \   /
          M-/--N--O   <-- master
         /_/
...--K--L   <-- feature

You can at this point run git diff hash-of-M HEAD, for instance, to see what the new merge looks like. Or, git diff hash-of-M redo produces the same diff, because the name redo refers to the same commit (M2) as the special name HEAD.

At this point you have multiple options, depending on whether commits N and O actually exist. One option that works regardless of whether they exist is to take the diff generated by git diff hash-of-M HEAD here and actually apply that diff on branch master. Of course, git checkout master changes the commit that HEAD refers to, so we might do this as:

git checkout master
git diff <hash-of-M> redo | git apply -3

(note that the -3 option lets git apply use a three-way merge if needed, so this can result in merge conflicts, if N and O conflict with the changes you'll bring in via the updated M2 merge).

Assuming all of this works, you're now ready to git add and git commit the result, producing:

...-----J-----M2   <-- redo
         \   /
          M-/--N--O--P   <-- master (HEAD)
         /_/
...--K--L   <-- feature

which holds commit P as a fix for the incorrect merge.

Note that you can, instead of the above diff ... | apply -3, just merge commit M2 (via git checkout master; git merge redo). This merge is unfortunately quite tricky internally for Git, as the merge base of O and M2 is both commits J and L. The recursive strategy, which is the default, will merge those two commits to produce a virtual merge base, and that particular merge always produces the conflicts that you've already seen twice at this point. The inner recursive merge just commits the conflicts, which guarantees further conflicts with M2. This tempts one to use git merge -s ours but that's quite wrong, or to find an equivalent of git merge -s theirs, but that's wrong too as it undoes commits N and O.

You could use git merge -s resolve redo from master. This will pick one of J or L to use as the merge base. This is still likely to produce some merge conflicts, though. That's why I suggested the git diff ... | git apply -3 approach: it's likely to have the fewest merge conflicts. Unfortunately, it leaves future users of the repository a bit of a mystery, if you delete the branch name redo at this point, because they won't have a record of the re-merge.

There is yet another workaround for this, using git commit-tree instead of git commit after doing the git apply and git add. I'll leave this option to someone else for now, though.


1This can be difficult when the only way to run tests is with some distant CI system! This is one reason I dislike CI systems that make it impossible to test locally.

2Running git merge --continue just makes sure that you will finish a suspended merge, and then runs git commit, so these will do the same thing at this point.

torek
  • 448,244
  • 59
  • 642
  • 775
  • Hey. Thank you so much for this. I've got as far as creating the ```redo``` branch - which was a beautiful fresh copy of ```master```. I then ```git merge feature```, well actually ```git merge hash``` - no conflicts at all. Where I've hit issues is at ```git diff redo | git apply -3``` - that seems to want to do the 'partial' commit that was the problem in the first place :( – Jossy Oct 01 '20 at 11:23
  • As this isn't a big complex commercial project I've hit the nuclear option and simply manually copy and pasted the contents of the four files to ```master``` – Jossy Oct 01 '20 at 11:36