checkout: gracefully handle files deleted from the index

Right now, when someone deletes a pointer from the index with `git rm`
and then runs `git lfs checkout`, the operation fails with a message of
"Could not update the index" because our invocation of `git
update-index` is missing the `--add` flag.

Obviously, the user does not expect an error in this case, and `git
checkout` simply ignores files staged for deletation, so let's do the
same thing.  If a file on disk is deleted, check the index with `git
diff-index` to see if it's deleted from `HEAD`.  If so, ignore the file,
just like Git does. Note that we use `git diff-index` specifically
because it doesn't refresh the index and is therefore much cheaper than
alternatives, such as `git status`, which might do that.
This commit is contained in:
brian m. carlson 2024-04-02 17:11:01 +00:00
parent 19a702e780
commit d3299175ff
No known key found for this signature in database
GPG Key ID: 2D0C9BC12F82B3A1
3 changed files with 45 additions and 6 deletions

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"io" "io"
"os" "os"
"strings"
"sync" "sync"
"github.com/git-lfs/git-lfs/v3/config" "github.com/git-lfs/git-lfs/v3/config"
@ -69,7 +70,19 @@ func (c *singleCheckout) Run(p *lfs.WrappedPointer) {
// Check the content - either missing or still this pointer (not exist is ok) // Check the content - either missing or still this pointer (not exist is ok)
filepointer, err := lfs.DecodePointerFromFile(cwdfilepath) filepointer, err := lfs.DecodePointerFromFile(cwdfilepath)
if err != nil && !os.IsNotExist(err) { if err != nil {
if os.IsNotExist(err) {
output, err := git.DiffIndexWithPaths("HEAD", true, []string{p.Name})
if err != nil {
LoggedError(err, tr.Tr.Get("Checkout error trying to run diff-index: %s", err))
return
}
if strings.HasPrefix(output, ":100644 000000 ") || strings.HasPrefix(output, ":100755 000000 ") {
// This file is deleted in the index. Don't try
// to check it out.
return
}
} else {
if errors.IsNotAPointerError(err) || errors.IsBadPointerKeyError(err) { if errors.IsNotAPointerError(err) || errors.IsBadPointerKeyError(err) {
// File has non-pointer content, leave it alone // File has non-pointer content, leave it alone
return return
@ -78,6 +91,7 @@ func (c *singleCheckout) Run(p *lfs.WrappedPointer) {
LoggedError(err, tr.Tr.Get("Checkout error: %s", err)) LoggedError(err, tr.Tr.Get("Checkout error: %s", err))
return return
} }
}
if filepointer != nil && filepointer.Oid != p.Oid { if filepointer != nil && filepointer.Oid != p.Oid {
// User has probably manually reset a file to another commit // User has probably manually reset a file to another commit

@ -258,6 +258,23 @@ func DiffIndex(ref string, cached bool, refresh bool, workingDir string) (*bufio
return bufio.NewScanner(cmd.Stdout), nil return bufio.NewScanner(cmd.Stdout), nil
} }
func DiffIndexWithPaths(ref string, cached bool, paths []string) (string, error) {
args := []string{"diff-index"}
if cached {
args = append(args, "--cached")
}
args = append(args, ref)
args = append(args, "--")
args = append(args, paths...)
output, err := gitSimple(args...)
if err != nil {
return "", err
}
return output, nil
}
func HashObject(r io.Reader) (string, error) { func HashObject(r io.Reader) (string, error) {
cmd, err := gitNoLFS("hash-object", "--stdin") cmd, err := gitNoLFS("hash-object", "--stdin")
if err != nil { if err != nil {

@ -51,6 +51,14 @@ begin_test "checkout"
grep 'accepting "file1.dat"' checkout.log grep 'accepting "file1.dat"' checkout.log
grep 'rejecting "file1.dat"' checkout.log && exit 1 grep 'rejecting "file1.dat"' checkout.log && exit 1
git rm file1.dat
echo "checkout should skip replacing files deleted in index"
git lfs checkout
[ ! -f file1.dat ]
git reset --hard
# Remove the working directory # Remove the working directory
rm -rf file1.dat file2.dat file3.dat folder1/nested.dat folder2/nested.dat rm -rf file1.dat file2.dat file3.dat folder1/nested.dat folder2/nested.dat