aboutsummaryrefslogtreecommitdiff
path: root/cmd/krino
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/krino')
-rw-r--r--cmd/krino/history_test.go76
-rw-r--r--cmd/krino/undo.go6
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