Search code examples
gitgit-plumbing

Plumbing equivalent for `git checkout -- .`


I'm looking for a scripting-suitable plumbing command (or commands) that are equivalent to the high-level porcelain command git checkout -- .

My initial thought would be to use git checkout-index --all --force, however this does not fully restore the working directory in the case of core.autocrlf = input:

#!/bin/bash
set -ex

rm -rf repo
git init repo
cd repo

git config --local core.autocrlf input

python3 -c 'open("foo", "wb").write(b"1\r\n2\r\n")'
git add foo
python3 -c 'open("foo", "wb").write(b"3\r\n4\r\n")'

git checkout-index --all --force
echo 'I expect this `git status` to have no modifications'
git status

This produces the following output:

+ rm -rf repo
+ git init repo
Initialized empty Git repository in /tmp/foo/repo/.git/
+ cd repo
+ git config --local core.autocrlf input
+ python3 -c 'open("foo", "wb").write(b"1\r\n2\r\n")'
+ git add foo
warning: CRLF will be replaced by LF in foo.
The file will have its original line endings in your working directory.
+ python3 -c 'open("foo", "wb").write(b"3\r\n4\r\n")'
+ git checkout-index --all --force
+ echo 'I expect this `git status` to have no modifications'
I expect this `git status` to have no modifications
+ git status
On branch master

Initial commit

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)

    new file:   foo

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

    modified:   foo

Note that git checkout -- . correctly restores the working directory to the contents of the index, even in this case.


Solution

  • This appears to be a bug in git checkout-index: it doesn't know how to deal with the fact that the work-tree matches the index after all.

    If we add git ls-files --stage --debug, we see that after the first git add, the index contains:

    100644 1191247b6d9a206f6ba3d8ac79e26d041dd86941 0   foo
      ctime: <ct>
      mtime: <mt>
      dev: <d>  ino: <i>
      uid: <u>  gid: <g>
      size: 6   flags: 0
    

    (I have replaced irrelevant variable values with <...> here). Note that the listed size is 6, not 4: this is the size of the work-tree file, which really is 6 bytes long because it contains \r\n line endings.

    Next, we do:

    python3 -c 'open("foo", "wb").write(b"3\r\n4\r\n")'
    

    which replaces the file, rewriting the existing inode with new time stamps and new contents. The new contents are 6 bytes long.

    Then we do:

    git checkout-index [arguments]
    

    which overwrites the work-tree file with the index contents, just as git checkout does. The file is now 4 bytes long ... but the index still says the file is 6 bytes long.

    If we rename foo, so that git checkout-index has to re-create foo with a different inode number, we find that the stat information in the index is still out of date. In other words, even though git checkout-index is re-writing foo, it never updates the cached stat information. So git status's internal index-vs-work-tree diff uses the fast path (compare cached stat data to stat data for actual in-file-system file) and assumes it must be modified.

    (Oddly enough, git update-index --refresh -q won't touch the cache info either, and I am not sure why not.)

    The solution would seem to be to use git checkout directly, at least until git checkout-index is fixed.