diff options
Diffstat (limited to 'cmd/krino')
| -rw-r--r-- | cmd/krino/sort.go | 17 | ||||
| -rw-r--r-- | cmd/krino/sort_test.go | 33 |
2 files changed, 46 insertions, 4 deletions
diff --git a/cmd/krino/sort.go b/cmd/krino/sort.go index 637587d..f600475 100644 --- a/cmd/krino/sort.go +++ b/cmd/krino/sort.go @@ -119,10 +119,11 @@ func cmdSort(g *globals, names []string, stdout, stderr io.Writer) int { exit := 0 printed := false jsonDirs := []plan.JSONDir{} // never nil: the document's "dirs" must marshal as [], not null - // A3: one Claims for the whole run, shared across every directory's - // Plan call below, so two directories that both plan a move to the - // same destination resolve the collision at planning time instead of - // each independently believing it owns that path. + // A3: in a dry run one Claims is shared across every directory's Plan + // call below, so two directories that both plan a move to the same + // destination resolve the collision at planning time instead of each + // independently believing it owns that path. A real run applies each + // directory before planning the next and starts fresh claims after it. claims := plan.NewClaims() for _, d := range e.Dirs { @@ -281,6 +282,14 @@ func cmdSort(g *globals, names []string, stdout, stderr io.Writer) int { if quit { break } + if !g.dry { + // This directory is applied (or skipped) now, so the disk is the + // truth for the next one: its claims - sources it moved away, + // destinations it planned but declined or failed - must not + // block a later directory (triage 28i). A dry run applies + // nothing, so there they carry over. + claims = plan.NewClaims() + } } if g.json { diff --git a/cmd/krino/sort_test.go b/cmd/krino/sort_test.go index 12edb24..2a72e6a 100644 --- a/cmd/krino/sort_test.go +++ b/cmd/krino/sort_test.go @@ -232,3 +232,36 @@ func TestDryRunShowsNeverDeletedSkip(t *testing.T) { t.Errorf("explain should name the skipped delete for exactly one copy:\n%s\n%s", outA, outB) } } + +// TestLaterDirectoryIsNotBlockedByAnEarlierOnesClaims: in a real run each +// directory is applied before the next is planned, so the disk is the +// truth; a path an earlier directory moved a file away from, or planned and +// did not apply, is not "taken" for a later one (triage 28i). +func TestLaterDirectoryIsNotBlockedByAnEarlierOnesClaims(t *testing.T) { + h := home(t) + old := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) + for _, p := range []string{"ca/x.txt", "cb/x.txt"} { + full := filepath.Join(h, p) + os.MkdirAll(filepath.Dir(full), 0o755) + os.WriteFile(full, []byte(p), 0o644) + os.Chtimes(full, old, old) + } + if code, _, errOut := runCLI(t, "init"); code != 0 { + t.Fatal(errOut) + } + for _, n := range []string{"ca", "cb"} { + 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, "ca.conf"), []byte("(path \"~/ca\")\n(rule \"away\" (move \"~/elsewhere\"))\n"), 0o644) + os.WriteFile(filepath.Join(dirs, "cb.conf"), []byte("(path \"~/cb\")\n(on-conflict skip)\n(rule \"in\" (move \"~/ca\"))\n"), 0o644) + code, out, errOut := runCLI(t, "-y", "ca", "cb") + if code != 0 { + t.Fatalf("exit %d\n%s\n%s", code, out, errOut) + } + if b, err := os.ReadFile(filepath.Join(h, "ca", "x.txt")); err != nil || string(b) != "cb/x.txt" { + t.Errorf("cb's x.txt did not move into the place ca's left free: %q, %v\n%s", b, err, out) + } +} |
