git merge somehow merged "both ways" (A to B and B to A) in one commit

3k Views Asked by At

I don't understand what happened. I merged branch master into my feature branch, which I've been doing regularly lately, and somehow that merge commit now exists in both branches and both branches were in the same state after that commit. I haven't found any info on how this can happen (or that it can actually be done). I've been using git-gui on Windows (but git command line on other machines so I understand basic git commands). Both git-gui and GitLab show in the history graph this commit as merging both branches with each other. Any clue how this happened? I don't what it to happen again, should I get rid of git-gui?

Then, since unfortunately this commit got pushed to the remote, I still want to do a reset --hard, because if I do a revert, then when I go to merge master into feature again in the future, I get everything in the feature branch as a conflict since it all got undone in that revert in master. Any comments or suggestions on how best to proceed?

Edit: I thought I understood what merging meant, but from answers I'm more confused now. If I make a feature branch that has a lot of new stuff, and I want to "import" a few changes that went on in master, but leave master untouched, is that not what this would do?

git checkout feature
git merge master

master A ------------ E <-- last master commit
        \              \  (try to merge into feature)
feature  B -- C -- D -- F <-- feature now contains "E" edits?

But what I got was:

master A ------------ E  F <-- master now contains all of feature stuff (B, C, D).
        \              \ | 
feature  B -- C -- D --- F <-- feature now contains "E" edits.

Edit2: If according to jszakmeister's answer this happened because 2 merge operations were performed, one of which was fast-forward, is there any way to recover information about that fast-forward merge, like who did it and when? If the commit message says "merge master into feature", I guess that means it was done first in that direction, and then someone fast-forward merged feature into master, right?

Thanks!

2

There are 2 best solutions below

5
On BEST ANSWER

There's one way I could see it happening. Let's say you have a feature branch named "feature". You merged "master" into "feature". So the graph looks something like:

---o---o---o        master
            \
-o---o---o---*      feature

If you the checkout "feature" and merge master, you'll likely get a fast-forward merge, since the new commit on "master" is directly ahead of the last commit in "feature". So the graph ends up looking like this:

---o---o---o
            \
-o---o---o---*      master, feature

It's not an extra commit, it's just that "master" and "feature" now point at the same commit. FWIW, my team and I found this behavior confusing, so I've outlined what we do below to help prevent the issue.

Also, I'm not sure what you mean by using "gitk" to do your commits. As far as I'm aware, gitk is only for viewing. Do you mean "git gui"? I do recommend that you take a look at the graph with "gitk" though. I tend use something like the following:

gitk --date-order master feature

That will show a picture with the master and feature branches on there, and helps me to discern the relationship. If I see master and feature branch labels on the same commit, that's a good sign that I accidentally fast-forward merged feature onto my master branch.

If you're seeing something different, then posting a picture would be helpful. But I suspect it's this fast-forward merge on the feature branch that has happened. You can back up the feature branch a step with:

git checkout master
git reset --hard HEAD^

You'll likely have to git push --force origin master. I recommend using this full form because Git's default push behavior was to push all local tracking branches which can result in rewinding the branches. Also, force pushing "master" isn't a good answer when working with others, so you'll need to coordinate with your team, if you are. The simple answer might be to just leave it as-is.

Depending on the situation, there may be more involved than that.

Also, if you want a merge commit, then you can do:

git checkout master
git merge --no-ff feature

This will force a merge commit to be created even though it could be fast-forwarded. My team and I like this so much, that we made it the default. I explain this below.

EDIT: I flipped the sense around to match the question and expanded on some parts.

As an aside, since I used Bazaar with Subversion for a while, I became very sensitive who was the mainline (left-most) parent in the history. Git and it's fast-forward merges made this more difficult, and I found that my team really found it off-putting as well. So we ended up doing a few things.

First, we changed our configuration around so that merge always made a merge commit. You can do this from the command line with:

git config merge.ff false

Or globally, with:

git config --global merge.ff false

The next bit was that we do want fast forward merges when bringing in updates from upstream. So we have a couple of aliases to help:

[alias]
    # Fetch the remote and update the current branch.
    up = !git remote update -p && git merge --ff --ff-only @{u}

    # Fast-forward pull locally
    ff = !sh -c 'git merge --ff --ff-only ${1:-@\\{u\\}}' -

For the most part, the team has upstream branches set to where they like to push and pull from. So a git up would then fetch updates from the server and fast-forward the current branch. This was typically done on "master". git ff could be used to do something similar, but without the fetch step. It also made it easier to fast-forward another branch on top of yours, if you needed such a thing (it has come up on occasion).

Some of this was still more involved than I cared for, so I wrote git ffwd to help us. git ffwd will do the fetching, and then fast-forward all of my local branches that have equivalents on the remote. It also takes care of pruning your remotes when branches have been removed server-side, making it easier to groom the refs and keep them it from getting disorderly. git ffwd will do fast-forward merges only (git up did the same too), and will fail if the branches have diverged, so it ends being a good marker about something interesting happening and you need to step in and figure out the right course of action.

