arche / internal/cli/cmd_undo.go

commit 154431fd
  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}