Let's make a few notes, then draw some commit graphs, since git merge
's actions depend critically on both the commit graph and the trees attached to the three "interesting" commits.
git commit --amend
does not modify a commit (because this is impossible). Instead, it makes a new commit and shoves the old one aside to make room for the new one, instead of doing the usual "add new one on the end".
git commit -o
(or --only
) omits the current index (staging area), making a temporary index instead, starting from the previous commit. That's a reasonable enough thing to do when amending, although if you have not staged anything, this has no effect. So this "amended" (really, copied) commit will have the same tree as the original commit, even if you had staged something. (This only matters for note 3.)
Finally, the subsequent git commit
(after removing a file and staging the removal) will make a new commit with the file removed, plus anything else you already had staged (as noted in note 2).
Now, we don't know whether the commit you're amending here is a merge commit itself, but let's just assume it isn't. (If it is, the picture gets more complicated, but the result is the same.) We also don't know for sure, but can reasonably assume, that the commit you're amending was already pushed—since that's precisely what will cause this problem. So, we can draw the graphs like this.
First, we draw the "before" situation: before you amended anything. That graph is pretty straightforward:
...--o--*--A <-- master, origin/master
That is, your branch master
points to commit A
(the one we're about to "amend"), and origin/master
—your copy of master
as seen in the other Git repository, over on the machine origin
—also points to commit A
. Commit *
is the commit just before A
; this one will be important later. There are additional earlier commits as well.
Now let's draw the graph after amending, but before making any new commits:
A <-- origin/master
/
...--o--*
\
A' <-- master
Commit A'
is the amended commit, with slightly different message. It has the same tree as commit A
—the same source code—but a different message, and therefore a different SHA-1 ID.
Let's add on the commit that deletes the file, and call this D
for D
elete:
A <-- origin/master
/
...--o--*
\
A'-D <-- master
You now try to git push origin master
and get an error. The error is because, on origin
, their master
points to commit A
, or perhaps even some commit(s) after A
that you do not have.1 Let's just proceed with the theory that origin
is not a super-active repository gaining ten new commits per second or whatever, though, and hope that it's just A
. :-)
What their Git, over on origin
, is complaining about is that if they let you do this git push
, they will "lose" commit A
. Your Git is asking their Git to set their master
to point to your commit D
(after, of course, handing over your A'
and your D
). If they do that, they will no longer have a name pointing to commit A
: commit A
will be forgotten.
Of course, this is precisely what you did to your own repository when you amended A
. You told your Git: "forget about A
, shove it off to one side where the only name for it is origin/master
, and give me an A'
copy with a different message."
OK, so far so good, except that the push failed. So you ran git pull origin master
.
1It's not possible to tell precisely what they had then, although you can see, now, what you have now, which will match up with what they had right when you ran git pull
. Of course your git pull
was at least several whole seconds ago, and who knows how much has changed in that time! Seriously, there's a synchronization issue because whatever you're doing in your Git, someone else may be doing other things in other Gits right now. How much of a problem this is for you depends on just how active origin
is.
git pull
is not the opposite of git push
In fact, there is no exact opposite for git push
. The closest thing is git fetch
, which tells your Git to call up their Git, same as with a push, but instead of sending them your commits and asking them to move their branches, you get their commits, and you have your Git move your "remote-tracking branches": your origin/master
.
What git pull
does is first run git fetch
, and then run git merge
.
git fetch
The first part of git pull origin master
is that Git runs git fetch origin master
.
This fetch
step called up their Git and brought over their new commits. We're assuming there were none: that their master
still points to their A
, which is also your A
. So your Git updated your origin/master
to keep pointing to your A
(not much of an update: nothing changed):
A <-- origin/master
/
...--o--*
\
A'-D <-- master
(If they did in fact have some commits—let's call them B
and C
—then your Git obtained these commits and updated your origin/master
:
A--B--C <-- origin/master
/
...--o--*
\
A'-D <-- master
but probably there were no such commits. Let's proceed without them.)
git merge
The second part of git pull origin master
is to run git merge
, with some extra arguments that are too complicated to reproduce exactly, but more or less amount to origin/master
.2 Let's just pretend it really is git merge origin/master
, since that's a little easier to work with.
What git merge
does is to first find the merge base, which is the first commit that is the same between your current branch—that's your master
—and the commit you tell it to merge in. The commit you (or git pull
, really) are telling your Git to merge in is the tip of origin/master
, i.e., commit A
.
So now we go back to that graph we've been drawing and find the merge base. That's commit *
. We also find the tip of origin/master
, i.e., commit A
, and the tip of our current branch: commit D
.
Next, we make (or Git makes) two git diff
listings:
git diff <id-of-*> <id-of-A> # "what they changed"
git diff <id-of-*> <id-of-D> # "what we changed"
Presumably, between commit *
and commit A
, "they" (whoever they were, maybe us) changed the file you deleted in D
.
Meanwhile, between commit *
and commit D
, "we" deleted the file. Note that Git doesn't even look at commit A'
here: all it does is compare *
and D
. It doesn't matter that we changed the file in the same way between *
and A'
, it only matters that we deleted the file in the overall change.
This is the source of the merge conflict. Between "what they did" and "what we did", Git does not know whether to keep the change to the file, or keep the deletion of the file.
If you wish to make the merge—you probably don't—you can resolve this by running git rm <file>
and then git commit
to commit the merge. The result will be a new merge commit:
A <-- origin/master
/ \
...--o--* \__
\ \
A'-D--M <-- master
The tree for the new merge will match that for commit D
, but now you will have both the original commit A
and the amended copy (with same tree but different commit message) in your history.
You will be able to push this to origin
, because now if you send them commit M
and ask them to set their master
to point to M
, they won't lose commit A
.
2The difference mainly shows up in the default merge commit message, which says, approximately, "merge the branch named master from the remote named origin" rather than "merge our own remote-tracking branch origin/master". Also, if you have an ancient version of Git, predating 1.8.4, there are a few additional technical gotchas that prevent origin/master
from working right here. Hopefully you're not stuck with Git version 1.7.x.
What to do about this mess
First, you can abort your in-progress merge:
git merge --abort
Now you're back to the original:
A <-- origin/master
/
...--o--*
\
A'-D <-- master
in your repository.
The big problem here is that they, whoever they are, over on origin
, have commit A
. You may be able to use a force-push to get them to ditch it:
git push --force origin master
This tells their Git "go ahead and lose commit A
, just set your master
to D
. The drawback is that if they've acquired some commit(s) B
and/or C
now, you will also lose those commits. Also, if anyone else has acquired a copy of commit A
, those other users have to do something to recover from your resetting origin
to abandon A
in favor of A'
.
You can use --force-with-lease
to tell them, in effect, "I believe your master
points to commit A
; if so, make your master
point to D
instead." (This requires that both your Git and their Git be new enough to have the --force-with-lease
option.)
Or, you can abandon your idea of modifying the commit message. Just give in and transplant your commit D
so that it comes after A
, instead of after A'
. Give up your modified A'
entirely, giving you this graph:
A <-- origin/master
/ \
...--o--* D' <-- master
\
A'-D [abandoned]
You can now push this because your new copy D'
of your original D
simply adds on to their existing commit A
. If, after doing this, we forget about the abandoned commits entirely and draw this more nicely, we get:
...--o--*--A--D <-- master, origin
which is a nice straight history, and is what you were hoping for.
Summary
Your three options, after using git merge --abort
to escape the in-progress merge, are:
- force push;
- force-with-lease;
- give up on the amend: cherry-pick
D
into D'
, or repeat your deletion to create D'
, after resetting your own master
to point the same place as origin/master
.
(If origin
is private enough, option 1 is safe and easy. [Also, pushing while you're in the in-progress merge is largely harmless since push only pushes completed commits.])