TL;DR
The linked answer perhaps should not have suggested git reset HEAD .
. It works, but only if you're at the top of the working tree. A simple git reset
(with no options and no additional arguments) is probably a better idea.
Long
Here's some background to know before reading the git reset
documentation:
A pathspec is a file name, possibly including path components such as dir/file
or a/b/file.ext
, or just file.ext
or whatever.
The git reset
command can affect up to three things: the current branch as seen through the name HEAD
, Git's index, and files in your working tree.
The git reset
command is overly complicated: it has multiple "modes of operation", as it were.
To talk about how git reset
affects HEAD
, we would need to discuss how branch names work1 and how HEAD
is attached to a branch name.2 But the kind of git reset
we'll talk about here specifically does not affect HEAD
itself, so except for the footnotes, we won't bother. It does affect Git's index, though, so let's talk about Git's index before we proceed.
1In a nutshell, a branch name points to a commit (by storing a raw commit hash ID). The name HEAD
is attached to the branch name, so by "moving your HEAD
", git reset
changes which commit it is to which the branch name points. That is, Git writes a different commit hash ID into the branch name.
2The git checkout
command, and the new-in-Git-2.23 git switch
command, do this "attaching" while checking out a commit using some branch name.
Git's index
Git's index is a persisent (stored-on-disk) data structure that, for the most part, records what Git needs to know in order for Git to make the next commit. The way I like to put this is that the index holds your proposed next commit, or at least, its snapshot. In this way, the index fulfills its role as your staging area, which is why it also has this name. But the index does more than just store the proposed next commit. In particular, when you are doing a merge and have merge conflicts, the index takes on an expanded role.
First, though, let's say a bit more about this staging-area role. As the staging area, what the index holds is a full copy of every source file. It does not hold only changed files. The git status
command specifically mentions the changed files, and specifically does not mention the unchanged files. But the staging area holds all the files.
These files originally came out of the current commit. If you have not replaced a file in the staging area, and it's still the one that came out of the current commit, then it matches the current commit. So git status
just doesn't mention it. It's still there! It's just not mentioned! But if you did change a working tree file, and then run git add
, you told Git: Eject the file that you have now, and put in this other file, using the same name. So now the staged file is different from the committed file.
It doesn't matter much for our particular purpose here, but it's worth noting that the staged copy of each file is already in Git's compressed and de-duplicated format. This is how files look when they're in commits: the name is stored somewhere else—as it is in the index—and the contents are stored compressed and de-duplicated, as they are in Git's index. The files in your working tree are not compressed and not de-duplicated,3 so git add
has to compress and de-duplicate them before storing them into Git's index, so as to be staged for the new commit.
The point of all of this is to make you aware of these things:
- What's in Git's index is in commit-able format.
- Every file that is going to be in the next commit is always staged. Removing a file from the staging area means that the entire file won't be in the next commit.
git status
doesn't bother to tell you about files that are the same in the current commit as they are in the index. It only says staged for commit when these files are different in some way. That way, even if your commits all hold 10,000 files each, you only need to pay attention to the two or three files you changed.
When you go to make a new commit, e.g., by running git commit
, what Git does is make the new commit using the index copies of the files as the snapshot. Since they're already in the format used for commits, this goes very fast.
In this particular case, though, we're concerned with the index after a merge conflict. So we need to understand the expanded index, not just the normal everyday proposed-next-snapshot index.
3Technically, whether working tree files are de-duplicated is up to your operating system. Using ZFS on Linux or FreeBSD, for instance, you can configure the file system itself to do de-duplication. That's irrelevant to Git, which does its own separate de-duplication.
The expanded index
In the normal index setup, every file is at what Git calls stage zero. We can see this—and the staging numbers—using git ls-files --stage
. Here is a snippet of what is in a Git index for the Git repository for Git:
100644 908330a0a3d5d1c1bad56544ba5bb18c3b783c84 0 .travis.yml
100644 5ba86d68459e61f87dae1332c7f2402860b4280c 0 .tsan-suppressions
100644 65651beada79b6267b1d0bda518a88269374cfdf 0 CODE_OF_CONDUCT.md
100644 536e55524db72bd2acf175208aef4f3dfc148d42 0 COPYING
100644 ddb030137d54ef3fb0ee01d973ec5cee4bb2b2b3 0 Documentation/.gitattributes
100644 9022d4835545cbf40c9537efa8ca9a7678e42673 0 Documentation/.gitignore
100644 45465bc0c98f5d88cfe1ade092d29b5dc32c1e23 0 Documentation/CodingGuidelines
(the full index contents run for almost 4000 lines: one entry per file, and there are almost 4000 files). The staging number is the third field in each line, and as you can see, each one is zero here. This means there are no merge conflicts in progress. The last field holds the file's name, as seen in the index; note that many names have embedded slashes (this is always a forward slash, even on Windows).
When you have a merge conflict, what Git does is leave, in the index, three—or more precisely, up to three—versions of each file.4 These three files are at three nonzero staging index numbers. Staging number 1 means that a file in the index is from the merge base commit. Staging number 2 means that this file is from the "ours" or HEAD
commit, and staging number 3 means that the file is from the "theirs" commit. The existence of any entries at a nonzero stage indicates that this particular file has a merge conflict.
One way you can think of this is that each file has up to four "slots". Slot zero is used if the file is not conflicted. Otherwise, slots 1, 2, and/or 3 are occupied. So, if a file is in merge-conflict state, it has a nonzero slot number (and there might be more entries for this same file, with other slot numbers). Otherwise it has a zero slot number (and there are no more entries for this file).
Whenever any file is in merge-conflict state, Git will refuse to make a new commit. It literally can't, because only slot-zero entries can go into commits: there's no space for a slot number in the commit. So, before you can commit, you must resolve the conflicts.
Exactly how you resolve the conflicts is up to you. What Git requires from you is simple though: it just needs you to tell Git: Take out the slot 1, 2, and/or 3 entries entirely. You can do that in many ways:
git rm
will remove these entries.
git add
will copy a file from your working tree into slot zero in the index, removing the nonzero entries.
git reset
and git restore
can both put in a stage-zero file and hence remove the nonzero entries as well.
This last one is going to be the key to the answer.
4The original index design was supposed to allow more than three here, but in fact nothing seems to make use of this.
Let's look back at the question again
The original question kicking all this off was what, if any, difference there is between:
git reset HEAD
and:
git reset HEAD .
and then how this was used in another StackOverflow answer (which consisted of running six listed commands).
The git reset HEAD .
command is perhaps better written as:
git reset HEAD -- .
to make it clear that .
here is a pathspec. We now turn to the git reset
documentation, specifically the SYNOPSIS section, which reads:
SYNOPSIS
git reset [-q] [<tree-ish>] [--] <pathspec>…
git reset [-q] [--pathspec-from-file=<file> [--pathspec-file-nul]] [<tree-ish>]
git reset (--patch | -p) [<tree-ish>] [--] [<pathspec>…]
git reset [--soft | --mixed [-N] | --hard | --merge | --keep] [-q] [<commit>]
There are some syntactic tricks to understand here: things in square brackets []
are optional, things in angle brackets <>
are to be replaced with some non-empty string, and ellipses (…
) represent "one or more of whatever we just said". (In this case, that's one or more pathspecs.) Parentheses surround alternatives that are separated with a vertical bar |
, so (--patch | -p)
means that you may write either --patch
or -p
here. These syntactic gimmicks are mostly standard across most Unix/Linux documentation, in SYNOPSIS sections.
There are more tricks here, specific to Git: the word tree-ish
means anything acceptable according to the gitrevisions documentation, as long as Git can turn that into an internal tree object specifier. In this case, that means anything that specifies a commit works. HEAD
specifies a commit—the current commit—so git reset HEAD
matches this git reset <tree-ish>
.
The first synopsis entry requires git reset
, allows an optional -q
, allows an optional <tree-ish>
, allows an optional --
, and then requires a <pathspec>
. So:
git reset HEAD
does not match this form, because the pathspec is missing. But:
git reset HEAD .
does match this form: it omits the --
but that is allowed.
The second form in the synopsis section requires the git reset
part, has an optional -q
as before, allows an optional --pathspec-from-file
—if that's used, --pathspec-file-nul
can also be used—and then has an optional <tree-ish>
. So:
git reset HEAD
matches this form. (This is a glitch in the documentation!)
The third form requires either --patch
or -p
so neither command matches this one.
The last form requires git reset
(as always), then allows one of the options --soft
, --mixed
, --hard
, --merge
, or --keep
, an optional -q
, and an optional <commit>
(note that this is not a <tree-ish>
but specifically a commit). The:
git reset HEAD
command matches this form too. So git reset HEAD
, without a dot as a pathspec, could be one of these.
As I suggested above, this is a bit of a glitch in the documentation: which of the two allowable matches should git reset HEAD
take? We only learn this by reading on in the documentation (and even then we have to guess a bit, or try a test reset).
The next section is the DESCRIPTION section. It says that the kinds of git reset
that use a pathspec
... reset the index entries for all paths that match the <pathspec>
to their state at <tree-ish>
. (It does not affect the working tree or the current branch.)
What this means is that the state of the file(s) in the specified commit—HEAD
, in this case—is copied into the index. What's not quite mentioned (though it's at least partly covered in the next paragraph via a reference to git restore
) is that this has the same side effect of clearing out nonzero-stage entries as git add
.
So this answers what:
git reset HEAD -- .
does: it resets, as in clears-out-conflicts as well as copying the HEAD
commit copy of, each file matched by the pathspec .
.
That leaves us with the question: Which files are matched by the pathspec .
? There's a flaw in the current reset documentation. It refers us to the gitglossary page, which tells us that pathspecs could be relative to the top of the working tree, or to the current working directory, and that each more-specific page (i.e., the one for git reset
) should say. It doesn't say. The fact is that .
here is relative to the current working directory. So if you're not at the top of the tree:
git reset HEAD -- .
means only the files in this directory and below.
Experimentation (git rm --cached
of some existing file, and git add
of some new file) shows that git reset HEAD -- .
restores to the index any file missing due to the git rm
, and removes from the index any new file not in HEAD
. It might be nice if the documentation were clearer on this, but perhaps we can take the experimental results as definite / goal-behavior.
Let's move on to the other command:
git reset HEAD
Does this mean git reset
with no --pathspec-from-file
options but a <tree-ish>
, or does this mean git reset
with no --hard
or --mixed
or whatever but a <commit>
? Well, if it were the former, it would not supply any pathspecs at all. The idea behind --pathspec-from-file
is to supply the pathspecs in a file, rather than on the command line, but that way there are some pathspecs. If Git treats this as the former, there are no pathspecs at all.
We can try a test here:
$ git reset HEAD^{tree}
error: object fcb94a429496c28fa7f95926e9d46840671d0d88 is a tree, not a commit
fatal: Could not parse object 'HEAD^{tree}'.
This uses the gitrevisions syntax to make sure that we supply git reset
with a tree-ish, rather than a commit. The result is an immediate error. Running git reset HEAD
works, so it must be the case that this particular git reset
is matching this with the fourth syntax, not the second. (The documentation should have omitted the square brackets around --pathspec-from-file
.)
The fourth syntax is the one described by the:
git reset [<mode>] [<commit>]
section, which says:
This form resets the current branch head to <commit>
and possibly updates the index (resetting it to the tree of <commit>
) and the working tree depending on <mode>
. If <mode>
is omitted, defaults to --mixed
. [snip]
So this is a --mixed
reset, since we omitted the <mode>
parameter (which is one of those four choices enumerated in the synopsis). The commit we choose—which causes the current branch name to move to that commit—is the commit selected by HEAD
. But HEAD
is the commit selected by the current branch name. So the move is from some commit—let's call it "commit X"—to commit X. If you jump from where you are standing now but arrange to land where you are standing now, that wasn't really much of a move, was it? :-)
Anyway, this means that the resets the current branch head phrase becomes irrelevant: the current branch head just stays where it is. We move on to and possibly updates the index. Whether git reset
updates the index depends on the <mode>
, which we just said is --mixed
, of which the documentation goes on to say this:
Resets the index but not the working tree ...
So this copies the current commit back into the index. It's a bit like using a pathspec that matches every file, except that we don't need a pathspec at all, and in fact are forbidden from using one. (Using a pathspec gets us into the first syntax, instead of this fourth syntax.)
As with git reset
with a pathspec, this has the side effect of undoing any merge-conflict state: any nonzero stage entries in the index get replaced with their stage-zero copy from the commit.
Something that is not well documented, but has always been true of a standard --mixed
reset, is that this kind of git reset
will remove from Git's index any file that is not in the selected commit. As I found by experimentation, the git reset HEAD -- .
command behaves the same way here.
Summary
Let me copy this again here:
As it turns out, Git is smart enough not to drop a stash if it doesn't apply cleanly.
This part means that the problem being addressed is:
<do some hacking>
<realize that this is the wrong commit>
$ git stash
$ git checkout somebranch
$ git stash pop
<receive merge conflict messages>
The git stash pop
operation stopped in the middle of its first (git stash apply
) step and has completely omitted, and will not run, its second (git stash drop
) step here.
I was able to get to the desired state with the following steps:
- To unstage the merge conflicts:
git reset HEAD .
(note the trailing dot)
This is where the question came in. A git reset --mixed HEAD
, which can be spelled git reset
, is probably a better approach here. This puts the index back to the state before the git stash apply
even started.
(Since we definitely don't need the result of this merge—we can re-create it any time later as long as we still have the two stash commits—we could just have run git reset --hard
. But this particular guy did not do that.)
- To save the conflicted merge (just in case):
git stash
This is quite unnecessary. It makes two more commits—each stash entry is either two or three commits; see this old answer of mine—one of which is a duplicate of the current commit and one of which holds the tracked files from the working tree; then it runs git reset --hard
(which we could have done earlier).
- To return to master:
git checkout master
This of course does exactly what we'd expect. Because of the git reset --hard
at the end of git stash
, there are no staged or unstaged changes: the current commit, Git's index, and your working tree all match, and git status
would say nothing to commit, working tree clean
.
- To pull latest changes:
git fetch upstream; git merge upstream/master
This obtains new commits from a remote named upstream
, then does whatever git merge upstream/master
will do; this obviously depends on the commits on upstream/master
obtained in the git fetch
vs the commits on the current (master
) branch.
If we make some assumptions—that master
is normally in sync with upstream/master
except when the upstream Git's users add new commits to upstream
—this would do a simple fast-forward operation.
- To correct my new branch:
git checkout new-branch; git rebase master
This will do the usual rebase work.
- To apply the correct stashed changes (now 2nd on the stack):
git stash apply stash@{1}
If we had skipped the extra (unnecessary) stash earlier, git stash apply
would do the desired thing here.