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 /cmd | |
| parent | 643eac4a2cba7b34b8a33af1b3dbdc54d9816a53 (diff) | |
| download | krino-93cf0729ec7cf3129de0808cbcc3403b86d86fff.tar.gz krino-93cf0729ec7cf3129de0808cbcc3403b86d86fff.zip | |
plan 9: an interrupted or failed undo can be finished
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/krino/history_test.go | 76 | ||||
| -rw-r--r-- | cmd/krino/undo.go | 6 |
2 files changed, 80 insertions, 2 deletions
diff --git a/cmd/krino/history_test.go b/cmd/krino/history_test.go index 5d86b6f..1a9ea2b 100644 --- a/cmd/krino/history_test.go +++ b/cmd/krino/history_test.go @@ -4,6 +4,7 @@ package main import ( "bytes" + "context" "fmt" "os" "path/filepath" @@ -12,6 +13,7 @@ import ( "time" "krino/internal/engine" + "krino/internal/journal" ) func TestLogListsRunsAndUndoReverses(t *testing.T) { @@ -45,8 +47,19 @@ func TestLogListsRunsAndUndoReverses(t *testing.T) { if _, out, _ = runCLI(t, "log"); !strings.Contains(out, "undone") { t.Errorf("log does not mark the run undone:\n%s", out) } - if code, _, errOut = runCLI(t, "undo", "-y"); code == 0 { - t.Errorf("undoing an undo run succeeded: %q", errOut) + // Plain undo after an undo continues the run it undid (review M10): + // everything came back, so nothing is left and nothing moves. + if code, out, errOut := runCLI(t, "undo", "-y"); code != 0 || !strings.Contains(out, "0 applied") { + t.Errorf("undo after a complete undo: %d\n%s\n%s", code, out, errOut) + } + if _, err := os.Stat(filepath.Join(h, "dl", "inv1.txt")); err != nil { + t.Errorf("the restored file moved: %v", err) + } + // Naming the undo run itself is still refused. + _, out, _ = runCLI(t, "log") + undoRun := strings.Fields(out)[0] + if code, _, errOut = runCLI(t, "undo", "-y", undoRun); code == 0 || !strings.Contains(errOut, "itself an undo") { + t.Errorf("undoing undo run %s: exit %d %q", undoRun, code, errOut) } } @@ -394,3 +407,62 @@ func TestGlobalYesBeforeUndo(t *testing.T) { t.Error("the filed copy survived -y undo") } } + +// TestUndoWithoutRunContinuesTheLastUndo: when the most recent run is an +// undo that could not finish, plain `krino undo` offers what that undo left +// instead of refusing because the last run is an undo (review M10). +func TestUndoWithoutRunContinuesTheLastUndo(t *testing.T) { + h := home(t) + dl := filepath.Join(h, "dl") + if err := os.MkdirAll(dl, 0o755); err != nil { + t.Fatal(err) + } + old := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) + p := filepath.Join(dl, "a.pdf") + if err := os.WriteFile(p, []byte("one"), 0o644); err != nil { + t.Fatal(err) + } + os.Chtimes(p, old, old) + if code, _, errOut := runCLI(t, "init"); code != 0 { + t.Fatal(errOut) + } + if code, _, errOut := runCLI(t, "new", "dl", dl); code != 0 { + t.Fatal(errOut) + } + rules := "(path \"~/dl\")\n(rule \"r\" (rename \"r-{name}\") (move \"Out\"))\n" + os.WriteFile(filepath.Join(h, ".config", "krino", "dirs", "dl.conf"), []byte(rules), 0o644) + if code, out, errOut := runCLI(t, "-y"); code != 0 { + t.Fatalf("sort: %d\n%s\n%s", code, out, errOut) + } + // An undo that fails part way: planned, then something takes the + // original name before it runs (driven through the engine, since the CLI + // plans and applies in one go). + e, errs := engine.Load(filepath.Join(h, ".config", "krino", "krino.conf")) + if len(errs) > 0 { + t.Fatal(errs) + } + runs, err := e.Runs(1) + if err != nil || len(runs) != 1 { + t.Fatalf("runs: %v %v", runs, err) + } + up, err := e.PlanUndo(runs[0].ID) + if err != nil { + t.Fatal(err) + } + os.WriteFile(p, []byte("in the way"), 0o644) + j, err := journal.Open(e.Config.LogFile()) + if err != nil { + t.Fatal(err) + } + time.Sleep(1100 * time.Millisecond) // run ids are per second + res, err := e.ApplyUndo(context.Background(), up, j, journal.NewRunID(time.Now())) + j.Close() + if err != nil || res.Failed != 1 { + t.Fatalf("blocked undo: %+v, %v", res, err) + } + os.Remove(p) + code, out, errOut := runCLI(t, "undo", "-n") + if code != 0 || !strings.Contains(out, "undo-rename") || strings.Contains(out, "undo-move") { + t.Fatalf("undo -n after a failed undo: exit %d\n%s\n%s", code, out, errOut) + } +} diff --git a/cmd/krino/undo.go b/cmd/krino/undo.go index b7a139f..c942ae1 100644 --- a/cmd/krino/undo.go +++ b/cmd/krino/undo.go @@ -97,6 +97,12 @@ func cmdUndo(g *globals, args []string, stdout, stderr io.Writer) int { return 0 } runID = runs[0].ID + // The most recent run is itself an undo: continue it, by planning the + // run it undid again - reversals it completed are not offered twice + // (review M10). Naming an undo run explicitly is still refused. + if runs[0].UndoOf != "" { + runID = runs[0].UndoOf + } } // PlanUndo only reads the log; nothing is touched yet (spec ยง10), which |
