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!
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.