aboutsummaryrefslogtreecommitdiff
path: root/internal/journal
diff options
context:
space:
mode:
Diffstat (limited to 'internal/journal')
-rw-r--r--internal/journal/read.go37
-rw-r--r--internal/journal/read_test.go48
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)
+ }
+ }
+}