1package cli
2
3import (
4 "encoding/hex"
5 "encoding/json"
6 "fmt"
7 "os"
8 "time"
9
10 "arche/internal/object"
11 "arche/internal/repo"
12 "arche/internal/store"
13 "arche/internal/wc"
14
15 "github.com/spf13/cobra"
16)
17
18var undoStep int
19
20var undoCmd = &cobra.Command{
21 Use: "undo [--step N]",
22 Short: "Undo the last N operations",
23 Long: `Revert the repository to the state it was in before the last N operations.
24The undo itself is recorded in the operation log, so it is always undoable.
25Use 'arche op log' to see what operations can be undone.`,
26 RunE: func(cmd *cobra.Command, args []string) error {
27 r := openRepo()
28 defer r.Close()
29
30 ops, err := r.Store.ListOperations(undoStep + 1)
31 if err != nil {
32 return err
33 }
34 if len(ops) == 0 {
35 fmt.Println("Nothing to undo.")
36 return nil
37 }
38
39 var target *store.Operation
40 nonUndoCount := 0
41 for i := range ops {
42 if ops[i].Kind == "undo" {
43 continue
44 }
45 nonUndoCount++
46 if nonUndoCount == undoStep {
47 target = &ops[i]
48 break
49 }
50 }
51 if target == nil && len(ops) >= undoStep {
52 target = &ops[undoStep-1]
53 }
54 if target == nil {
55 fmt.Println("Not enough operations to undo.")
56 return nil
57 }
58
59 var beforeState repo.RefState
60 if err := json.Unmarshal([]byte(target.Before), &beforeState); err != nil {
61 return fmt.Errorf("parse before state: %w", err)
62 }
63
64 if beforeState.Head != "" {
65 if err := r.WriteHead(beforeState.Head); err != nil {
66 return fmt.Errorf("restore HEAD: %w", err)
67 }
68 }
69
70 after, _ := r.CaptureRefState()
71 tx, err := r.Store.Begin()
72 if err != nil {
73 return err
74 }
75
76 currentBMs, _ := r.Store.ListBookmarks()
77 for _, bm := range currentBMs {
78 if _, exists := beforeState.Bookmarks[bm.Name]; !exists {
79 if err := r.Store.DeleteBookmark(tx, bm.Name); err != nil {
80 r.Store.Rollback(tx)
81 return fmt.Errorf("delete bookmark %s: %w", bm.Name, err)
82 }
83 }
84 }
85 for name, hexID := range beforeState.Bookmarks {
86 raw, err := hex.DecodeString(hexID)
87 if err != nil || len(raw) != 32 {
88 r.Store.Rollback(tx)
89 return fmt.Errorf("invalid bookmark ID for %s: %s", name, hexID)
90 }
91 var id [32]byte
92 copy(id[:], raw)
93 if err := r.Store.SetBookmark(tx, store.Bookmark{Name: name, CommitID: id}); err != nil {
94 r.Store.Rollback(tx)
95 return fmt.Errorf("restore bookmark %s: %w", name, err)
96 }
97 }
98
99 op := store.Operation{
100 Kind: "undo",
101 Timestamp: time.Now().Unix(),
102 Before: after,
103 After: target.Before,
104 Metadata: fmt.Sprintf(`{"undid":%d}`, target.Seq),
105 }
106 if _, err := r.Store.InsertOperation(tx, op); err != nil {
107 r.Store.Rollback(tx)
108 return err
109 }
110 if err := r.Store.Commit(tx); err != nil {
111 return err
112 }
113
114 if beforeState.Head != "" {
115 bare := object.StripChangeIDPrefix(beforeState.Head)
116 if commitID, gcErr := r.Store.GetChangeCommit(bare); gcErr == nil {
117 if c, rcErr := r.ReadCommit(commitID); rcErr == nil {
118 if matErr := wc.New(r).MaterializeQuiet(c.TreeID); matErr != nil {
119 fmt.Fprintf(os.Stderr, "warning: could not materialize working copy: %v\n", matErr)
120 }
121 }
122 }
123 }
124
125 fmt.Printf("Undid operation #%d (%s %s)\n",
126 target.Seq, target.Kind,
127 time.Unix(target.Timestamp, 0).Format("2006-01-02 15:04:05"))
128 return nil
129 },
130}
131
132func init() {
133 undoCmd.Flags().IntVar(&undoStep, "step", 1, "number of operations to undo")
134}