Author SHA1 Message Date
sneak 988bf9937f Rewrite README.md sentence by sentence for a newcomer
check / check (push) Canceled after 0s
The old README was a specification written as one dense document:
sentences past 30 words, lists chained with commas, exceptions ahead
of the rules, and release history in the user text. This rewrite
leads with what the program is, what it is for and how to use it,
then gives a reference for the settings, each subcommand, the
database, the output formats, the progress display and the exit
codes. Non-goals come before the reference; the constraints, the
entrypoints and the build come last. Every statement was checked
against the code on next and against what the built binary prints on
a small tree. The section names the code comments cite are kept, and
three comments in main_test.go now cite Settings. TODO.md records the
step.

Model: fable-5-1
2026-10-26 08:15:26 +00:00
5 changed files with 888 additions and 967 deletions

No files matched your search

+872 -852
View File
File diff suppressed because it is too large. Load diff
+6 -5
View File
@@ -29,11 +29,12 @@
# Completed Steps # Completed Steps
- an operand under another is walked on its own when a directory between them, - `README.md` rewritten from scratch, sentence by sentence, as a newcomer reads
the inner operand included, is named `.zfs` or is a symlink. The walk of the it. It leads with what the program is and how to use it. A reference section
outer operand does not reach it then. `scan` now records the files below it covers the settings, the database, the output formats and the exit codes;
and no longer deletes their records (2026-10-26, development and testing come last. Every statement was checked against the
https://git.eeqj.de/sneak/sfdupes/issues/142) code on `next`. Three comments in `main_test.go` cite "Settings", where their
rules now live (2026-10-26, https://git.eeqj.de/sneak/sfdupes/issues/135)
- a file or directory removed while the scan runs is not an error, in the walk - a file or directory removed while the scan runs is not an error, in the walk
or in the hash phase. The scan prints no line for it and does not count it as or in the hash phase. The scan prints no line for it and does not count it as
+5 -5
View File
@@ -648,7 +648,7 @@ func TestRunUsageErrors(t *testing.T) {
} }
func TestRunScanRejectsWorkersBelowOne(t *testing.T) { func TestRunScanRejectsWorkersBelowOne(t *testing.T) {
// README §scan mode: --workers below 1 is a usage error reported in // README §Settings: --workers below 1 is a usage error reported in
// one line on stderr, before the scan opens the database. // one line on stderr, before the scan opens the database.
for _, workers := range []string{"0", "-1"} { for _, workers := range []string{"0", "-1"} {
t.Run(workers, func(t *testing.T) { t.Run(workers, func(t *testing.T) {
@@ -686,8 +686,8 @@ func TestRunScanRejectsWorkersBelowOne(t *testing.T) {
func TestRunHelp(t *testing.T) { func TestRunHelp(t *testing.T) {
t.Parallel() t.Parallel()
// README §Subcommands: help goes to stderr, exits 0, and leaves // README §Settings: help goes to stderr, exits 0, and leaves stdout
// stdout empty. // empty.
cases := [][]string{{"--help"}, {"-h"}, {cmdScan, "--help"}} cases := [][]string{{"--help"}, {"-h"}, {cmdScan, "--help"}}
for _, args := range cases { for _, args := range cases {
@@ -713,8 +713,8 @@ func TestRunHelp(t *testing.T) {
func TestRunVersion(t *testing.T) { func TestRunVersion(t *testing.T) {
t.Parallel() t.Parallel()
// README §Subcommands: the version is one line on stdout, with // README §Settings: the version is one line on stdout, with nothing
// nothing on stderr, and exits 0. // on stderr, and exits 0.
info, _ := debug.ReadBuildInfo() info, _ := debug.ReadBuildInfo()
for _, arg := range []string{"--version", "-v"} { for _, arg := range []string{"--version", "-v"} {
+5 -20
View File
@@ -48,11 +48,6 @@ const sampleWindow = 1024 * 1024
// and hash worker pools. // and hash worker pools.
const workQueueDepth = 1024 const workQueueDepth = 1024
// zfsDirName is the name of the directory in which ZFS shows a
// filesystem's snapshots. The walk never enters one, since it would
// list every file once per snapshot.
const zfsDirName = ".zfs"
// errInterrupted reports a scan stopped by SIGINT or SIGTERM. runScan // errInterrupted reports a scan stopped by SIGINT or SIGTERM. runScan
// has already printed its line, so run prints nothing more. // has already printed its line, so run prints nothing more.
var errInterrupted = errors.New("scan interrupted") var errInterrupted = errors.New("scan interrupted")
@@ -277,10 +272,9 @@ func pruneRoots(roots []string, oneFS bool, rules walkRules,
} }
// walkReaches reports whether the walk of operand above reaches r, an // walkReaches reports whether the walk of operand above reaches r, an
// operand under it. That walk never follows a symlink. It does not // operand under it. That walk does not enter a default exclude in
// enter a directory named .zfs, nor a default exclude in rules, nor a // rules, nor a directory that device puts on a filesystem in rules that
// directory that device puts on a filesystem in rules that is not // is not persistent, nor, with -x, one that device puts on a different
// persistent, nor, with -x, one that device puts on a different
// filesystem than above, when device knows above's. It reaches every // filesystem than above, when device knows above's. It reaches every
// regular file in a directory it enters, so for a regular-file operand // regular file in a directory it enters, so for a regular-file operand
// the directories checked start at its directory; they end below above. // the directories checked start at its directory; they end below above.
@@ -301,15 +295,6 @@ func walkReaches(above, r string, oneFS bool, rules walkRules,
return false return false
} }
if filepath.Base(dir) == zfsDirName {
return false
}
fi, err = os.Lstat(dir)
if err == nil && fi.Mode()&fs.ModeSymlink != 0 {
return false
}
dev, ok := device(dir) dev, ok := device(dir)
if !ok { if !ok {
continue continue
@@ -898,7 +883,7 @@ func operandWarning(root string, fi fs.FileInfo) string {
case mode.IsRegular(): case mode.IsRegular():
return "" return ""
case mode.IsDir(): case mode.IsDir():
if filepath.Base(root) != zfsDirName { if filepath.Base(root) != ".zfs" {
return "" return ""
} }
@@ -1168,7 +1153,7 @@ func subdirJob(ctx context.Context, p string, e fs.DirEntry,
parent dirJob, oneFS bool, rules walkRules, parent dirJob, oneFS bool, rules walkRules,
events chan<- walkEvent, events chan<- walkEvent,
) (dirJob, bool) { ) (dirJob, bool) {
if e.Name() == zfsDirName { if e.Name() == ".zfs" {
return dirJob{}, false return dirJob{}, false
} }
-85
View File
@@ -1672,81 +1672,6 @@ func TestSyncScanOverlappingRoots(t *testing.T) {
} }
} }
// TestScanWalksOperandInZFSDirectory checks that an operand inside a
// directory named .zfs below another operand is walked on its own,
// since the walk of the other operand does not enter .zfs. The file
// under it is recorded, and a second scan keeps the record.
func TestScanWalksOperandInZFSDirectory(t *testing.T) {
t.Parallel()
dir := t.TempDir()
db := openTestDB(t)
f := writeFile(t, dir, "pool/.zfs/snapshot/daily/f", pattern(1, 10))
daily := filepath.Dir(f)
syncTree(t, db, dir, daily)
recordByPath(t, dbRecords(t, db), f)
syncTree(t, db, dir, daily)
recordByPath(t, dbRecords(t, db), f)
}
// TestScanWalksOperandBehindSymlink checks that an operand reached
// through a symlinked directory below another operand is walked on its
// own, since the walk of the other operand does not follow the symlink.
// The file under it is recorded, and a second scan keeps the record.
func TestScanWalksOperandBehindSymlink(t *testing.T) {
t.Parallel()
dir := t.TempDir()
target := t.TempDir()
db := openTestDB(t)
writeFile(t, target, "sub/f", pattern(1, 10))
link := filepath.Join(dir, "link")
err := os.Symlink(target, link)
if err != nil {
t.Fatal(err)
}
sub := filepath.Join(link, "sub")
f := filepath.Join(sub, "f")
syncTree(t, db, dir, sub)
recordByPath(t, dbRecords(t, db), f)
syncTree(t, db, dir, sub)
recordByPath(t, dbRecords(t, db), f)
}
// TestScanDropsOperandReachedBehindSymlink checks that an operand under
// another one is still dropped when both lie behind the same symlinked
// directory: the walk of the other operand starts past the symlink and
// reaches it, so the file under it is walked once.
func TestScanDropsOperandReachedBehindSymlink(t *testing.T) {
t.Parallel()
dir := t.TempDir()
target := t.TempDir()
db := openTestDB(t)
writeFile(t, target, "sub/deeper/f", pattern(1, 10))
link := filepath.Join(dir, "link")
err := os.Symlink(target, link)
if err != nil {
t.Fatal(err)
}
sub := filepath.Join(link, "sub")
st := syncTree(t, db, sub, filepath.Join(sub, "deeper"))
if st != (scanStats{walked: 1, added: 1}) {
t.Fatalf("stats = %+v, want 1 walked and added", st)
}
}
// TestScanHashesUniqueSize checks that a file whose size no other file // TestScanHashesUniqueSize checks that a file whose size no other file
// shares is hashed like every other file. // shares is hashed like every other file.
func TestScanHashesUniqueSize(t *testing.T) { func TestScanHashesUniqueSize(t *testing.T) {
@@ -2160,16 +2085,6 @@ func TestPruneRoots(t *testing.T) {
t.Fatalf("pruneRoots with %s not persistent = %q, want %q", t.Fatalf("pruneRoots with %s not persistent = %q, want %q",
pqRoot, got, want) pqRoot, got, want)
} }
// Nor does it enter a directory named .zfs, so /p/.zfs/s is kept. The
// walk of /p/.zfs/s reaches /p/.zfs/s/t, which is dropped.
got = pruneRoots([]string{"/p", "/p/.zfs/s", "/p/.zfs/s/t"}, false,
walkRules{}, device)
want = []string{"/p", "/p/.zfs/s"}
if !slices.Equal(got, want) {
t.Fatalf("pruneRoots with .zfs = %q, want %q", got, want)
}
} }
func TestReportsNeverTouchFilesystem(t *testing.T) { func TestReportsNeverTouchFilesystem(t *testing.T) {