Repository: numtide/treefmt (go)

Issue:
`nil pointer dereference` when running --no-cache on removed, git-staged file
**Describe the bug**

`nil pointer dereference` when doing `treefmt --no-cache` after `rm` on a file that was staged in `git`.

**To Reproduce**

Steps to reproduce the behavior:

```sh
~
❯ mkdir test && cd test

~/test
❯ cat << EOF > treefmt.toml
[formatter.yaml]
command = "yamlfmt"
includes = ["*.yaml", "*.yml"]
EOF

~/test
❯ echo "key:  value" > test.yaml

~/test
❯ git init
Initialized empty Git repository in /home/will/test/.git/

test on  main [?]
❯ git add -A

test on  main [+]
❯ treefmt
WARN format: no formatter for path: treefmt.toml
traversed 2 files
emitted 2 files for processing
formatted 1 files (1 changed) in 2ms

test on  main [!+]
❯ treefmt --no-cache
WARN format: no formatter for path: treefmt.toml
traversed 2 files
emitted 2 files for processing
formatted 1 files (0 changed) in 2ms

test on  main [!+]
❯ rm test.yaml

test on  main [✘+]
❯ treefmt
treefmt: error: failed to generate change set: failed to walk path: lstat /home/will/test/test.yaml: no such file or directory

test on  main [✘+]
❯ treefmt --no-cache
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x18 pc=0x9970b6]

goroutine 23 [running]:
git.numtide.com/numtide/treefmt/cli.(*Format).Run.(*Format).walkFilesystem.func6.2(0xc0001ea5d0, {0x19?, 0x0?})
	git.numtide.com/numtide/treefmt/cli/format.go:241 +0x56
git.numtide.com/numtide/treefmt/walk.(*gitWalker).Walk(0xc0001f2160, {0xba6f58, 0xc000168b40}, 0xc0001f2180)
	git.numtide.com/numtide/treefmt/walk/git.go:64 +0x66a
git.numtide.com/numtide/treefmt/cli.(*Format).Run.(*Format).walkFilesystem.func6()
	git.numtide.com/numtide/treefmt/cli/format.go:235 +0x425
golang.org/x/sync/errgroup.(*Group).Go.func1()
	golang.org/x/sync@v0.7.0/errgroup/errgroup.go:78 +0x56
created by golang.org/x/sync/errgroup.(*Group).Go in goroutine 1
	golang.org/x/sync@v0.7.0/errgroup/errgroup.go:75 +0x96
```

**Expected behavior**

no crash

**System information**

Linux

```sh
$ treefmt --version
treefmt v2.0.3

$ yamlfmt --version
yamlfmt 0.13.0 (v0.13.0)

$ nixos-version
24.11.20240727.b73c222 (Vicuna)
```

**Additional context**

What I'm doing is weird, but I'm trying to narrow down another issue I'm having that might be related to https://github.com/numtide/treefmt/issues/343.


Pull request description:
fix(git): gracefully handle a file in the index but not in the filesystem
This can happen if a user removes a file from the filesystem and that change hasn't been staged yet.

We log a warning when this happens and continue.

Closes #364 



Patch:
diff --git a/walk/git.go b/walk/git.go
index 4dc9196..f0c5b52 100644
--- a/walk/git.go
+++ b/walk/git.go
@@ -13,6 +13,7 @@ import (
 )
 
 type gitWalker struct {
+	log           *log.Logger
 	root          string
 	paths         chan string
 	repo          *git.Repository
@@ -59,7 +60,11 @@ func (g gitWalker) Walk(ctx context.Context, fn WalkFunc) error {
 					path := filepath.Join(g.root, entry.Name)
 
 					info, err := os.Lstat(path)
-					if err != nil {
+					if os.IsNotExist(err) {
+						// the underlying file might have been removed without the change being staged yet
+						g.log.Warnf("Path %s is in the index but appears to have been removed from the filesystem", path)
+						continue
+					} else if err != nil {
 						return fmt.Errorf("failed to stat %s: %w", path, err)
 					}
 
@@ -136,6 +141,7 @@ func NewGit(root string, paths chan string) (Walker, error) {
 		return nil, fmt.Errorf("failed to open git repo: %w", err)
 	}
 	return &gitWalker{
+		log:           log.WithPrefix("walker[git]"),
 		root:          root,
 		paths:         paths,
 		repo:          repo,


Test patch:
diff --git a/cli/format_test.go b/cli/format_test.go
index 1d6d75d..b8532c3 100644
--- a/cli/format_test.go
+++ b/cli/format_test.go
@@ -530,14 +530,18 @@ func TestGitWorktree(t *testing.T) {
 	as.NoError(err)
 	run(32, 32, 32, 0)
 
-	// remove python directory
+	// remove python directory from the worktree
 	as.NoError(wt.RemoveGlob("python/*"))
 	run(28, 28, 28, 0)
 
+	// remove nixpkgs.toml from the filesystem but leave it in the index
+	as.NoError(os.Remove(filepath.Join(tempDir, "nixpkgs.toml")))
+	run(27, 27, 27, 0)
+
 	// walk with filesystem instead of git
 	_, err = cmd(t, "-c", "--config-file", configPath, "--tree-root", tempDir, "--walk", "filesystem")
 	as.NoError(err)
-	assertStats(t, as, 61, 61, 61, 0)
+	assertStats(t, as, 60, 60, 60, 0)
 }
 
 func TestPathsArg(t *testing.T) {