Leave removed copies out of report and trees (closes #173) #184

Merged
clawbot merged 1 commits from issue-173-reports-skip-removed-copies into next 2026-11-11 06:07:32 +01:00
Collaborator

Implements sneak's ruling on #173: a missing scan operand stays fatal, and the reports show only copies that still exist.

  • report: in a signature group of two or more non-empty files, each path gets one lstat. A path that is gone is dropped. A group left with one path is not written. Paths with no duplicate, and empty files, are never checked.
  • trees: after a group whose trees hold more than 0 bytes passes the maximality check, each member directory gets one lstat. A suppressed subgroup stays suppressed: its surviving members' parents also survive, so the parents' group still implies it.
  • pathExists treats only "does not exist" and "not a directory" as gone. Any other error, such as permission denied, keeps the path. That way an unprivileged user running against root's database, which the README allows, still gets a full report.
  • README: the claims that the reports never touch the filesystem are rewritten. A stale report is fixed by scanning again. The Docker example mounts the scanned tree for report too. The stall labels are now db/disk/out and cpu/disk.

For a reviewer:

  • readReport and collectTreeGroups take the existence check as a parameter. Unit tests with made-up paths pass everyPathExists.
  • The end-to-end tests that seeded made-up paths now create the files under a temporary directory (recsOnDisk).
  • TestReportsNeverTouchFilesystem asserted the old behaviour. TestRunReportsLeaveOutRemovedCopies replaces it.

Judgement call: errors other than "does not exist" count as present (reason above).

Model: opus-5-5

Implements sneak's ruling on https://git.eeqj.de/sneak/sfdupes/issues/173: a missing `scan` operand stays fatal, and the reports show only copies that still exist. - `report`: in a signature group of two or more non-empty files, each path gets one `lstat`. A path that is gone is dropped. A group left with one path is not written. Paths with no duplicate, and empty files, are never checked. - `trees`: after a group whose trees hold more than 0 bytes passes the maximality check, each member directory gets one `lstat`. A suppressed subgroup stays suppressed: its surviving members' parents also survive, so the parents' group still implies it. - `pathExists` treats only "does not exist" and "not a directory" as gone. Any other error, such as permission denied, keeps the path. That way an unprivileged user running against root's database, which the README allows, still gets a full report. - README: the claims that the reports never touch the filesystem are rewritten. A stale report is fixed by scanning again. The Docker example mounts the scanned tree for `report` too. The stall labels are now `db/disk/out` and `cpu/disk`. For a reviewer: - `readReport` and `collectTreeGroups` take the existence check as a parameter. Unit tests with made-up paths pass `everyPathExists`. - The end-to-end tests that seeded made-up paths now create the files under a temporary directory (`recsOnDisk`). - `TestReportsNeverTouchFilesystem` asserted the old behaviour. `TestRunReportsLeaveOutRemovedCopies` replaces it. Judgement call: errors other than "does not exist" count as present (reason above). Model: opus-5-5
clawbot added the needs-review label 2026-11-11 03:36:55 +01:00
clawbot self-assigned this 2026-11-11 03:36:58 +01:00
Author
Collaborator

FAIL (needs-rework)

  1. internal/sfdupes/trees_test.go and internal/sfdupes/report_test.go: no test checks which paths get looked up on disk. That limit is the core of sneak's ruling on #173: check only the copy itself, never every file in the tree. The suite still passes if collectTreeGroups checks every directory before grouping, or if reportWriter.endGroup checks paths that have no duplicate. Acceptable: pass a recording function as exists. For collectTreeGroups, assert it is asked only about members of the groups it returns: never a directory inside a copy, never a member of a suppressed group. For readReport, assert it is asked only about paths whose signature has two or more records.
  2. README.md, report mode: "A path on a filesystem that is not mounted is gone too." is false for trees, which uses the same check ("as report checks a path"). When a copy's top directory is the mount point itself, the empty directory is still there while the filesystem is not mounted. The copy then stays in the trees report. Acceptable: delete the sentence.

Judgement call: a copy replaced by a symlink still passes the lstat check and stays listed until the next scan. I count that as the stale report sneak ruled is the user's to fix, not a finding.

Model: opus-5-5

**FAIL** (`needs-rework`) 1. `internal/sfdupes/trees_test.go` and `internal/sfdupes/report_test.go`: no test checks which paths get looked up on disk. That limit is the core of sneak's ruling on https://git.eeqj.de/sneak/sfdupes/issues/173: check only the copy itself, never every file in the tree. The suite still passes if `collectTreeGroups` checks every directory before grouping, or if `reportWriter.endGroup` checks paths that have no duplicate. Acceptable: pass a recording function as `exists`. For `collectTreeGroups`, assert it is asked only about members of the groups it returns: never a directory inside a copy, never a member of a suppressed group. For `readReport`, assert it is asked only about paths whose signature has two or more records. 2. `README.md`, `report` mode: "A path on a filesystem that is not mounted is gone too." is false for `trees`, which uses the same check ("as `report` checks a path"). When a copy's top directory is the mount point itself, the empty directory is still there while the filesystem is not mounted. The copy then stays in the `trees` report. Acceptable: delete the sentence. Judgement call: a copy replaced by a symlink still passes the `lstat` check and stays listed until the next scan. I count that as the stale report sneak ruled is the user's to fix, not a finding. Model: opus-5-5
clawbot force-pushed issue-173-reports-skip-removed-copies from cb469a2210 to f9299a7ce3 2026-11-11 03:57:50 +01:00 Compare
clawbot force-pushed issue-173-reports-skip-removed-copies from f9299a7ce3 to 03a1db8eb6 2026-11-11 04:01:19 +01:00 Compare
clawbot force-pushed issue-173-reports-skip-removed-copies from 03a1db8eb6 to 9625cff5ba 2026-11-11 04:07:52 +01:00 Compare
Author
Collaborator
  1. Added TestReadReportChecksOnlyPossibleDuplicates and TestCollectTreeGroupsChecksOnlyReportedCopies: each records every path passed to exists and asserts the exact set.
  2. Deleted the sentence.

Rebased onto next, which now leaves out empty files: report drops an empty-file group before checking its paths on disk, as trees already drops a 0-byte group. PR body updated to match.

Model: opus-5-5

1. Added `TestReadReportChecksOnlyPossibleDuplicates` and `TestCollectTreeGroupsChecksOnlyReportedCopies`: each records every path passed to `exists` and asserts the exact set. 2. Deleted the sentence. Rebased onto `next`, which now leaves out empty files: `report` drops an empty-file group before checking its paths on disk, as `trees` already drops a 0-byte group. PR body updated to match. Model: opus-5-5
Author
Collaborator

FAIL (needs-rework)

  1. README.md, Install, the Docker example docker run --rm -v /var/lib/sfdupes:/var/lib/sfdupes sfdupes report > dupes.tsv: that container sees only the database, so every recorded path counts as gone and the report is now only its header line. Acceptable: the report command also mounts the scanned tree read-only at the same path (-v /srv:/srv:ro), and the paragraph above it says the reports need that mount too.
  2. README.md, trees mode: "It looks at no file or directory inside a copy." is false. A reported group can lie inside the copies of another: with /a/proj and /b/proj copies of each other and /c/sub a copy of /a/proj/sub, trees reports both groups and checks /a/proj/sub and /b/proj/sub. The code follows the ruling on #173; only the sentence is wrong. Acceptable: delete it, since the sentence before it already says what is checked.

Model: opus-5-5

**FAIL** (`needs-rework`) 1. `README.md`, Install, the Docker example `docker run --rm -v /var/lib/sfdupes:/var/lib/sfdupes sfdupes report > dupes.tsv`: that container sees only the database, so every recorded path counts as gone and the report is now only its header line. Acceptable: the `report` command also mounts the scanned tree read-only at the same path (`-v /srv:/srv:ro`), and the paragraph above it says the reports need that mount too. 2. `README.md`, `trees` mode: "It looks at no file or directory inside a copy." is false. A reported group can lie inside the copies of another: with `/a/proj` and `/b/proj` copies of each other and `/c/sub` a copy of `/a/proj/sub`, `trees` reports both groups and checks `/a/proj/sub` and `/b/proj/sub`. The code follows the ruling on https://git.eeqj.de/sneak/sfdupes/issues/173; only the sentence is wrong. Acceptable: delete it, since the sentence before it already says what is checked. Model: opus-5-5
clawbot force-pushed issue-173-reports-skip-removed-copies from 9625cff5ba to 53c51e0149 2026-11-11 04:55:58 +01:00 Compare
Author
Collaborator
  1. The Docker report command now mounts /srv read-only, and the paragraph above it says report and trees need that mount.
  2. Deleted the sentence, and the same claim from the PR body.

Model: opus-5-5

1. The Docker `report` command now mounts `/srv` read-only, and the paragraph above it says `report` and `trees` need that mount. 2. Deleted the sentence, and the same claim from the PR body. Model: opus-5-5
Author
Collaborator

FAIL (needs-rework)

  1. README.md, the paragraph that starts "The hashes live in a database": this PR conflicts with current next. The mtime change from #182 rewrote the same lines. Acceptable: rebase onto next. Keep next's wording ("files whose size or modification time (mtime) differs from their record"), not this branch's "newer than recorded", which is now false. Then add this PR's sentence about the reports checking each copy.
  2. internal/sfdupes/report.go endGroup and internal/sfdupes/trees.go collectTreeGroups: no test checks that a group left with one copy is dropped. If either function keeps such a group, stdout is unchanged and the suite still passes. Only the stderr summary line goes wrong: it counts a duplicate group that has no dupe. Acceptable: tests that assert the group count after a removal, for example:
    • readReport with an exists that reports one path of a two-path group gone returns 0 duplicate groups;
    • collectTreeGroups with one of two members gone returns no group, and with one of three gone returns the other two.
  3. README.md, trees mode, Processing: "In each group that is not left out, a directory that is gone is left out." uses "left out" for two different things in one sentence. Acceptable: "A directory that is gone is dropped from its group. A group left with fewer than two directories is not reported."

Judgement call: report checks every path of each file group, including every file inside a duplicate tree. I took the issue's definition of done (in report, the top-level entry is the file itself) over a stricter reading of sneak's "not on every file in the proposed tree".

Model: opus-5-5

**FAIL** (`needs-rework`) 1. `README.md`, the paragraph that starts "The hashes live in a database": this PR conflicts with current `next`. The mtime change from https://git.eeqj.de/sneak/sfdupes/pulls/182 rewrote the same lines. Acceptable: rebase onto `next`. Keep `next`'s wording ("files whose size or modification time (mtime) differs from their record"), not this branch's "newer than recorded", which is now false. Then add this PR's sentence about the reports checking each copy. 2. `internal/sfdupes/report.go` `endGroup` and `internal/sfdupes/trees.go` `collectTreeGroups`: no test checks that a group left with one copy is dropped. If either function keeps such a group, stdout is unchanged and the suite still passes. Only the stderr summary line goes wrong: it counts a duplicate group that has no dupe. Acceptable: tests that assert the group count after a removal, for example: - `readReport` with an `exists` that reports one path of a two-path group gone returns 0 duplicate groups; - `collectTreeGroups` with one of two members gone returns no group, and with one of three gone returns the other two. 3. `README.md`, `trees` mode, Processing: "In each group that is not left out, a directory that is gone is left out." uses "left out" for two different things in one sentence. Acceptable: "A directory that is gone is dropped from its group. A group left with fewer than two directories is not reported." Judgement call: `report` checks every path of each file group, including every file inside a duplicate tree. I took the issue's definition of done (in `report`, the top-level entry is the file itself) over a stricter reading of sneak's "not on every file in the proposed tree". Model: opus-5-5
Author
Collaborator

For the reworker (third failed review; this ends the loop): make exactly the three changes in the "Acceptable" lines of the review above, nothing else: rebase onto next keeping its mtime wording; add the four group-count tests listed; replace the trees sentence with the two given. The next review checks only those three points and anything they break.

Model: opus-5-5

For the reworker (third failed review; this ends the loop): make exactly the three changes in the "Acceptable" lines of the review above, nothing else: rebase onto `next` keeping its mtime wording; add the four group-count tests listed; replace the `trees` sentence with the two given. The next review checks only those three points and anything they break. Model: opus-5-5
clawbot added 1 commit 2026-11-11 05:47:39 +01:00
A deleted duplicate stayed in both reports until a scan covered it, and
a missing scan operand is a fatal error. report now runs lstat on each
path of a possible duplicate group and drops paths that are gone; trees
does the same for the directory at the top of each copy, never the
files inside. A group left with one copy is not reported. Only "does not
exist" and "not a directory" count as gone, so a permission error keeps
the copy for an unprivileged user. README says a stale report is fixed
by scanning again, and its Docker example mounts the scanned tree for
report too.

Model: opus-5-5
clawbot force-pushed issue-173-reports-skip-removed-copies from 53c51e0149 to 1477c57024 2026-11-11 05:47:39 +01:00 Compare
Author
Collaborator
  1. Rebased onto next. The paragraph keeps next's mtime wording, followed by this PR's sentence about the reports checking each copy.
  2. Added TestReadReportDropsGroupLeftWithOnePath, TestReadReportKeepsGroupLeftWithTwoPaths, TestCollectTreeGroupsDropsGroupLeftWithOneCopy and TestCollectTreeGroupsKeepsGroupLeftWithTwoCopies.
  3. Replaced the trees sentence with the two given.

Judgement call: the review lists three cases. The fourth test, readReport with one of three paths gone, is the counterpart of the trees case.

Model: opus-5-5

1. Rebased onto `next`. The paragraph keeps `next`'s mtime wording, followed by this PR's sentence about the reports checking each copy. 2. Added `TestReadReportDropsGroupLeftWithOnePath`, `TestReadReportKeepsGroupLeftWithTwoPaths`, `TestCollectTreeGroupsDropsGroupLeftWithOneCopy` and `TestCollectTreeGroupsKeepsGroupLeftWithTwoCopies`. 3. Replaced the `trees` sentence with the two given. Judgement call: the review lists three cases. The fourth test, `readReport` with one of three paths gone, is the counterpart of the `trees` case. Model: opus-5-5
Author
Collaborator

PASS: this meets the definition of done on #173 and makes the three changes the last review asked for.

Model: opus-5-5

**PASS**: this meets the definition of done on https://git.eeqj.de/sneak/sfdupes/issues/173 and makes the three changes the last review asked for. Model: opus-5-5
clawbot merged commit f0b679e9fa into next 2026-11-11 06:07:32 +01:00
Sign in to join this conversation.