Reputation: 7908
I did git commit -a
in the middle of a merge. I was trying to commit the resolved conflicts and leave the unresolved ones to a colleague of mine. Retrospectively, that sounds like a bad idea.
How can I undo this git commit -a
and get back the merge conflicts?
I tried git reset filename
and git rm --cached filename
but with no luck.
Upvotes: 0
Views: 388
Reputation: 487885
For a TL;DR see David Sugar's answer: you must throw out, or at least stop using, the merge commit you made. What I'm going to write about here is what really happened, and why:
I was trying to commit the resolved conflicts and leave the unresolved ones to a colleague of mine. Retrospectively, that sounds like a bad idea.
is right: it's a bad idea. :-)
It's worth pointing out two things here:
git commit -a
is loosely1 the same as git add -u; git commit
.It's the git add -u
step (or its equivalent, really) that caused the problem: that declared to Git that all conflicts were resolved, after which you could commit.
Unfortunately, using git add
to resolve conflicts is almost irreversible—I'll go more into the "almost" part in a bit—and a subsequent git commit
makes it completely irreversible. You will have to throw away this merge commit, in some way or another.
A conflicted merge means that Git's index has taken on an expanded role. But without a good clear definition of what "the index" actually is in the first place, knowing this isn't much help. So let's take a brief look at what exactly the index is.
Let's start with this: there are actually three different terms for this thing. While a lot of Git calls it the index, other Git documentation calls it the staging area—which refers to how you use it—and some really old parts of Git leave a trace where the same thing is sometimes called the cache. So the index, the staging area, or the cache all refer to the same thing—which is mostly a file inside the .git
directory, .git/index
.2
What I like to say about the index, to summarize it in a nutshell, is that it holds the next commit you plan to make. The way this works is that the index holds a copy of—well, really, a reference to—a frozen, ready-to-commit version of each of your files. This frozen copy came out of the current commit.
The fundamental nature of Git is that it is all about commits. Commits hold full snapshots of files. They do not hold changes to files! To see changes, you must compare one commit to some other commit. For instance, suppose you compare the latest commit to the second-latest. Whatever is different in those two commits, those are the changes.
Files that are frozen for all time are great for archival, but useless for getting any new work done. So when you git checkout
some commit, Git fills in your work-tree with unfrozen (and de-compressed and otherwise de-Git-ified) copies of all of your files. The frozen (and compressed and Git-only) files came out of the commit, and got expanded into useful form in your work-tree. But they all got copied into the index first!3 So the frozen-form files are there, ready to go into the next commit.
We often use the phrase staging area because of the way we use the index. Having made some change to some file in the work-tree, we have to get Git to prepare a new, frozen-format, compressed and Git-ified copy of the updated version of the file. To do that, we run git add file
. That boots the old copy of the file out of the index and replaces it with the new (frozen format) copy.
You can, of course, also git add
a file that isn't in the index at all. In this case, Git prepares the frozen copy as usual and puts into the index a new file.
What's in the index, then, is really one entry per file that will be in the next commit:
If the frozen-format index copy of a file that's going to be in the next commit is different from the frozen-format file that's in the current commit, git status
will say that the file is staged for commit
. If the frozen-format index copy of that file is the same as the current commit copy, git status
says nothing about the file.4
So this is what you should know to start with, before we dive into the conflicted merge case: The index holds the proposed next commit. A git commit
works by packaging up the index copy of each file—which is already in the format that git commit
needs—and adding the commit metadata, including your name and email address and whatever log message you supply.
It's worth noting here that Git doesn't really care very much about the work-tree copy of any file. The important copy, for git commit
, is the one in the index. In general, your work-tree is for you; Git's index is for Git. There are some exceptions to this rule, though.
1I say "loosely" because git commit -a
uses a temporary index while git add -u
uses the index.
2Sometimes it gets split up into more than one file, but it's always possible for everything important to be in one file. This is because Git allows Git programs to provide a separate temporary index, as mentioned in footnote 1. To use a different index temporarily, you simply export an environment variable, GIT_INDEX_FILE
, set to the name of the temporary index. The various Git commands then use that temporary index instead of the real one.
3As noted before, Git doesn't literally copy the whole file itself into the index. Instead, it keeps a reference to the frozen copy. So when you git add
an updated version, Git checks to see if there's a frozen copy of the updated file, and if not, creates one. Git then puts the reference to the correct frozen copy into the index.
4The git status
command also, separately, compares each file in the index to the unfrozen, normal-format copy in the work-tree. When they are the same, git status
says nothing. When they are different, git status
says not staged for commit
.
To perform a true merge—the kind that can have conflicts—Git has to identify three commits. Typically,5 merges start with you running:
git checkout somebranch
git merge otherbranch
The three commits that are involved in this operation are:
HEAD
, which is the last commit in somebranch
; andotherbranch
.At this point, Git expands the index. Instead of one entry per file, it now uses three. The best way to describe this is to imagine the index has having four "slots" per file entry:
--ours
files, from HEAD
.--theirs
files, from otherbranch
Slot zero is cleared out. The three copies of each file, from each of the three commits, go into slots 1, 2, and 3. Then the main work of git merge
begins:
If all three slots hold the same contents, nobody changed the file at all. Move the file (from any slot) to slot zero. The merge for this file is done: we took any of the three copies because they are all the same.
If slot 1 matches slot 2, but slot 3 is different, they changed the file. Move the file from slot 3 to slot zero and empty out slots 1 and 2. The merge for this file is done; we took the slot-3 copy—their version—because we didn't change the file but they did.
If slot 1 matches slot 3, but slot 2 is different, we changed the file. Move the file from slot 2 to slot zero and empty out slots 1 and 3. The merge for this file is done.
If all three slots differ, we changed the file and they changed the file. Compute the actual differences (with git diff
of that one file), combine the two sets of changes, apply the combined changes to the copy in slot 1, and—provided that this worked—write the result to slot zero and clear out slots 1, 2, and 3.
There are a few more (and trickier) cases when files get created, renamed, and/or deleted, but these are the three main, common cases. The last one above is the hard case, because sometimes, combining the changes doesn't work.
5I say "typically" here in part because git pull
runs git merge
, and because Git can invoke the merge machinery for operations that result in ordinary, non-merge commits, such as with git cherry-pick
.
When we need a real merge, and hit a merge conflict, what Git does is this:
Git leaves all three input version of the file in the index, in the three slots.
Git writes to your work-tree Git's best effort at merging the three input files, complete with conflict markers.
Your job is to come up with content and put it in index slot zero. Git does not care how you do this! All it cares is that you do it. To resolve a conflicted file, you construct the right file and then stuff it into slot zero. You can do this in any way you like.
Some people edit the work-tree file and inspect the conflict markers. This is my usual preferred method. Some people use third-party merge tools, perhaps running git mergetool
to invoke them: these generally extract the three input versions and put them up in multiple windows/panes and have you select the final result. In all cases, your goal is to get the final, correctly-merged copy of the file into slot zero.
If you do use the "edit work-tree copy of file until satisfied" method, you end up by writing the correct file to your work-tree and running git add file
. The git add
step notices that there are copies in slots 1, 2, and 3 and removes them, while copying the work-tree file (and compressing, Git-ify-ing, and otherwise getting the content freeze-ready) into slot zero. So that does the trick.
If you use git mergetool
, Git runs your tool of choice and—depending on lots of conditions—then runs git add
for you, which does the same write to slot zero and clear out slots 1, 2, and 3 thing. That, too, does the trick.
You can even remove the file, with git rm
, which empties out all the slots and removes the work-tree copy as well. In any case, git commit
won't run until all higher slot number entries have been cleared out. That's how Git determines whether you've resolved the merge conflicts: by the slot numbers.6 If all occupied slots are the normal slot zero, the conflicts are resolved.
6To see the slot numbers, along with the non-internal non-debug information in the index, use git ls-files --stage
. Note that this command is not meant for everyday use—it's one of Git's plumbing commands, meant to be a building block for user-facing or porcelain commands—so it does not page its output.
git add -u
/ git commit -a
goes wrongIf you run git add -u
, Git assumes you mean to git add
each file that is not in the "index and work-tree match" state. That includes all the conflicted files: it effectively runs git add
on each conflicted file, copying whatever is in the work-tree—usually including conflict markers—into the index, ready to be committed. With the git commit -a
variant, it then goes on to make the commit.
Suppose you have the three copies of one file in staging slots 1, 2, and 3. You have the conflicts-marked-up copy in the work-tree. You intend to edit this file for a while and then git add
it, but you accidentally git add
too soon. The git add
step wipes out the slot 1-2-3 copies.
You can just continue editing the work-tree copy, and git add
again. That works fine: it just replaces the slot zero copy with a new, updated slot zero copy. But what if you realize you have totally botched the conflict resolution and want to start over?
Fortunately, git checkout
offers a flag, -m
, for this.7 Running git checkout -m -- file
tells Git to find the secret "recover/undo" record that Git leaves, for a bit, in the index when you wipe out the higher numbered slots. The undo record keeps the three nonzero slot entries around, for that file, until you commit. This lets you get the three files back into the three index slots, and removes slot zero.
You can also use this if you haven't run git add
and all three nonzero slots are still occupied. In this case it doesn't have to search for the undo record, it just works with the existing index slots.
In all cases, this kind of git checkout
will also write the conflicts into the work-tree copy of the file again. In fact, it redoes the conflicts. This allows you to change the conflict style, from merge
to diff3
or vice versa, if you like:
git checkout --conflict=diff3 -- file
(the --conflict
argument implies -m
). You may find the diff3
style more useful than the default merge
style. If you do—I do—you may wish to use git config
to set it as your default:
git config --global merge.conflictStyle diff3
The undo/redo-conflict entries get wiped out by git commit
, though, so once you have committed, you can't restore the conflicts. You just have to repeat the merge instead.
7Watch out: git checkout -m -- file
means one thing, but git checkout -m branch
means a different thing entirely. This is yet another case of Git putting too many things into one command. The new Git 2.23 git switch
and git restore
commands help out here, by splitting up git checkout
into multiple different commands for its different intended use cases.
Upvotes: 1
Reputation: 1236
Executing git commit -a
commits all changes in your worktree. To undo this:
Case 1: You have not yet pushed the commit to a central repository. (Have not published yet) In this case, you want to undo the commit---as though it never happened.
git reset --hard HEAD^
This resets the branch to the parent of the current commit, discarding all changes made with that commit. Then perform the merge again:
git merge <branch>
Now you can fix any conflicts, and commit after fixing them.
Note that this discards all changes you have made for the merge. If you do not wish to do that (for example because you have resolved 15 out of 20 conflicts), you can continue by fixing remaining conflicts and then amending your commit:
<fix remaining conflicts>
git add <files that need fixing>
git commit --amend #this does not create a new commit; it fixes the current one
Case 2: You have already pushed the commit to the central repository, others may have already fetched or pulled the commit
In this case, you will want to fix the conflicts you mistakenly committed in your worktree, and make a new commit when done. You do not want to re-write history.
<fix all files with conflicts in your working tree>
git commit -a #make sure you are committing the right files.
You can use git log (or a gui that has a good git repository browser) to see the list of files changed by your merge commit.
git show <hash of merge commit>
will show you the files changed, and diffs of them. You can use this to help you resolve your merge conflicts.
Upvotes: 1