Consider what git reset
(in its three modes, soft vs mixed vs hard, and not its other additional things that aren't one of these three modes at all) does. You run:
git reset --<mode> <commit-specifier>
and it does the following three things, or maybe 2, or 1:
(--soft
, --mixed
, --hard
) It changes the current branch name, as stored in HEAD
, to point to the given commit. With --soft
, it now stops.
(--mixed
and --hard
) It replaces the existing index contents with the contents of the commit you selected in step 1. With --mixed
(or the default), it now stops.
(--hard
only) It replaces the work-tree contents with the files as updated in step 2.
Now, you can, if you choose, select the current commit as the target in step 1, by running git reset HEAD
for instance. If you do this, the updated current commit in step 1 is the current commit. That is, step 1 makes no actual change.
If you run git reset --soft HEAD
, step 1 makes no change, and git reset
then quits. So this does absolutely nothing.
If you run git reset
without --soft
, however—with no argument at all, which defaults to --mixed
, or with --hard
—it goes on to step 2: replace the index's contents with the files that are frozen for all time into the current commit. In effect, this un-does any git add
that you did earlier.
If your goal is to re-do all the git add
again afterward, this isn't useful. But if your goal is to not re-do all these git add
s, it can be. Consider, for instance, this example—not terribly useful by itself, but meant to be illustrative:
git checkout somebranch
<edit for a while, test, `git add .`>
# think: wait, I don't want to commit everything
git reset # index now matches HEAD
<edit README file to announce that the NEXT commit changes a lot of things>
git add README
git commit # make a commit in which everything is the same except README
git add .
git commit # make final commit in which everything is changed
To make sense out of all of this, remember that there are, at all times, three copies of each file:
There is a read-only, frozen-for-all-time copy of each file (README.md
, main.py
, etc., whatever files you have) in the current commit. You can change which commit is the current commit any time you like, using git checkout
or git reset
, but the commit itself is frozen for all time: none of its files will ever change at all, nor will its commit message, author, and so on.
There is a frozen-format copy of the file in Git's index.1 You can change this all you like: you can overwrite the copy of the file with a copy of another file or another version of the same file. You can even remove the file from the index entirely, using git rm
. The index copy is in the frozen format, ready to go into a commit, but is not itself a commit, so it can be changed.
Last, there's a usable version of the file. It's not in some special Git-only format, and the rest of your computer programs can actually use this version of the file. That version resides in your work-tree.
It's very typical for the index copy of a file to match at least one other copy—the HEAD
copy, the work-tree copy, or both—but in fact, you can have all three files be different. To do that, just:
- edit the work-tree copy (be sure to save it so that
git add
can see it);
- use
git add
to copy the work-tree copy to the index; and
- edit the work-tree copy some more (and be sure to save again).
Now the file is both staged for commit, meaning that the HEAD
and index copies differ, and not staged for commit, meaning that the index and work-tree copies differ.
1Technically, what's in the index is just a mode, file name, and reference to an internal Git blob object. The blob object represents the frozen-format copy of the file's content. Reset (git reset
) copies the mode, name, and hash ID into the index. Adding (git add
) compresses (makes ready-for-freezing) the work-tree file by making a new blob object, or finding the already-existing blob object, that will re-expand to the work-tree file later.