From d3299175ff52e9f02187329ff38e35edca298315 Mon Sep 17 00:00:00 2001 From: "brian m. carlson" Date: Tue, 2 Apr 2024 17:11:01 +0000 Subject: [PATCH] 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. --- commands/pull.go | 26 ++++++++++++++++++++------ git/git.go | 17 +++++++++++++++++ t/t-checkout.sh | 8 ++++++++ 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/commands/pull.go b/commands/pull.go index 75f8ee92c6..9c64798c4c 100644 --- a/commands/pull.go +++ b/commands/pull.go @@ -4,6 +4,7 @@ import ( "bytes" "io" "os" + "strings" "sync" "github.com/git-lfs/git-lfs/v3/config" @@ -69,14 +70,27 @@ func (c *singleCheckout) Run(p *lfs.WrappedPointer) { // Check the content - either missing or still this pointer (not exist is ok) filepointer, err := lfs.DecodePointerFromFile(cwdfilepath) - if err != nil && !os.IsNotExist(err) { - if errors.IsNotAPointerError(err) || errors.IsBadPointerKeyError(err) { - // File has non-pointer content, leave it alone + 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) { + // File has non-pointer content, leave it alone + return + } + + LoggedError(err, tr.Tr.Get("Checkout error: %s", err)) return } - - LoggedError(err, tr.Tr.Get("Checkout error: %s", err)) - return } if filepointer != nil && filepointer.Oid != p.Oid { diff --git a/git/git.go b/git/git.go index 40ea7d61be..9afe71a811 100644 --- a/git/git.go +++ b/git/git.go @@ -258,6 +258,23 @@ func DiffIndex(ref string, cached bool, refresh bool, workingDir string) (*bufio 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) { cmd, err := gitNoLFS("hash-object", "--stdin") if err != nil { diff --git a/t/t-checkout.sh b/t/t-checkout.sh index b7f74fc23e..727cbf5644 100755 --- a/t/t-checkout.sh +++ b/t/t-checkout.sh @@ -51,6 +51,14 @@ begin_test "checkout" grep 'accepting "file1.dat"' checkout.log 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 rm -rf file1.dat file2.dat file3.dat folder1/nested.dat folder2/nested.dat