The net effect of all of this is that now when we type git merge foo, there will be a merge commit. Git will ask for a message, and if you're about to do the wrong thing, you can simply delete the contents of the buffer and save (causing Git to abort) or exit with :cq in Vim which is a little easier but causes the editor to exit with an error. Either way, you're a little more in control, and it's gone a long way towards making it much easier to track things and keep our history looking sharp.

One other note, we also set git config push.default upstream if we all work on the same project together in the same repo, or git config push.default current if we're going to be in separate repos. If you're on Git < 2.0, then it's important to set this setting, otherwise you might do something like git push -f on your master branch, but end up rewinding other branches you have locally that track remote branches. This is because in Git < 2.0, the default was matching and would push all of your remote tracking branches, and they were rarely up-to-date.

This is a bit long-winded, but I wanted to show you that there is another way to work that gives you a better shape, removes some of the confusion, and feels easier in the long run--at least it does for me and my team.

I have more of my configuration here, if you're interested.

1
On

I always wince a bit when I read statements like this and see pictures like this WRT git.

somehow that merge commit now exists in both branches ... Both git-gui and GitLab show in the history graph this commit as merging both branches with each other.

master A ------------ E  F <-- master now contains all of feature stuff (B, C, D).
        \              \ | 
feature  B -- C -- D --- F <-- feature now contains "E" edits.

Its suggests a very Subversion kind of thinking: i.e. that branches are buckets and stable, that the commits live in the buckets, and that committing and merging are about moving commits in and out of buckets.


I think for git it really helps to use a different mental model where the commits / commit tree are stable and the branch labels are transient and move around.

I'm going to redraw the diagrams for what @jszakmeister said happened:

$ git checkout feature

                    master
                      |
                      |
         ,----------- E
        /
       A
        \              
         B -- C -- D 
                   |
                   |
                feature

$ git merge master

                     master    <-- last master commit
                      |
                      |
         ,----------- E 
        /              \
       A                F      (try to merge [master] into feature)
        \              /|
         B -- C -- D -/ |
                        |
                        |
                     feature   <-- feature now contains "E" edits?

$ git checkout master

$ git merge feature # fast forwarded

                      master   <-- master now contains all of feature stuff (B, C, D).
                        |
                        |
         ,----------- E |
        /              \|
       A                F
        \              /|
         B -- C -- D -/ |
                        |
                        |
                     feature   <-- feature now contains "E" edits

I would avoid the idea that the top line is the master branch and the bottom line is the feature branch - the "branches" are the labels that move around. A month from now you aren't going to care whether the master label rode down A--E or B--C--D and it won't matter - its all master branch after the merge. If you look at the git log --graph/gitk lines and start thinking of certain columns as fixed branches (buckets), eventually the tool is going to mislead you: "master" branch may be the first column in one part of the tree, but a different column in another part of the tree.

Note that it could have easily been the other way. The feature branch could have been merged into master first, i.e. a merge commit was created and the master label was moved to the merge commit. And then feature was fast forwarded to master, i.e. feature label was moved to the same commit that master is pointing at. The end result/graph would be the same and it doesn't really matter which branch was which or how the labels got to where they ended up.

Note too that moving branch decorations around is cheap and easy. A statement like Both git-gui and GitLab show in the history graph this commit as merging both branches with each other sounds like a big deal, but given the picture, its really not a big deal - it just means a branch label got moved by mistake, but its easy to move it elsewhere.

If you are not happy with one or both merges, use git log --graph --decorate or gitk to visualize the current commit tree and branch labels. and then mentally visualize what the tree is supposed to look like for what you want and move the master and feature labels around to reflect that.

To merge master into feature, but not feature into master yet, you want:

                     master
                      |
                      |
         ,----------- E 
        /              \
       A                F 
        \              /|
         B -- C -- D -/ |
                        |
                        |
                     feature

$ git checkout master
$ git reset --hard E
$ git checkout feature   # Move feature label if necessary
$ git reset --hard F

To merge feature into master, but not master into feature yet, you want:

                       master
                        |
                        |
         /----------- E |
        /              \|
       A                F 
        \              /
         B -- C -- D -/
                   |
                   |
                feature

$ git checkout master   # Move master label if necessary
$ git reset --hard F
$ git checkout feature
$ git reset --hard D

To start over before both merges, you want:

                     master
                      |
                      |
         ,----------- E
        /           
       A            
        \           
         B -- C -- D
                   |
                   |
                feature

$ git checkout master
$ git reset --hard E
$ git checkout feature
$ git reset --hard D

--

More about this kind of thinking here: https://stackoverflow.com/a/23375479/11296