Commit Graph

33 Commits

Author SHA1 Message Date
b62b4f297f Stat in the walk, hash only shared sizes, flush batches mid-scan
Restructure scan into three phases: walk+stat, hash, update.

The stat pass is folded into the walk workers: each regular file is
lstatted as its directory is read, while the metadata is hot. The
walk builds a scan-wide size census (walked files plus records
outside the scan roots), and unchanged already-hashed files resolve
during the walk without further work.

Only files whose size at least one other file shares are ever read:
a size-unique file cannot be a duplicate, so it is recorded without
hashes (head and tail empty). When a later scan makes its size
shared, the file is hashed then, even if otherwise unchanged. report
excludes unhashed records; trees gives them a never-matching
signature so a tree containing one never compares equal to another.

Hashed records are committed in batched transactions while the hash
phase runs, so an interrupted scan keeps everything hashed so far
and the next run resumes cheaply. The hash phase total is exact,
giving a meaningful ETA.

Memory drops accordingly: the existing-record index holds only path,
size, mtime, and a hashed flag (no hash values); the walk carries one
small record per candidate file; overlapping operands are pruned up
front instead of deduplicating every walked path in a scan-wide set.
Files no bigger than one chunk are hashed with a single read.
2026-07-25 06:06:22 +07:00
340bdbe39e Merge branch 'make-default-target': plain make builds the binary
All checks were successful
check / check (push) Successful in 1m12s
2026-07-24 11:21:18 +07:00
a1c3b852c3 Make the binary the default Make target
All checks were successful
check / check (push) Successful in 4s
Plain make now builds sfdupes (previously the default was all =
check + build); make build remains as an alias, so the Dockerfile
and existing habits keep working. The sfdupes target is phony: go
build's own cache decides what to recompile.
2026-07-24 11:21:16 +07:00
9f03eb3e2a Merge branch 'scan-wide-phases': scan-wide phases, concurrent operands, batched updates
All checks were successful
check / check (push) Successful in 1m8s
2026-07-24 10:26:54 +07:00
3ecf73c80a Make phases scan-wide, walk operands concurrently, batch updates
All checks were successful
check / check (push) Successful in 5s
All PATH operands belong to a single scan: every operand seeds the
shared walk worker pool, and each pass (walk, stat, hash, update)
runs exactly once over the whole scan, so pass totals, percentages,
and ETAs are scan-global. The per-operand walk/hash/update cycles and
their stderr operand announcements are gone; duplicate paths from
overlapping operands are deduplicated before stat.

