aboutsummaryrefslogtreecommitdiff
path: root/internal/journal
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-09-15 00:00:05 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-09-15 00:00:05 +0200
commit5310c857d7531986c806404103f118b3c5e364f3 (patch)
tree4022cb838184c7e889793698a8a3cd3c4b6deebc /internal/journal
parent67ada0b5bc25cb6cff3ab780d82cb0bfe64e4968 (diff)
downloadkrino-5310c857d7531986c806404103f118b3c5e364f3.tar.gz
krino-5310c857d7531986c806404103f118b3c5e364f3.zip
krino log marks a run partly undone while reversible steps remain
Diffstat (limited to 'internal/journal')
-rw-r--r--internal/journal/read.go38
-rw-r--r--internal/journal/read_test.go48
2 files changed, 81 insertions, 5 deletions
diff --git a/internal/journal/read.go b/internal/journal/read.go
index 9099c0d..438e6ba 100644
--- a/internal/journal/read.go
+++ b/internal/journal/read.go
@@ -43,7 +43,19 @@ 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
+ // PartlyUndone is set with Undone while fewer of the run's reversible
+ // steps have been reversed, over all its undo runs, than it took
+ // (triage 34l): some were declined, refused or failed.
+ PartlyUndone bool
+ UndoOf string // for an undo run, the run it reverses; "" otherwise
+}
+
+// reversible names the undo action of each logged action undo can reverse;
+// delete (permanent) has none, and mkdir's removal is tidiness, not a
+// restoration (ranAnyUndoStep).
+var reversible = map[string]string{
+ "move": "undo-move", "rename": "undo-rename", "copy": "undo-copy",
+ "trash": "undo-trash", "displace": "undo-displace",
}
// ReversedKey identifies one reversal an undo run carried out: the file's
@@ -247,11 +259,19 @@ func Runs(path string, n int) ([]Run, error) {
// run-start line - and therefore its claim on pendingUndo - is always
// written before its own step entries, so whether it actually reversed
// anything cannot be known until its Counts are complete.
- undoes := make(map[string]bool) // run IDs actually reversed by some later run
+ undoes := make(map[string]bool) // run IDs actually reversed by some later run
+ reversed := make(map[string]int) // run ID -> reversals carried out by all its undo runs
for undoRun, orig := range pendingUndo {
- if r, ok := byID[undoRun]; ok && ranAnyUndoStep(r.Counts) {
+ r, ok := byID[undoRun]
+ if !ok {
+ continue
+ }
+ if ranAnyUndoStep(r.Counts) {
undoes[orig] = true
}
+ for _, undo := range reversible {
+ reversed[orig] += r.Counts[undo]
+ }
}
runs := make([]Run, len(order))
@@ -260,8 +280,16 @@ 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]
+ r := &runs[i]
+ r.Undone = undoes[r.ID]
+ r.UndoOf = pendingUndo[r.ID]
+ if r.Undone {
+ took := 0
+ for action := range reversible {
+ took += r.Counts[action]
+ }
+ r.PartlyUndone = reversed[r.ID] < took
+ }
}
if n > 0 && n < len(runs) {
diff --git a/internal/journal/read_test.go b/internal/journal/read_test.go
index 676b2bd..efb5a15 100644
--- a/internal/journal/read_test.go
+++ b/internal/journal/read_test.go
@@ -731,3 +731,51 @@ func TestEntriesRefusesALineCutInsideItsRunColumn(t *testing.T) {
t.Errorf("Entries = %+v, no error; a line cut inside its run column must refuse the run", got)
}
}
+
+// TestRunsMarksAPartlyUndoneRun: a run whose undo reversed some of its
+// reversible steps but not all is partly undone; once a later undo reverses
+// the rest, it is undone in full. A permanent delete counts toward neither
+// (triage 34l).
+func TestRunsMarksAPartlyUndoneRun(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: "old.pdf", Step: 1, Action: "delete", Status: "ok", Src: "/a/old.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", Dir: "dl", File: "y.pdf", Step: 1, Action: "undo-rename", Status: "declined", Src: "/a/z.pdf", Dst: "/a/y.pdf"},
+ {Time: t0.Add(time.Hour), Run: "B", Action: "run-end", Status: "ok"},
+ } {
+ w.Append(e)
+ }
+ runA := func() Run {
+ t.Helper()
+ runs, err := Runs(path, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, r := range runs {
+ if r.ID == "A" {
+ return r
+ }
+ }
+ t.Fatal("no run A")
+ return Run{}
+ }
+ if a := runA(); !a.Undone || !a.PartlyUndone {
+ t.Errorf("after one of two reversals: %+v, want undone, partly", a)
+ }
+ t2 := t0.Add(2 * time.Hour)
+ w.Append(Entry{Time: t2, Run: "C", Action: "run-start", Status: "ok", Detail: "undo of A"})
+ w.Append(Entry{Time: t2, Run: "C", Dir: "dl", File: "y.pdf", Step: 1, Action: "undo-rename", Status: "ok", Src: "/a/z.pdf", Dst: "/a/y.pdf"})
+ w.Append(Entry{Time: t2, Run: "C", Action: "run-end", Status: "ok"})
+ w.Close()
+ if a := runA(); !a.Undone || a.PartlyUndone {
+ t.Errorf("after both reversals: %+v, want undone in full", a)
+ }
+}