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
The Dockerfile gets a lint phase on the pinned golangci-lint v2.12.2
image and a test phase on the Debian Go image. The tests run as an
unprivileged user, because root reads a file with mode 0000 and the
permission test then fails. The build stage copies a file from each
phase, so no build finishes unless both pass, and it no longer runs
make check. script/lint and script/test each build their phase,
uncached and tagged; script/cibuild bootstraps, runs script/check, then
builds the image. script/bootstrap no longer installs golangci-lint.
README.md and TODO.md describe the new setup.
Model: opus-5-5
With --continue, check stopped at the first file whose content or
checksum attribute it could not read, and at the first directory it
could not list. Each of these now counts as failed, its error, which
names the path, goes to stderr, and the walk goes on; the run still
exits non-zero. The count that sizes the progress bar leaves such a
path out, so the bar stays. The walk and the count, shared with sum
and clear, take the continue setting; those commands pass false and
still stop at the first error. Without --continue the first such error
still stops the run, and the summary printed before it now counts an
unreadable checksum attribute as failed, as it already did for
unreadable content.
Model: opus-5-5
golangci-lint v2 ignores a top-level linters-settings block without a
warning, so limits such as the 88-column line length were never
applied. .golangci.yml is now the canonical copy from sneak/prompts,
verbatim, which keeps its settings under linters.settings and also
configures depguard and gomodguard_v2. golangci-lint is pinned at
v2.12.2 by commit in the Dockerfile and script/bootstrap. The long
lines in attrsum.go are rewrapped and one nolint directive moves to
its own line; behaviour is unchanged.
Model: opus-5-5
`make try` ran against a fixed directory on one person's machine. It
failed for anyone else, and where that directory existed it rewrote the
checksum attributes and modification times of real files.
It now writes three small files into a new directory from `mktemp -d`,
runs the same sequence of commands on it, and removes it with an EXIT
trap. All steps run in one shell, so the trap also removes the
directory when a step fails.
`TODO.md`: the `try` sentence leaves Next Step and is recorded under
Completed Steps.
Model: opus-5-5
attrsum --version now prints the git tag or short commit. make build
stamps it with -X from git describe, and the Dockerfile takes the VERSION
build argument when given, otherwise git describe --tags --always on the
.git in the build context, failing if .git is present and no version
comes out. A new .dockerignore, the canonical one, keeps .git/config out
of the context, and also this repo's host-built /attrsum. CI checks out
full history so it sees the 1.0.0 tag and stamps what a full clone does.
script/docker is replaced with the canonical copy.
Model: opus-5-5
Fix every golangci-lint finding under the repo's standard .golangci.yml
(from ~192 down to 0 under the pinned v2.10.1) without changing program
behavior:
- gochecknoglobals: replace the verbose/quiet/exclude* package globals with
an options struct threaded through the command implementations.
- err113: introduce package-level sentinel errors and wrap them with %w.
- errcheck: check or explicitly discard every previously unchecked error
(bar.Add/Finish, deferred Close, verbose writes).
- forbidigo: route verbose output through os.Stdout instead of fmt.Print*.
- noinlineerr / wsl_v5 / nlreturn / gofmt: split inline error checks and
normalize whitespace.
- complexity (cyclop/gocognit/nestif): extract small behavior-preserving
helpers (runOverPaths, countAndBar, walkSkip, clearOne, checkOne,
missingChecksum, reportCheck).
- mnd/lll/nonamedreturns/revive/modernize/nilnil: named constants, wrapped
lines, unnamed returns, doc comments, SplitSeq, non-(nil,nil) returns.
- paralleltest/thelper: mark tests parallel (now race-safe with no shared
globals) and add t.Helper(); probe the real xattr key so the guard is
accurate.
Also make the tests actually runnable in CI rather than skipping:
- move the xattr keys into the user.* namespace (user.berlin.sneak.app.*),
which Linux requires for regular-file xattrs; macOS treats the whole
string as an opaque name, so behavior is unchanged there.
- run the Docker builder's checks as an unprivileged user so the permission
tests are meaningful (root bypasses file mode bits).
Add the standard STRTA scaffold, mirroring the conformant Go repos:
- script/ POSIX-sh entrypoints (bootstrap, setup, projectname, test,
lint, fmt, fmt-check, check, docker, cibuild, precommit,
install-precommit).
- Makefile rewritten as thin shims: .PHONY plus the nine standard
targets each delegating to script/NAME; repo-specific build, clean,
and try targets retained.
- .golangci.yml matching the org-standard Go lint config.
- Dockerfile whose build runs make check then make build, so the image
fails on any check failure.
- .gitea/workflows/check.yml running script/cibuild.
make fmt-check and make test are green. make check is not yet green
because of pre-existing lint findings in the application code, which
are out of scope for this scaffold change; hence refs (not closes).
fileMultihash now returns the number of bytes actually read during
hashing. This ensures BytesProcessed reflects the true amount of
data processed, not a potentially stale size from the initial walk.
Replace the non-atomic 'bad' bool with atomic comparison of FilesFailed
count before and after the walk. This ensures consistent use of atomic
operations for all shared state and eliminates a potential race if
parallelism is added in the future.
- Check file mtime before and after hashing; error if they differ
- Store file's mtime as sumtime instead of wall-clock time
- Use fresh stat for BytesProcessed to get accurate count
This fixes a TOCTOU race where a file could be modified between
hashing and writing the xattr, resulting in a stale checksum.
It also makes sum update comparisons semantically correct by
comparing file mtime against stored mtime rather than wall-clock time.
countFiles and countFilesMultiple now return errors instead of silently
ignoring them. This ensures that issues like non-existent paths or
permission errors are reported early rather than showing a misleading
progress bar with 0 total.
Instead of creating a new progress bar for each path, count total files
across all paths upfront and use a single unified progress bar. This
provides clearer UX when processing multiple directories.
When using "-" to read paths from stdin, if stdin is empty or contains
only blank lines, return an explicit error instead of silently succeeding
with no work done.
filepath.Walk uses Lstat, so symlinks are reported with ModeSymlink set,
never ModeDir. The info.IsDir() check was always false, making the
filepath.SkipDir branch unreachable dead code.
- Add -q/--quiet flag to suppress all output except errors
- Add progress bar with 250ms refresh, file count, and ETA display
- Print summary report to stderr on completion (files processed, skipped, failed, bytes, duration)
- Support reading paths from stdin with "-" argument (e.g., find | attrsum sum add -)
- Update README with new features and updated TODO section
- Change sum add, sum update, check, and clear to accept 1+ paths
- Update README usage examples to show multiple path support
- Add TODO section with planned future improvements