The update pass now commits in batched transactions (10k changes per
batch) instead of one scan-wide transaction: the filesystem is
authoritative and the database is an eventually-consistent reflection
of it, so scan-level atomicity buys nothing, while batches keep the
WAL small and let concurrent reports observe progress.
2026-07-24 10:26:52 +07:00
732fc351d7 Merge branch 'parallel-phases': sequential phases, parallelism within each
All checks were successful
check / check (push) Successful in 1m9s
2026-07-24 08:12:49 +07:00
1e7a519608 Split the stat pass back out of the walk
All checks were successful
check / check (push) Successful in 4s
Phases are strictly sequential again — walk, stat, hash, update per
operand — with parallelism only inside each phase. The walk
enumerates paths with per-directory workers (no lstat of file
entries); the stat pass lstats every collected path with per-file
workers, restoring its exact-total/ETA progress bar and per-file
parallelism inside wide flat directories.
2026-07-24 08:12:47 +07:00
09ff9b5f30 Merge branch 'scan-operand-progress': announce operands on stderr
All checks were successful
check / check (push) Successful in 1m15s
2026-07-24 07:52:55 +07:00
a0f0050ada Announce each operand on stderr before its passes
All checks were successful
check / check (push) Successful in 6s
With per-operand walk/hash/update cycles, a multi-operand invocation
(e.g. scan /srv/*) showed pass totals that looked like the whole
run's: an operator watching operand 3 of 14 hash 300k files concluded
the other 20M files were being skipped. Print the operand path and
its position before each cycle.
2026-07-24 07:52:54 +07:00
6a15b879de Merge branch 'parallel-walk': parallel per-directory walk, per-operand commits
All checks were successful
check / check (push) Successful in 1m10s
2026-07-24 07:44:28 +07:00
dced5cf0d2 Parallelize the walk and commit per operand
All checks were successful
check / check (push) Successful in 5s
Replace the single-goroutine WalkDir traversal with a per-directory
worker pool: workers read directories concurrently and lstat entries
while each directory is fresh in cache, recording size and mtime
during the walk. This folds the separate stat pass away (halving
metadata I/O per run) and overlaps metadata latency, which dominated
on busy pools — a sequential walk of a ~22M-file tree was observed
taking over 4 hours.

Each PATH operand now loads its scope, walks, hashes, and commits in
its own transaction, so an interrupted scan keeps every operand
completed so far; a later overlapping operand sees the records
committed by earlier ones and reuses them unchanged.
2026-07-24 07:44:18 +07:00
3ebf98940a Specify parallel per-directory walk and per-operand commits
The walk pass is a single goroutine; on a busy ZFS pool it manages
only a few thousand directory entries per second and takes hours at
~20M files. Respecify it as a worker-pool traversal that reads
directories concurrently and records size/mtime during the walk,
folding away the separate stat pass and halving metadata I/O. Each
PATH operand now commits in its own transaction so an interrupted
scan keeps the operands completed so far.
2026-07-24 06:28:12 +07:00
abea945730 Merge branch 'persistent-database': persistent SQLite scan database
All checks were successful
check / check (push) Successful in 1m2s
2026-07-24 03:08:58 +07:00
d8fbcb32c2 Implement persistent SQLite scan database
All checks were successful
check / check (push) Successful in 3s
scan now synchronizes a database that survives between runs
(SFDUPES_DATABASE, default /var/lib/sfdupes/db.sqlite) instead of
emitting a stream: operands are resolved to absolute paths, unchanged
files (same size, mtime not newer than recorded) are never re-read, new
and changed files are hashed, and records under the scanned operands
that were not verified this run are deleted; records outside the
operands are untouched. All changes commit in a single transaction, and
WAL journaling with a busy timeout keeps a report run during a cron
scan safe.

report and trees read the database (no positional arguments); the
NUL-terminated stream format, its parser, and the malformed-record
handling are gone. The driver is modernc.org/sqlite (pure Go), so
builds keep cgo disabled.
2026-07-24 03:08:49 +07:00
e0d578a707 Specify persistent SQLite scan database in README; plan in TODO.md
scan will maintain a persistent database of file signatures
(default /var/lib/sfdupes/db.sqlite, overridable via
SFDUPES_DATABASE) that survives between runs; rescans hash only new
or changed files (mtime/size) and remove records for vanished files,
so scan can be cronned daily. report and trees will read the
database instead of a scan stream.
2026-07-24 02:54:08 +07:00
90c9ef3546 Record remote setup and v0.0.1 tag in TODO.md
All checks were successful
check / check (push) Successful in 40s
2026-07-23 09:08:28 +07:00
13b9839e73 Disable background-session worktree isolation for this repo 2026-07-23 09:06:24 +07:00
4b1c3cbf70 Make scan paths required operands; add -x/--one-file-system
All checks were successful
check / check (push) Successful in 4s
scan now takes one or more PATH operands (directories or regular
files) via cobra flags instead of the -root flag with its /srv
default; invoking scan with no operand is a usage error and a
nonexistent operand is fatal. Filesystem boundaries are crossed by
default; the new -x/--one-file-system flag (GNU du/rsync convention)
stops the walk at each operand's filesystem, implemented by comparing
lstat device IDs with build-tagged helpers for darwin's int32 Dev.
Verified against a real mounted disk image: default crosses, -x does
not, --one-file-system is identical to -x.
v0.0.1
2026-07-23 08:55:17 +07:00
3391207f8d Check off completed repo policy compliance items in TODO.md 2026-07-23 07:59:12 +07:00
5b20171dbd Install pre-commit hook via make hooks; resolve shared hooks dir
The hooks target uses git rev-parse --git-common-dir so it works from
both the main checkout and linked worktrees.
2026-07-23 06:45:02 +07:00
375233fff4 Restructure README with required policy sections
Adds Description (name/purpose/category/license/author), Getting
Started, Rationale, TODO, License, and Author sections; the full
normative specification is preserved under Design. The stale non-goal
about having no git repository or CI is removed, and the Build section
now documents the Makefile targets.
2026-07-23 06:44:32 +07:00
b4d013cb34 Add hash-pinned Dockerfile, .dockerignore, and Gitea CI workflow
Multistage build per policy: a fail-fast lint stage on the pinned
golangci-lint image runs fmt-check and lint, the builder stage reuses
its linter binary (which also forces stage ordering), runs make check,
and builds; the runtime stage is pinned alpine with just the binary.
CI runs docker build . on push with the checkout action pinned by
commit SHA. All image references pinned by sha256 digest with
version/date comments.
2026-07-23 06:42:11 +07:00
7a6adae0d2 Add test suite covering parsing, grouping, digests, and scan passes
Covers splitNUL framing, parseScanStream (malformed records, tabs and
newlines in paths, unterminated trailing record), duplicate grouping
with ordering/tie-break/determinism, humanBytes units, Merkle digest
equality and name/content sensitivity, maximal-tree suppression
(including sibling and differing-parent cases), hashHeadTail edge
sizes and error paths, walkPass .zfs/symlink skipping, statPass
vanished files, and an end-to-end walk/stat/hash pipeline test of the
README smoke-test scenario. 64% statement coverage.
2026-07-23 05:42:38 +07:00
316ea9072d Rewrite Makefile with required policy targets
test/lint/fmt/fmt-check/check/docker/hooks plus build, all, and clean.
make check no longer builds the binary (it must not modify files); the
test target uses the 30s-timeout conditional -v rerun pattern. Version
is injected via -ldflags and exposed as sfdupes --version.
2026-07-23 05:37:29 +07:00
065a533224 Make code lint-clean under the canonical golangci-lint config
150 findings fixed: linter autofixes for whitespace style (wsl_v5,
nlreturn, noinlineerr), named returns removed, magic numbers replaced
with named constants, static errNotRegular error, explicit Close/Parse
error handling, unused cobra params renamed to _, and runReport/
runTrees/hashPass split into helpers to satisfy cyclop, gocognit, and
funlen. Behavior verified unchanged against the README smoke test.
2026-07-23 05:36:05 +07:00
733b33d9d1 Add canonical .golangci.yml 2026-07-22 22:24:04 +07:00
2512ae797b Add REPO_POLICIES.md from canonical source (2026-07-06) 2026-07-22 22:24:04 +07:00
ab8533ea58 Add MIT LICENSE 2026-07-22 22:24:04 +07:00
7a36d91d8a Add .editorconfig: 4-space default, tabs for Go and Makefile 2026-07-22 22:24:04 +07:00
8c4ec4bd44 Add .gitignore covering OS, editor, Go artifacts, secrets, and scan data 2026-07-22 22:24:04 +07:00
ecb272d6cc Add TODO.md with repo policy compliance audit 2026-07-22 22:21:41 +07:00
1fad4a7d1c Add sfdupes implementation: scan, report, and trees subcommands 2026-07-22 22:06:22 +07:00
dbc7f5a786 Add README: complete sfdupes specification 2026-07-22 22:06:22 +07:00