Reputation: 2896
So I have a git repo which has the structure:
.DS_Store
.git
.gitignore
README.md
folder1/
folder2/
folder1
is what I work on, need not bother about folder2
. I branch of master and start working on the files in folder1
. Another developer also works on files in folder1
. He then goes ahead and merges his changes. Now it my turn to merge my changes. My typical git merge
workflow looks like:
git fetch
git rebase -i origin/master
squash commits
git push -f my_branch
However in this case, I went ahead and did a git pull in my branch which raised some conflicts. Using git diff
I resolved the conflicts. Continuing like an idiot I then did git commit -a
which showed up files from folder2
as modified files in the commit screen. I aborted the commit since I realized something was off. Instead of the usualu "No commit message entered, commit aborted" message I got this : Merge branch 'master' of https://github.com/comapny/my_repo into my_branch
. Instead of rebasing my branch on top on master I think I rebased master onto my branch. A git log
shows up like this:
ME:
Merge branch 'master' of https://github.com/company/my_repo into my branch
ME:
final commit on my_branch
Other dev:
His merge
Other dev:
His merge
Other dev:
His merge
Other dev:
His merge
Other dev:
His merge
ME:
2nd commit on my_branch
ME:
1st commit on my_branch
I am really unsure as to what has happened and Id would be great if someone can tell me what I screwed up. I dont even know what to google for.
P.S. - when I visit my repo
on github.com the master is untouched, my branch has the last commit as final commit on my_branch
so Ive just messed up locally
Upvotes: 2
Views: 4212
Reputation: 490068
OK, first, let's just review a bit (this is to help you feel more sure, or less unsure anyway).
Normally when you do git commit -a
, or git add foo; git add bar; git commit
, Git fires up whatever editor you have configured on an otherwise totally empty commit (or more precisely, one consisting of a blank line and some comments that get stripped out). Then, if you exit the editor without writing a message you get:
Aborting commit due to empty commit message.
In this case, though, you were in the middle of a git merge
(because git pull
is just git fetch
followed by git merge
). The merge had some conflicts but you had resolved them. After resolving conflicts, you can commit a merge with git commit
, just as you would for a non-merge.
There's one big difference here though, which is what bit you: a merge commit starts with a non-empty message. So when you exit the editor, Git reads the file ... which has a non-empty message, and it goes ahead and makes the merge commit.
It's too late now, but one trick to stopping the merge commit in a case like this is to delete the non-empty merge message, writing out an empty file. This way Git sees the empty message and aborts the merge commit. (The merge itself would still be in progress, though: to stop that you would need to use git merge --abort
. But that's another recovery process entirely, not the one we need now.)
One more very useful thing to know: Until you push, all your work is local. (Well, unless you let others fetch from you—but you don't; if you did, you would know it.)
git push -f
Using -f
(the force flag) with git push
takes off the safety checks. If you and someone else are both pushing at about the same time, one of you will win and the other will lose. This is a recipe for sadness. Your normal workflow will avoid the need for -f
: if you manage to hit one of these races, your git push
will fail and you will have to git fetch && git rebase
again but it should be nearly painless, and won't force the push and therefore won't lose anything.
git status
It's never bad to check where you are before you do anything. The git status
command does this. Use it: it will tell you which branch you are on right now.
From the above, I can tell that it will say On branch my_branch
, but it's good to check. It should also say nothing to commit, working directory clean
, probably.
You may want to save the merge commit you just made, since it has your conflict resolution. To save it in an easy-to-grab-later method, make a new branch or tag name pointing to it. (Be careful not to push the new name unless you really want it pushed.)
(You can even just save the SHA-1 hash somewhere, including writing it down or copy-pasting to a clipboard or whatever. This keeps you from accidentally giving out the name, but remembering long strings like 19cfa3105d2...
is annoying, and is what branch and tag names are for. Moreover, without a name, this value only remains valid as long as the commit stays in your repository: the default is that every commit is protected for at least 30 days through your reflogs, but without a branch or tag name to contain it, it will eventually expire.)
Let's use a temporary branch name, since that's straightforward:
$ git branch saved-merge HEAD
This makes new branch saved-merge
whose branch tip is the current (HEAD
) commit. We could leave out HEAD
but this is just being explicit.
If you really did not want this merge—if you want to get back to your normal work flow, by removing this commit from your current branch—the git command to remove it from the branch is git reset
.
The git reset
command does way too many things, which makes it complicated to explain. In this particular case, however, we will use it to do two things at once:
This means that you should be very sure that the work tree has nothing valuable in it before we do this. In other words, run git status
and inspect the result carefully. You probably just did, but do it again, it's good for you. :-) Seriously, an occasional reflexive git status
is a good habit. It has saved me from having to recover, numerous times, by telling me that I am still in the middle of a forgotten rebase or merge, for instance. In the bad old version-1.5 days, git status
missed a lot; it is much better now.
Then:
$ git reset --hard HEAD^
will remove the merge. (In some shells the ^
character is special and the alternate spelling, HEAD~1
, is easier to use.)
First, let's take a quick look at HEAD^
(aka HEAD~1
).
The name HEAD
always refers to the current commit and/or the current branch. Commands that need a branch instead of a commit use the branch part; commands like git reset
that need both, use it for both.
The git reset
command resets your HEAD. This particular HEAD is implied: if you said git reset 1234567
it would still reset HEAD
, but to 1234567
. In our case we said git reset HEAD^
, so it resets HEAD, but what it resets to is HEAD^
.
That ^
suffix is the nice little trick. What it does is take any commit—a raw number like 1234567
, or a name like HEAD
or my_branch
—and find the underlying commit, and then go back one commit from there.
In particular, the ^
suffix, without anything else after it, goes back to the first parent. For ordinary commits, that's the only parent, and is the "before" state. For a merge commit like this one, the first parent is still the "before" state; there's just a second parent as well, which is the thing merged-in.
(Aside: you can repeat the ^
to step back further. The name HEAD^^
goes back two commits, HEAD^^^
goes back 3, and so on. The ~
is shorthand for this: HEAD~3
means HEAD^^^
. Since there is one ^
in HEAD^
, HEAD~1
is just a synonym for HEAD^
. In fact, you can leave off the 1
as well. I avoid that, as there is a HEAD^2
notation as well and it means "the second parent", so I try to stick to un-numbered ^
and numbered ~
, so as to avoid using the wrong one.)
The --hard
tells git reset
to re-set both the index/staging-area, and the work-tree, while also changing whatever branch HEAD
indicates. So when we put these all together, git reset --hard HEAD^
:
^
) of the current commit (HEAD
);reset
) to match the commit found in step 1;--hard
); and--hard
).Note that we need to do this exactly once, because if we did it again, that would re-set away another commit. (We could recover using either the reflogs, or the fact that we saved the merge commit ID. Basically, once you commit, you can almost always get things back for at least 30 days, which is why some people use the saying "commit early and often".)
Now you can use git rebase -i
, as is your usual work-flow.
This is pretty likely to get a merge conflict.
You already resolved these merge conflicts, in your merge commit. We can use those resolutions! (Now you know why we saved it.)
In your git log
output, you showed two commits. You might get merge conflicts on just one, or you might get merge conflicts on both. If you are lucky they only happen once and the saved merge result is what you want for that one file and/or case.
(If you get unlucky, you might want or need to re-do multiple merges, in which case you might not be able to re-use your already-done work, but you say you normally squash everything down anyway, i.e., in the end you demand that there be just one final commit of everything, not one commit of some stuff followed by a second commit of the rest of it, for instance.)
So let's say you do the rebase -i
and change all but the first pick
to squash
and let it run. Git will try to cherry-pick and combine all your commits and apply them to the tip of the upstream branch. Some of these may hit merge conflicts, but in the end, you want the resolution you made earlier. So we can cheat, and even if there are repeated merge conflicts, we can just take the already-known final result, like this:
$ git rebase -i
[edit, write, exit editor]
... rebase begins ...
... conflict occurs on files folder1/foo and folder2/bar ...
$ git checkout saved-merge -- folder1/foo folder2/bar
$ git rebase --continue
... another conflict, on just folder2/bar this time maybe ...
$ git checkout saved-merge -- folder2/bar
$ git rebase --continue
One thing to be careful about here is that you can, on occasion, get a change duplicated this way. Check the final result carefully. You should check the final result even if you don't see merge conflicts, though, because Git is not smart and sometimes duplicates things you would not expect it to, or gets automatic merges wrong.
(Automated tests are good, as they tend to catch these cases.)
Once you are all done and thoroughly satisfied with the result, use git branch -D
to delete the temporarily saved branch.
Upvotes: 3