diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-14 21:33:39 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-14 21:33:39 +0200 |
| commit | 93cf0729ec7cf3129de0808cbcc3403b86d86fff (patch) | |
| tree | d04bf6825019b68a6e48f4b9e6a5dbdfa0e56c64 /internal/journal | |
| parent | 643eac4a2cba7b34b8a33af1b3dbdc54d9816a53 (diff) | |
| download | krino-93cf0729ec7cf3129de0808cbcc3403b86d86fff.tar.gz krino-93cf0729ec7cf3129de0808cbcc3403b86d86fff.zip | |
plan 9: an interrupted or failed undo can be finished
Diffstat (limited to 'internal/journal')
| -rw-r--r-- | internal/journal/read.go | 37 | ||||
| -rw-r--r-- | internal/journal/read_test.go | 48 |
2 files changed, 85 insertions, 0 deletions
diff --git a/internal/journal/read.go b/internal/journal/read.go index 3de15d2..de06422 100644 --- a/internal/journal/read.go +++ b/internal/journal/read.go @@ -43,6 +43,42 @@ type Run struct { Dirs []string Counts map[string]int // action -> count of status "ok" Undone bool // a later run reversed this one + UndoOf string // for an undo run, the run it reverses; "" otherwise +} + +// ReversedKey identifies one reversal an undo run carried out: the file's +// directory and name, the undo action and the path it started from - enough +// to tell which step of the original run it reversed. +type ReversedKey struct { + Dir, File, Action, Src string +} + +// ReversedSteps counts, for runID, every reversal that earlier undo runs of +// it completed ("ok" undo- entries of runs whose run-start says they undo +// runID), so a later undo of the same run can offer only what is left +// (review M10). An undo run's own unparsable lines are skipped; a missing +// reversal is then offered again, where its own checks refuse it if it had +// in fact happened. +func ReversedSteps(path, runID string) (map[ReversedKey]int, error) { + lines, err := readLines(path) + if err != nil { + return nil, err + } + undoRuns := map[string]bool{} + for _, line := range lines { + if e, ok := parseLine(line); ok && e.Action == "run-start" && e.Detail == UndoOf(runID) { + undoRuns[e.Run] = true + } + } + out := map[ReversedKey]int{} + for _, line := range lines { + e, ok := parseLine(line) + if !ok || !undoRuns[e.Run] || e.Status != "ok" || !strings.HasPrefix(e.Action, "undo-") { + continue + } + out[ReversedKey{Dir: e.Dir, File: e.File, Action: e.Action, Src: e.Src}]++ + } + return out, nil } // Entries returns every entry belonging to runID, in file order. A line @@ -198,6 +234,7 @@ func Runs(path string, n int) ([]Run, error) { sort.SliceStable(runs, func(i, j int) bool { return runs[i].Start.After(runs[j].Start) }) for i := range runs { runs[i].Undone = undoes[runs[i].ID] + runs[i].UndoOf = pendingUndo[runs[i].ID] } if n > 0 && n < len(runs) { diff --git a/internal/journal/read_test.go b/internal/journal/read_test.go index 2f0f40f..2b46bfb 100644 --- a/internal/journal/read_test.go +++ b/internal/journal/read_test.go @@ -625,3 +625,51 @@ func TestEntriesFailsClosedOnMissingRunStart(t *testing.T) { t.Errorf("entries = %+v", got) } } + +// TestReversedStepsCountsEveryUndoOfARun: the steps earlier undo runs of a +// run already reversed - only "ok" undo entries of runs that undo it - so +// a later undo of the same run can offer just what is left (review M10). +// Runs also names the run an undo run reversed. +func TestReversedStepsCountsEveryUndoOfARun(t *testing.T) { + path := filepath.Join(t.TempDir(), "krino.log") + w, err := Open(path) + if err != nil { + t.Fatal(err) + } + at := time.Date(2026, 9, 11, 9, 0, 0, 0, time.UTC) + for _, e := range []Entry{ + {Time: at, Run: "R", Action: "run-start", Status: "ok"}, + {Time: at, Run: "R", Dir: "dl", File: "a.pdf", Step: 1, Action: "move", Status: "ok", Src: "/dl/a.pdf", Dst: "/w/a.pdf"}, + {Time: at, Run: "R", Action: "run-end", Status: "ok"}, + {Time: at.Add(time.Minute), Run: "U1", Action: "run-start", Status: "ok", Detail: UndoOf("R")}, + {Time: at.Add(time.Minute), Run: "U1", Dir: "dl", File: "a.pdf", Step: 1, Action: "undo-move", Status: "ok", Src: "/w/a.pdf", Dst: "/dl/a.pdf"}, + {Time: at.Add(time.Minute), Run: "U1", Dir: "dl", File: "b.pdf", Step: 1, Action: "undo-rename", Status: "failed", Src: "/dl/r-b.pdf", Dst: "/dl/b.pdf"}, + {Time: at.Add(time.Minute), Run: "U1", Action: "run-end", Status: "ok"}, + {Time: at.Add(2 * time.Minute), Run: "U2", Action: "run-start", Status: "ok", Detail: UndoOf("OTHER")}, + {Time: at.Add(2 * time.Minute), Run: "U2", Dir: "dl", File: "a.pdf", Step: 1, Action: "undo-move", Status: "ok", Src: "/w/a.pdf", Dst: "/dl/a.pdf"}, + {Time: at.Add(2 * time.Minute), Run: "U2", Action: "run-end", Status: "ok"}, + } { + if err := w.Append(e); err != nil { + t.Fatal(err) + } + } + w.Close() + got, err := ReversedSteps(path, "R") + if err != nil { + t.Fatal(err) + } + want := map[ReversedKey]int{{Dir: "dl", File: "a.pdf", Action: "undo-move", Src: "/w/a.pdf"}: 1} + if len(got) != len(want) || got[ReversedKey{Dir: "dl", File: "a.pdf", Action: "undo-move", Src: "/w/a.pdf"}] != 1 { + t.Errorf("ReversedSteps = %v, want %v", got, want) + } + runs, err := Runs(path, 0) + if err != nil { + t.Fatal(err) + } + for _, r := range runs { + want := map[string]string{"R": "", "U1": "R", "U2": "OTHER"}[r.ID] + if r.UndoOf != want { + t.Errorf("run %s: UndoOf = %q, want %q", r.ID, r.UndoOf, want) + } + } +} |
