From d4eafcf3e1a10ee61d7f2f50234d30c8a0dd0ea6 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Tue, 6 Oct 2026 03:58:49 +0000 Subject: [PATCH] Skip excluded paths before looking at their errors (closes #16) filepath.Walk hands its callback the error for a path it could not read, such as a directory it cannot list. Both callbacks, in walkAndProcess and countFiles, returned that error before they checked the exclusions, so a directory that --exclude or --exclude-dotfiles excluded still failed the run when it could not be listed. Given an error, each callback now checks the exclusions first and skips an excluded path. The new test excludes a directory that cannot be listed and runs sum add and check over its parent. Model: opus-5-5 --- TODO.md | 3 +++ attrsum.go | 13 ++++++++++++- attrsum_test.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/TODO.md b/TODO.md index da1244e..134766e 100644 --- a/TODO.md +++ b/TODO.md @@ -25,6 +25,9 @@ today), `.dockerignore`, `.gitea/workflows/check.yml` and # Completed Steps +* 2026-10-06: a path that `--exclude` or `--exclude-dotfiles` excludes + is skipped even when it cannot be read, so an excluded directory + that cannot be listed no longer fails the run * 2026-10-06: lint and test run as phases of the `Dockerfile`, and the build stage depends on both; `script/lint` and `script/test` each build their phase with `--no-cache`; `script/cibuild` bootstraps, diff --git a/attrsum.go b/attrsum.go index 5cfd6d9..7ec7499 100644 --- a/attrsum.go +++ b/attrsum.go @@ -613,6 +613,12 @@ func countFiles(opts *options, root string, cont bool) (int64, error) { root = filepath.Clean(root) err := filepath.Walk(root, func(p string, info os.FileInfo, err error) error { + // An excluded path is skipped whatever went wrong reading it. + rel, _ := filepath.Rel(root, p) + if err != nil && shouldExclude(opts, rel) { + return nil + } + if err != nil && cont { return nil } @@ -627,7 +633,6 @@ func countFiles(opts *options, root string, cont bool) (int64, error) { return nil } - rel, _ := filepath.Rel(root, p) if shouldExclude(opts, rel) { if info.IsDir() { return filepath.SkipDir @@ -699,6 +704,12 @@ func walkAndProcess( root = filepath.Clean(root) return filepath.Walk(root, func(p string, info os.FileInfo, err error) error { + // An excluded path is skipped whatever went wrong reading it. + rel, _ := filepath.Rel(root, p) + if err != nil && shouldExclude(opts, rel) { + return nil + } + if err != nil { return unreadable(cont, stats, err) } diff --git a/attrsum_test.go b/attrsum_test.go index 803acb7..c1e5db0 100644 --- a/attrsum_test.go +++ b/attrsum_test.go @@ -214,6 +214,54 @@ func TestExcludeDotfilesAndPatterns(t *testing.T) { } } +func TestExcludeUnreadableDir(t *testing.T) { + t.Parallel() + + opts := &options{excludePatterns: []string{"locked"}} + dir := t.TempDir() + skipIfNoXattr(t, dir) + + keep := writeFile(t, dir, "keep.txt", "keep") + hidden := writeFile(t, dir, "locked/a.txt", "hidden") + + // The excluded directory cannot be listed. + sub := filepath.Join(dir, "locked") + + err := os.Chmod(sub, noPerm) + if err != nil { + t.Fatalf("chmod dir: %v", err) + } + + defer func() { _ = os.Chmod(sub, dirPerm) }() + + err = processSumAdd(opts, dir, newTestStats(), nil) + if err != nil { + t.Fatalf("add: %v", err) + } + + _, err = xattr.Get(keep, checksumKey) + if err != nil { + t.Fatalf("expected xattr on keep.txt: %v", err) + } + + // Without --quiet, runCheck counts the files for the progress bar + // before it checks any, so this also covers the count. + err = runCheck(opts, []string{dir}, false) + if err != nil { + t.Fatalf("check: %v", err) + } + + err = os.Chmod(sub, dirPerm) + if err != nil { + t.Fatalf("chmod dir back: %v", err) + } + + _, err = xattr.Get(hidden, checksumKey) + if err == nil { + t.Fatalf("locked/a.txt should have been excluded") + } +} + func TestSkipBrokenSymlink(t *testing.T) { t.Parallel() -- 2.54.0