5

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!

4
  • my understanding here is that you simply want to reset your master to the commit before the merge commit (which normally has master and feature previous commits as parent). git reset --hard to that commit (possibly HEAD~1, not quite sure right now) while being on the master branch should do. Commented Dec 3, 2014 at 22:31
  • This is not particularly unusual and not anything to be worried out really. You can avoid it by explicitly telling merge you don't want to do fast-forward merges. Commented Dec 3, 2014 at 22:32
  • then to force push you'll need to have the rights on the repo. Commented Dec 3, 2014 at 22:32
  • @njzk2 thanks, that's what I thought for "fixing" this. Commented Dec 3, 2014 at 23:45

2 Answers 2

3

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.

Sign up to request clarification or add additional context in comments.

5 Comments

yup sorry about gitk, edited it a minute after posting.
That seems reasonable. But I never wanted to merge feature into master. And I can't find any evidence of such a commit. The commit in question was called "merge master into feature", not the other way around... I'll see if I can get a picture in here.
@zorgkang - try using git reflog master to see history. And as he said, he wouldn't have created a new commit, it would have moved master to the existing commit.
@zorgkang I caught things early on when you posted, so I wasn't quite sure of the direction. I've flipped the sense now. I also updated the answer with what we do at my shop. It's wordy, but it helps us maintain the shape that we want, and avoids seeing things like "merged master into feature" in mainline commits on master--which was disturbing.
Great help thanks! I actually did some reading yesterday evening and I had also decided to not ff when merging branches, but ff only when pulling. :)
1

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

1 Comment

Thanks I appreciate your effort. The main reason I duplicated F in my drawing was that it takes a lot of effort to draw these... but I understand what you explained. My confusion was due to not realizing I had done the ff merge and that these didn't exist as separate commits, so I thought a single merge did weird things. And because it was a single commit, I wasn't sure what would happen if I reverted it. Because I didn't have rights to "force push", I couldn't reset, so I did a revert instead (and merged master into feature with -s ours to allow "nice" merging back eventually).

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.