Files
onyx-prebootstrap/internal/commands/undo.go
2025-10-09 19:19:31 -04:00

205 lines
5.0 KiB
Go

package commands
import (
"fmt"
"os"
"path/filepath"
"git.dws.rip/DWS/onyx/internal/core"
"git.dws.rip/DWS/onyx/internal/storage"
"github.com/spf13/cobra"
)
// NewUndoCmd creates the undo command
func NewUndoCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "undo",
Short: "Undo the last operation",
Long: `Undo the last operation by restoring the repository to its previous state.
This command reads the last entry from the oplog and restores all refs
and working directory state to what they were before the operation.`,
Args: cobra.NoArgs,
RunE: runUndo,
}
return cmd
}
func runUndo(cmd *cobra.Command, args []string) error {
// Get current directory
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get current directory: %w", err)
}
// Check if we're in an Onyx repository
if !core.IsOnyxRepo(cwd) {
return fmt.Errorf("not an onyx repository (or any parent up to mount point)")
}
// Open the repository
repo, err := core.Open(cwd)
if err != nil {
return fmt.Errorf("failed to open repository: %w", err)
}
defer repo.Close()
// Open oplog reader
oplogPath := filepath.Join(repo.GetOnyxPath(), "oplog")
reader := storage.NewOplogReader(oplogPath)
// Check if oplog is empty
isEmpty, err := reader.IsEmpty()
if err != nil {
return fmt.Errorf("failed to check oplog: %w", err)
}
if isEmpty {
return fmt.Errorf("nothing to undo")
}
// Read the last entry
lastEntry, err := reader.ReadLastEntry()
if err != nil {
return fmt.Errorf("failed to read last entry: %w", err)
}
// Check if we have state_before to restore
if lastEntry.StateBefore == nil {
return fmt.Errorf("cannot undo: last operation has no state_before")
}
// Show what we're undoing
fmt.Printf("Undoing: %s - %s\n", lastEntry.Operation, lastEntry.Description)
// Create state capture to restore the state
stateCapture := storage.NewStateCapture(repo.GetGitRepo())
// Restore the state
err = stateCapture.RestoreState(lastEntry.StateBefore)
if err != nil {
return fmt.Errorf("failed to restore state: %w", err)
}
// Log the undo operation
txn, err := core.NewTransaction(repo)
if err != nil {
// Don't fail if we can't create transaction, state is already restored
fmt.Fprintf(os.Stderr, "Warning: failed to log undo to oplog: %v\n", err)
} else {
defer txn.Close()
metadata := map[string]string{
"undone_entry_id": fmt.Sprintf("%d", lastEntry.ID),
"undone_operation": lastEntry.Operation,
}
err = txn.ExecuteWithTransactionAndMetadata(
"undo",
fmt.Sprintf("Undid operation: %s", lastEntry.Operation),
metadata,
func() error {
// The actual undo has already been performed above
// This function is just to capture the state after undo
return nil
},
)
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to log undo: %v\n", err)
}
}
// Show what changed
stateAfter, _ := stateCapture.CaptureState()
if stateAfter != nil {
differences := stateCapture.CompareStates(stateAfter, lastEntry.StateBefore)
if len(differences) > 0 {
fmt.Println("\nChanges:")
for ref, change := range differences {
fmt.Printf(" %s: %s\n", ref, change)
}
}
}
fmt.Println("\nUndo complete!")
return nil
}
// UndoToEntry undoes to a specific entry ID
func UndoToEntry(repo *core.OnyxRepository, entryID uint64) error {
oplogPath := filepath.Join(repo.GetOnyxPath(), "oplog")
reader := storage.NewOplogReader(oplogPath)
// Read the target entry
entry, err := reader.ReadEntry(entryID)
if err != nil {
return fmt.Errorf("failed to read entry %d: %w", entryID, err)
}
if entry.StateBefore == nil {
return fmt.Errorf("entry %d has no state_before to restore", entryID)
}
// Restore the state
stateCapture := storage.NewStateCapture(repo.GetGitRepo())
err = stateCapture.RestoreState(entry.StateBefore)
if err != nil {
return fmt.Errorf("failed to restore state: %w", err)
}
// Log the undo operation
txn, err := core.NewTransaction(repo)
if err != nil {
return fmt.Errorf("failed to create transaction: %w", err)
}
defer txn.Close()
metadata := map[string]string{
"undone_to_entry_id": fmt.Sprintf("%d", entryID),
"undone_operation": entry.Operation,
}
err = txn.ExecuteWithTransactionAndMetadata(
"undo",
fmt.Sprintf("Undid to entry %d: %s", entryID, entry.Operation),
metadata,
func() error {
return nil
},
)
if err != nil {
return fmt.Errorf("failed to log undo: %w", err)
}
return nil
}
// ListUndoStack shows the undo stack
func ListUndoStack(repo *core.OnyxRepository) error {
oplogPath := filepath.Join(repo.GetOnyxPath(), "oplog")
reader := storage.NewOplogReader(oplogPath)
entries, err := reader.GetUndoStack()
if err != nil {
return fmt.Errorf("failed to get undo stack: %w", err)
}
if len(entries) == 0 {
fmt.Println("Nothing to undo")
return nil
}
fmt.Println("Undo stack (most recent first):")
for i, entry := range entries {
fmt.Printf("%d. [%d] %s: %s (%s)\n",
i+1,
entry.ID,
entry.Operation,
entry.Description,
entry.Timestamp.Format("2006-01-02 15:04:05"),
)
}
return nil
}