scan: show each phase as N of M, with progress, elapsed time and ETA, also while it waits (closes #112)
check / check (push) Canceled after 0s

Every scan display now starts with its phase and its place among the
five, as "[2/5] walk", and always shows the rate, elapsed time and
ETA. load counts the records before reading them, and the content
phase counts what its search finds before checking it, so both have
totals; until then the line says what the phase waits for. The walk's
total is estimated from the records under the operands, with the ETA
unknown past the estimate or without one. Each display draws from its
own goroutine under one mutex: several times a second on a terminal,
every 5 seconds in plain mode. scanPhase holds the phase list.

Judgement call: on a terminal the line is the plain line behind the library's spinner; the bar graphic is gone.
Judgement call: the content phase runs its search query twice, once to count its rows.

Model: opus-5-5
This commit is contained in:
2026-10-16 13:48:19 +00:00
parent 264817c413
commit 39e576c276
8 changed files with 655 additions and 288 deletions
+26
View File
@@ -514,6 +514,18 @@ func loadDupeRows(ctx context.Context, db *sql.DB,
return records, nil
}
// countRecords returns the number of records in the database.
func countRecords(ctx context.Context, db *sql.DB) (int64, error) {
var n int64
err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM files").Scan(&n)
if err != nil {
return 0, fmt.Errorf("count records: %w", err)
}
return n, nil
}
// loadFileMeta streams every record's path, size, mtime, and whether
// it carries hashes to fn. Scan change detection needs no hash
// values, and skipping the hash columns keeps the scan's in-memory
@@ -572,6 +584,20 @@ JOIN (
ORDER BY size, head, tail
`
// countContentCandidates returns the number of rows of
// contentCandidatesSQL.
func countContentCandidates(ctx context.Context, db *sql.DB) (int64, error) {
var n int64
err := db.QueryRowContext(ctx,
"SELECT COUNT(*) FROM ("+contentCandidatesSQL+")", headTailMin).Scan(&n)
if err != nil {
return 0, fmt.Errorf("count records: %w", err)
}
return n, nil
}
// loadContentCandidates streams the rows of contentCandidatesSQL to fn:
// each record, without its content hash, and whether it has one.
func loadContentCandidates(ctx context.Context, db *sql.DB,