From a2cb20851499e9c00bd3bf642680a9ce3148cae8 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 17 Sep 2026 10:15:45 +0200 Subject: gui: name the other copy of a duplicate, and offer to keep this one instead --- internal/engine/facts.go | 12 ++++++++++++ internal/engine/match.go | 8 ++++++++ internal/engine/match_test.go | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+) (limited to 'internal/engine') diff --git a/internal/engine/facts.go b/internal/engine/facts.go index 641f1ef..ce05df4 100644 --- a/internal/engine/facts.go +++ b/internal/engine/facts.go @@ -158,6 +158,10 @@ type facts struct { // could not read), so (matched) is unknown while none has matched. undecided bool + // dupOriginal is the file a (duplicate) test last matched against, + // absolute, for a front end that offers to act on the other copy. + dupOriginal string + contentDone bool // extraction was attempted contentErr error // why it failed partialErr error // extract.ErrPartial: a keyword not found may be in the unread part @@ -290,9 +294,17 @@ func (f *facts) Duplicate(dirs []string) (string, bool, error) { if !isDup { return "", false, nil } + // The absolute path is kept for a front end that has to act on the + // other copy - the window offers to keep this one instead - while the + // reason text stays as it reads best (plan 21). + f.dupOriginal = orig return displayOriginal(orig, root), true, nil } +// DuplicateOriginal is the file the last (duplicate) test matched against, +// absolute; "" when none did. +func (f *facts) DuplicateOriginal() string { return f.dupOriginal } + // displayOriginal reports orig relative to root when it lies inside root, // else home-abbreviated (xdg.Abbrev), as every other user-visible path is. func displayOriginal(orig, root string) string { diff --git a/internal/engine/match.go b/internal/engine/match.go index 59cea42..25c05e5 100644 --- a/internal/engine/match.go +++ b/internal/engine/match.go @@ -46,6 +46,13 @@ type FileMatch struct { // directory's rules use, or that check failed. Only set for a file some // matching rule would delete. NoDelete string + + // DuplicateOf is the file a (duplicate) test matched this one against, + // absolute; "" when none did. The reason text says the same thing the + // way it reads best - relative to the directory when it is inside it - + // which leaves a front end no way to act on the other copy, or even to + // say where it is (his report, 2026-09-17). + DuplicateOf string } // Result is everything Match found in one directory. @@ -190,6 +197,7 @@ func evalFile(run *matchRun, file scan.File) FileMatch { if len(run.d.DupScopes) > 0 && deletes(fm.Rules) { fm.NoDelete = noDelete(f, run.d.DupScopes) } + fm.DuplicateOf = f.DuplicateOriginal() return fm } diff --git a/internal/engine/match_test.go b/internal/engine/match_test.go index 63bb54a..224f881 100644 --- a/internal/engine/match_test.go +++ b/internal/engine/match_test.go @@ -460,3 +460,37 @@ func TestRuleDuplicateWarningShortensThePath(t *testing.T) { t.Errorf("no shortened rule warning for a.txt: %+v %+v", r.Matched, r.Unmatched) } } + +// TestFileMatchNamesTheDuplicate: a file a (duplicate) test matched comes +// back with the other copy's absolute path, so a front end can say where it +// is and act on it - the reason text alone says "duplicate of NAME" for a +// copy inside the directory, which reads as no place at all (plan 21). +func TestFileMatchNamesTheDuplicate(t *testing.T) { + e, d, _ := fixture(t) + res, err := e.Match(context.Background(), d) + if err != nil { + t.Fatal(err) + } + found := false + for _, fm := range res.Matched { + if fm.File.Rel != "report (1).pdf" && fm.File.Rel != "report.pdf" { + continue + } + if fm.DuplicateOf == "" { + continue + } + found = true + if !filepath.IsAbs(fm.DuplicateOf) { + t.Errorf("%s: DuplicateOf = %q, want an absolute path", fm.File.Rel, fm.DuplicateOf) + } + if fm.DuplicateOf == fm.File.Path { + t.Errorf("%s: reported as a duplicate of itself", fm.File.Rel) + } + if _, err := os.Stat(fm.DuplicateOf); err != nil { + t.Errorf("%s: DuplicateOf does not exist: %v", fm.File.Rel, err) + } + } + if !found { + t.Fatal("neither copy came back with the file it duplicates") + } +} -- cgit v1.3