From 2a7452c3fd83000f239755825f38246f8537b6cc Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Tue, 15 Sep 2026 00:33:47 +0200 Subject: a permanently deleted file's steps do not keep a run partly undone --- docs/design.md | 3 ++- internal/journal/read.go | 18 ++++++++++++++++-- internal/journal/read_test.go | 31 +++++++++++++++++++++++++++++++ man/krino.1 | 3 ++- 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/docs/design.md b/docs/design.md index 744ef05..e8dd1af 100644 --- a/docs/design.md +++ b/docs/design.md @@ -664,7 +664,8 @@ not logged; declined files are. - `krino log` lists recent runs: id, time, directories, counts, and `(undone)` once undo has reversed every reversible step of a run, or - `(partly undone)` while some are not (declined, refused or failed). + `(partly undone)` while some are not (declined, refused or failed). A file + whose chain ends in a permanent delete is never undoable and not counted. - `krino undo` reverses the most recent run, in every directory it touched. An older run is undone by naming it: `krino undo RUN`. Undo runs cannot themselves be undone: naming one is refused. diff --git a/internal/journal/read.go b/internal/journal/read.go index 438e6ba..5a1de04 100644 --- a/internal/journal/read.go +++ b/internal/journal/read.go @@ -230,6 +230,12 @@ func Runs(path string, n int) ([]Run, error) { order := make([]string, 0) byID := make(map[string]*Run) pendingUndo := make(map[string]string) // undo run ID -> the run ID it claims to undo + // A file whose chain ended in a permanent delete is never undone, so its + // reversible steps do not count toward what a run took (plan 11 review + // L8). + type fileOf struct{ run, dir, file string } + reversibleOf := map[fileOf]int{} + deletedFile := map[fileOf]bool{} for _, line := range lines { e, ok := parseLine(line) @@ -247,6 +253,12 @@ func Runs(path string, n int) ([]Run, error) { } if e.Status == "ok" { r.Counts[e.Action]++ + if _, ok := reversible[e.Action]; ok { + reversibleOf[fileOf{e.Run, e.Dir, e.File}]++ + } + if e.Action == "delete" { + deletedFile[fileOf{e.Run, e.Dir, e.File}] = true + } } if e.Action == "run-start" { if orig, ok := strings.CutPrefix(e.Detail, undoOfPrefix); ok && orig != "" { @@ -285,8 +297,10 @@ func Runs(path string, n int) ([]Run, error) { r.UndoOf = pendingUndo[r.ID] if r.Undone { took := 0 - for action := range reversible { - took += r.Counts[action] + for f, n := range reversibleOf { + if f.run == r.ID && !deletedFile[f] { + took += n + } } r.PartlyUndone = reversed[r.ID] < took } diff --git a/internal/journal/read_test.go b/internal/journal/read_test.go index efb5a15..4f1c902 100644 --- a/internal/journal/read_test.go +++ b/internal/journal/read_test.go @@ -779,3 +779,34 @@ func TestRunsMarksAPartlyUndoneRun(t *testing.T) { t.Errorf("after both reversals: %+v, want undone in full", a) } } + +// TestRunsIgnoresAPermanentlyDeletedFilesSteps: a file whose chain ended in +// a permanent delete can never be undone, so its earlier steps do not keep +// the run partly undone forever (plan 11 review L8). +func TestRunsIgnoresAPermanentlyDeletedFilesSteps(t *testing.T) { + path := filepath.Join(t.TempDir(), "krino.log") + w, _ := Open(path) + t0 := time.Date(2026, 9, 11, 9, 0, 0, 0, time.UTC) + for _, e := range []Entry{ + {Time: t0, Run: "A", Action: "run-start", Status: "ok"}, + {Time: t0, Run: "A", Dir: "dl", File: "x.pdf", Step: 1, Action: "move", Status: "ok", Src: "/a/x.pdf", Dst: "/b/x.pdf"}, + {Time: t0, Run: "A", Dir: "dl", File: "y.pdf", Step: 1, Action: "rename", Status: "ok", Src: "/a/y.pdf", Dst: "/a/z.pdf"}, + {Time: t0, Run: "A", Dir: "dl", File: "y.pdf", Step: 2, Action: "delete", Status: "ok", Src: "/a/z.pdf"}, + {Time: t0, Run: "A", Action: "run-end", Status: "ok"}, + {Time: t0.Add(time.Hour), Run: "B", Action: "run-start", Status: "ok", Detail: "undo of A"}, + {Time: t0.Add(time.Hour), Run: "B", Dir: "dl", File: "x.pdf", Step: 1, Action: "undo-move", Status: "ok", Src: "/b/x.pdf", Dst: "/a/x.pdf"}, + {Time: t0.Add(time.Hour), Run: "B", Action: "run-end", Status: "ok"}, + } { + w.Append(e) + } + w.Close() + runs, err := Runs(path, 0) + if err != nil { + t.Fatal(err) + } + for _, r := range runs { + if r.ID == "A" && (!r.Undone || r.PartlyUndone) { + t.Errorf("run A = %+v; want undone in full: y.pdf could never be reversed", r) + } + } +} diff --git a/man/krino.1 b/man/krino.1 index aa981cc..be370cd 100644 --- a/man/krino.1 +++ b/man/krino.1 @@ -359,7 +359,8 @@ while some of its reversible steps are not reversed \(em declined during the undo's own review, refused or failed \(em and .Pq undone once every one is. -A permanent delete is not reversible and counts toward neither. +A file whose steps end in a permanent delete can never be undone, and none +of its steps count toward either. .Sh ENVIRONMENT .Bl -tag -width Ds .It Ev XDG_CONFIG_HOME -- cgit v1.3