TL;DR
The answer to the question in the subject line is "yes, but." :-) I'll rephrase it a bit here so that we can talk about all the technical details. The "but" is that there's a big surprise if you use git commit --only
. I seem to recall that there was a time when it affected git commit -a
badly too, but it's better about that now. (I'm not sure which Git versions behave badly with -a
.)
Long
Git makes each new commit from an—or the, in most cases—index. The index, or any index, is essentially a proposed commit: it consists of a sort of flattened tree, with blob hash numbers and file names. It's in a form that makes it especially convenient for git mktree
, which turns the index into a series of tree objects and returns the hash ID of the top level tree object, which then goes into the commit object.
The question, then, touches on these three parts:
- Precisely which index is being used to build the new commit?
- Can that index be modified? If so, what happens to the new commit?
- How does this affect the index?
Here, the index is the special, distinguished index that goes with the work-tree. There is one such index per work-tree: if you use git worktree add
you get more work-trees, and each one gets its own private index, but you're only ever in one work-tree at a time, so "this work-tree's index" is the index.
However, when you run git commit
, you can instruct it (using --only
and/or other command line arguments) to build its own private index, separate from the index. If you have done so, it will run each of the various hooks with an environment variable, GIT_INDEX_FILE
, set to the path name of the temporary index. (If not, GIT_INDEX_FILE
will contain .git/index
, which is the path to the index file.)
So the answer to Q1 is: $GIT_INDEX_FILE
.
Now, you can in fact modify whichever index Git is using to build the commit, because Git will re-read this index after running your hooks. So the answer to Q2 is: Yes, and this makes the next commit use whatever's in whichever index is in use.
Q3 is the hardest. If you use git commit --only <paths>
, Git must make a temporary index that:
- is a copy of
HEAD
- except for the specified
<paths>
without disturbing the index at all. If the commit goes on and succeeds, though, Git must now modify the index to account for the fact that these <paths>
have new blobs in them in the new HEAD
commit.
In fact, Git needs to make two temporary indexes (indices?), one for the proposed commit with its --only
files, and one for the proposed commit result. The proposed commit result becomes index.lock
, which acts as the new index.
If you git add
files while running, they will go into the temporary index. But what about the index? Let's find out:
$ cat .git/hooks/pre-commit
#! /bin/sh
echo pre-commit hook, GIT_INDEX_FILE = $GIT_INDEX_FILE
git add sneaky
$ echo this is the base version of sneaky > sneaky
$ echo this is the base version of other > other
$ git add other sneaky
$ git commit -m 'create two files'
pre-commit hook, GIT_INDEX_FILE = .git/index
[master 5131b63] create two files
2 files changed, 2 insertions(+)
create mode 100644 other
create mode 100644 sneaky
As you can see, the pre-commit hook fired here, and added sneaky
. This is no big deal since what got copied into the index for this commit—which is the index—was already that same base version.
Now, however, let's modify the current contents of sneaky
, git add
it, and also modify the current contents of other
, and git add
it, so that we have new contents in the index for both files...
$ echo version 2 of other > other
$ echo version 2 of sneaky > sneaky
$ git add other sneaky
At this point, both index and work-tree have "version 2". Now let's update both work-tree files to "version 3" without git add
ing them:
$ echo version 3 of other > other
$ echo version 3 of sneaky > sneaky
$ git status --short
MM other
MM sneaky
This tells us that the HEAD
, index, and work-tree versions all differ: the HEAD
version of each is the base, the index version is version 2, and the work-tree version is version 3.
Now we run git commit --only other
:
$ git commit --only other -m 'jump other straight to v3'
pre-commit hook, GIT_INDEX_FILE = [path]/.git/next-index-72393.lock
[master 91ec03b] jump other straight to v3
2 files changed, 2 insertions(+), 2 deletions(-)
Let's see what we have in the commit, the index, and the work-tree now:
$ git status --short
MM sneaky
OK, the work-tree version of other
matches the index version of other
which matches the HEAD
version of other
, so:
$ cat other
version 3 of other
they are all version 3. What about sneaky
, though? The HEAD
and index differ, and the index and the work-tree differ. Let's see what's in HEAD
first:
$ git show HEAD:sneaky
version 3 of sneaky
Now let's see what's in the work-tree:
$ cat sneaky
version 3 of sneaky
Aha, these match! The tricky part is viewing the index version, but we can do that too with git show
:
$ git show :0:sneaky
version 2 of sneaky
Whoa, look at that! The index version is the old one!
This, then, is the answer to Q3: the git add
in the pre-commit hook updates whichever index is being used to build the next commit, but if that's a temporary index, it does not update what will become the real index. This is arguably a bug: git add
in the pre-commit hook should also add to the index that will become the index. Making that happen is a little tricky (git commit
could read the temporary index before and after the hook and copy any updates to the index.lock file, perhaps).
Note that I skipped the details for what happens if you commit without --only
. Fortunately, in this case the index you add to is the index, so everything works as you would expect:
$ git reset --hard 5131b63
HEAD is now at 5131b63 create two files
$ echo v2 > other && echo v2 > sneaky && git add other
$ git commit -m 'regular commit'
pre-commit hook, GIT_INDEX_FILE = .git/index
[master 10a8b20] regular commit
2 files changed, 2 insertions(+), 2 deletions(-)
$ git status --short
$
The pre-commit git add
replaced the index version of sneaky
, and it stayed replaced ... which it will even if the commit fails.
Note also that when using git commit -a
, we get a different temporary index:
$ git commit -a -m test
pre-commit hook, GIT_INDEX_FILE = [path]/.git/index.lock
Here, what Git does for git commit -a
is to create the proposed new index as the index.lock
file. The git add
proceeds normally, adding the file in the usual way, and then when the commit succeeds, Git renames index.lock
to index
. This unlocks the index file and, at the same time, puts the modified index in place, so that the answer for git commit -a
to Q3 is that the temporary index becomes the index. This is different from that for git commit --only
.