diff options
| -rw-r--r-- | CHANGELOG.md | 2 | ||||
| -rw-r--r-- | cmd/krino/sort.go | 16 | ||||
| -rw-r--r-- | cmd/krino/sort_test.go | 29 | ||||
| -rw-r--r-- | docs/design.md | 4 |
4 files changed, 47 insertions, 4 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index af4124c..efd456d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,7 +53,7 @@ walk, now include it. - In a real run, a later directory is no longer blocked by paths an earlier one moved files away from, or planned and did not apply ("target exists"); - what an earlier directory did put somewhere stays protected from a later + where an earlier directory's files ended up stays protected from a later `overwrite`. - `krino explain` says when a duplicate's delete would be skipped, no longer removes a directory's unused keyword cache (it holds no lock), and walks diff --git a/cmd/krino/sort.go b/cmd/krino/sort.go index b516629..aeeb1d4 100644 --- a/cmd/krino/sort.go +++ b/cmd/krino/sort.go @@ -264,12 +264,26 @@ func cmdSort(g *globals, names []string, stdout, stderr io.Writer) int { } res, aerr := e.Apply(ctx, toApply, approved, j, run) if res != nil { + // Where files ended up: each copy, and the last place a + // move or rename put the file - not a path it passed + // through and left. for _, fr := range res.Files { + final := "" for _, sr := range fr.Steps { - if sr.Status == "ok" && sr.Dst != "" { + switch { + case sr.Status != "ok": + case sr.Step.Kind == plan.DeletePermanent: + final = "" // gone: nothing is left anywhere + case sr.Dst == "": + case sr.Step.Kind == plan.Copy: applied = append(applied, sr.Dst) + default: + final = sr.Dst } } + if final != "" { + applied = append(applied, final) + } } } if aerr != nil { diff --git a/cmd/krino/sort_test.go b/cmd/krino/sort_test.go index 071a64d..4f3e29f 100644 --- a/cmd/krino/sort_test.go +++ b/cmd/krino/sort_test.go @@ -366,3 +366,32 @@ func TestLaterDirectoryNeverOverwritesAnEarlierOnesResult(t *testing.T) { t.Errorf("the Trash holds %d entries; nothing should have been displaced", len(entries)) } } + +// TestOnlyWhereFilesEndedUpStaysClaimed: a path an earlier directory's file +// passed through and left - renamed, then moved on - is free for a later +// directory; only where files ended up stays claimed (plan 11 re-check). +func TestOnlyWhereFilesEndedUpStaysClaimed(t *testing.T) { + h := home(t) + old := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) + if code, _, errOut := runCLI(t, "init"); code != 0 { + t.Fatal(errOut) + } + for _, n := range []string{"a", "b"} { + p := filepath.Join(h, n, "x.pdf") + os.MkdirAll(filepath.Dir(p), 0o755) + os.WriteFile(p, []byte("from "+n), 0o644) + os.Chtimes(p, old, old) + if code, _, errOut := runCLI(t, "new", n, filepath.Join(h, n)); code != 0 { + t.Fatal(errOut) + } + } + dirs := filepath.Join(h, ".config", "krino", "dirs") + os.WriteFile(filepath.Join(dirs, "a.conf"), []byte("(path \"~/a\")\n(rule \"r\" (rename \"x-r.pdf\") (move \"~/Out\"))\n"), 0o644) + os.WriteFile(filepath.Join(dirs, "b.conf"), []byte("(path \"~/b\")\n(rule \"r\" (rename \"x-r.pdf\") (move \"~/a\"))\n"), 0o644) + if code, out, errOut := runCLI(t, "-y", "a", "b"); code != 0 { + t.Fatalf("exit %d\n%s\n%s", code, out, errOut) + } + if b, err := os.ReadFile(filepath.Join(h, "a", "x-r.pdf")); err != nil || string(b) != "from b" { + t.Errorf("a/x-r.pdf: %q, %v; want b's file under its planned name", b, err) + } +} diff --git a/docs/design.md b/docs/design.md index e8dd1af..bda6b58 100644 --- a/docs/design.md +++ b/docs/design.md @@ -478,8 +478,8 @@ next free name instead, because that path will hold the other step's output by the time either runs, and displacing it would destroy that step's result. A real run applies each directory before planning the next, so a later directory sees the earlier one's result on disk; of its claims only the paths -it actually put files at carry over, so a later `overwrite` never displaces -this run's own result. +its files ended up at (each copy, and a moved or renamed file's last place) +carry over, so a later `overwrite` never displaces this run's own result. A dry run (`-n`) applies nothing: its claim set spans every directory, so two configured directories cannot show the same final name, but each directory is otherwise planned as if no earlier one had been applied (a file the first |
