简体   繁体   中英

Messed up git merge

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.

PS - 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

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 add foo; git add bar; git commit 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.)

Try to avoid 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.

Find out where you are now: 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.

Saving the merge commit

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.

Getting rid of the merge

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:

  • change the commit ID to which the current branch's name points, and
  • clear out the work tree and make it match the new commit ID.

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

How this works

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^ :

  1. finds the parent commit ( ^ ) of the current commit ( HEAD );
  2. changes the current branch (part of the built in action of reset ) to match the commit found in step 1;
  3. re-sets the index/staging area (due to --hard ); and
  4. re-sets the work-tree (also due to --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 that you're back to where you wanted to be...

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, ie, 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM