[DEPRECATED] Go implementation of plcbundle

rollback cmd

+216 -1
+212
cmd/plcbundle/commands/rollback.go
··· 1 + package commands 2 + 3 + import ( 4 + "context" 5 + "flag" 6 + "fmt" 7 + "os" 8 + "path/filepath" 9 + "strings" 10 + 11 + "tangled.org/atscan.net/plcbundle/internal/bundleindex" 12 + ) 13 + 14 + // RollbackCommand handles the rollback subcommand 15 + func RollbackCommand(args []string) error { 16 + fs := flag.NewFlagSet("rollback", flag.ExitOnError) 17 + toBundle := fs.Int("to", 0, "rollback TO this bundle (keeps it, removes everything after)") 18 + last := fs.Int("last", 0, "rollback last N bundles") 19 + force := fs.Bool("force", false, "skip confirmation") 20 + rebuildDIDIndex := fs.Bool("rebuild-did-index", false, "rebuild DID index after rollback") 21 + 22 + if err := fs.Parse(args); err != nil { 23 + return err 24 + } 25 + 26 + if *toBundle == 0 && *last == 0 { 27 + return fmt.Errorf("usage: plcbundle rollback [options]\n\n" + 28 + "Options:\n" + 29 + " --to <N> Rollback TO bundle N (keeps N, removes after)\n" + 30 + " --last <N> Rollback last N bundles\n" + 31 + " --force Skip confirmation\n" + 32 + " --rebuild-did-index Rebuild DID index after rollback\n\n" + 33 + "Examples:\n" + 34 + " plcbundle rollback --to 100 # Keep bundles 1-100\n" + 35 + " plcbundle rollback --last 5 # Remove last 5 bundles\n" + 36 + " plcbundle rollback --to 50 --force --rebuild-did-index") 37 + } 38 + 39 + if *toBundle > 0 && *last > 0 { 40 + return fmt.Errorf("cannot use both --to and --last together") 41 + } 42 + 43 + mgr, dir, err := getManager("") 44 + if err != nil { 45 + return err 46 + } 47 + defer mgr.Close() 48 + 49 + index := mgr.GetIndex() 50 + bundles := index.GetBundles() 51 + 52 + if len(bundles) == 0 { 53 + fmt.Println("No bundles to rollback") 54 + return nil 55 + } 56 + 57 + // Determine target bundle 58 + var targetBundle int 59 + if *toBundle > 0 { 60 + targetBundle = *toBundle 61 + } else { 62 + targetBundle = bundles[len(bundles)-1].BundleNumber - *last 63 + } 64 + 65 + if targetBundle < 1 { 66 + return fmt.Errorf("invalid rollback: would result in no bundles (target: %d)", targetBundle) 67 + } 68 + 69 + // Find bundles to delete (everything AFTER target) 70 + var toDelete []*bundleindex.BundleMetadata 71 + var toKeep []*bundleindex.BundleMetadata 72 + 73 + for _, meta := range bundles { 74 + if meta.BundleNumber > targetBundle { 75 + toDelete = append(toDelete, meta) 76 + } else { 77 + toKeep = append(toKeep, meta) 78 + } 79 + } 80 + 81 + if len(toDelete) == 0 { 82 + fmt.Printf("Nothing to rollback (already at bundle %d)\n", targetBundle) 83 + return nil 84 + } 85 + 86 + // Display rollback plan 87 + fmt.Printf("╔════════════════════════════════════════════════════════╗\n") 88 + fmt.Printf("║ ROLLBACK PLAN ║\n") 89 + fmt.Printf("╚════════════════════════════════════════════════════════╝\n\n") 90 + fmt.Printf(" Directory: %s\n", dir) 91 + fmt.Printf(" Current state: %d bundles (%06d - %06d)\n", 92 + len(bundles), bundles[0].BundleNumber, bundles[len(bundles)-1].BundleNumber) 93 + fmt.Printf(" Target: bundle %06d\n", targetBundle) 94 + fmt.Printf(" Will DELETE: %d bundle(s)\n\n", len(toDelete)) 95 + 96 + fmt.Printf("Bundles to delete:\n") 97 + for i, meta := range toDelete { 98 + if i < 10 || i >= len(toDelete)-5 { 99 + fmt.Printf(" %06d (%s, %s)\n", 100 + meta.BundleNumber, 101 + meta.CreatedAt.Format("2006-01-02 15:04"), 102 + formatBytes(meta.CompressedSize)) 103 + } else if i == 10 { 104 + fmt.Printf(" ... (%d more bundles)\n", len(toDelete)-15) 105 + } 106 + } 107 + fmt.Printf("\n") 108 + 109 + // Calculate what will be deleted 110 + var deletedOps int 111 + var deletedSize int64 112 + for _, meta := range toDelete { 113 + deletedOps += meta.OperationCount 114 + deletedSize += meta.CompressedSize 115 + } 116 + 117 + fmt.Printf("⚠️ WARNING: This will delete:\n") 118 + fmt.Printf(" • %s operations\n", formatNumber(deletedOps)) 119 + fmt.Printf(" • %s of data\n", formatBytes(deletedSize)) 120 + fmt.Printf(" • Mempool will be cleared\n") 121 + if didStats := mgr.GetDIDIndexStats(); didStats["exists"].(bool) { 122 + fmt.Printf(" • DID index will be invalidated\n") 123 + } 124 + fmt.Printf("\n") 125 + 126 + if !*force { 127 + fmt.Printf("Type 'rollback' to confirm: ") 128 + var response string 129 + fmt.Scanln(&response) 130 + if strings.TrimSpace(response) != "rollback" { 131 + fmt.Println("Cancelled") 132 + return nil 133 + } 134 + } 135 + 136 + fmt.Printf("\n") 137 + 138 + // Step 1: Delete bundle files 139 + fmt.Printf("[1/4] Deleting bundle files...\n") 140 + deletedCount := 0 141 + for _, meta := range toDelete { 142 + bundlePath := filepath.Join(dir, fmt.Sprintf("%06d.jsonl.zst", meta.BundleNumber)) 143 + if err := os.Remove(bundlePath); err != nil { 144 + if !os.IsNotExist(err) { 145 + fmt.Printf(" ⚠️ Failed to delete %06d: %v\n", meta.BundleNumber, err) 146 + continue 147 + } 148 + } 149 + deletedCount++ 150 + } 151 + fmt.Printf(" ✓ Deleted %d bundle file(s)\n\n", deletedCount) 152 + 153 + // Step 2: Clear mempool 154 + fmt.Printf("[2/4] Clearing mempool...\n") 155 + if err := mgr.ClearMempool(); err != nil { 156 + fmt.Printf(" ⚠️ Warning: failed to clear mempool: %v\n", err) 157 + } else { 158 + fmt.Printf(" ✓ Mempool cleared\n\n") 159 + } 160 + 161 + // Step 3: Update bundle index 162 + fmt.Printf("[3/4] Updating bundle index...\n") 163 + index.Rebuild(toKeep) 164 + if err := mgr.SaveIndex(); err != nil { 165 + return fmt.Errorf("failed to save index: %w", err) 166 + } 167 + fmt.Printf(" ✓ Index updated (%d bundles)\n\n", len(toKeep)) 168 + 169 + // Step 4: Handle DID index 170 + fmt.Printf("[4/4] DID index...\n") 171 + didStats := mgr.GetDIDIndexStats() 172 + if didStats["exists"].(bool) { 173 + if *rebuildDIDIndex { 174 + fmt.Printf(" Rebuilding DID index...\n") 175 + ctx := context.Background() 176 + if err := mgr.BuildDIDIndex(ctx, func(current, total int) { 177 + if current%100 == 0 || current == total { 178 + fmt.Printf(" Progress: %d/%d (%.1f%%) \r", 179 + current, total, float64(current)/float64(total)*100) 180 + } 181 + }); err != nil { 182 + return fmt.Errorf("failed to rebuild DID index: %w", err) 183 + } 184 + fmt.Printf("\n ✓ DID index rebuilt\n") 185 + } else { 186 + // Just mark as needing rebuild 187 + fmt.Printf(" ⚠️ DID index is out of date\n") 188 + fmt.Printf(" Run: plcbundle index build\n") 189 + } 190 + } else { 191 + fmt.Printf(" (no DID index)\n") 192 + } 193 + 194 + fmt.Printf("\n") 195 + fmt.Printf("╔════════════════════════════════════════════════════════╗\n") 196 + fmt.Printf("║ ROLLBACK COMPLETE ║\n") 197 + fmt.Printf("╚════════════════════════════════════════════════════════╝\n\n") 198 + 199 + if len(toKeep) > 0 { 200 + lastBundle := toKeep[len(toKeep)-1] 201 + fmt.Printf(" New state:\n") 202 + fmt.Printf(" Bundles: %d (%06d - %06d)\n", 203 + len(toKeep), toKeep[0].BundleNumber, lastBundle.BundleNumber) 204 + fmt.Printf(" Chain head: %s\n", lastBundle.Hash[:16]+"...") 205 + fmt.Printf(" Last update: %s\n", lastBundle.EndTime.Format("2006-01-02 15:04:05")) 206 + } else { 207 + fmt.Printf(" State: empty (all bundles removed)\n") 208 + } 209 + 210 + fmt.Printf("\n") 211 + return nil 212 + }
+4 -1
cmd/plcbundle/main.go
··· 24 24 err = commands.FetchCommand(os.Args[2:]) 25 25 case "clone": 26 26 err = commands.CloneCommand(os.Args[2:]) 27 + case "rollback": 28 + err = commands.RollbackCommand(os.Args[2:]) 27 29 case "rebuild": 28 30 err = commands.RebuildCommand(os.Args[2:]) 29 31 case "verify": ··· 67 69 plcbundle <command> [options] 68 70 69 71 Commands: 70 - fetch Fetch next bundle from PLC directory 72 + fetch Fetch next bundle(s) from PLC directory 71 73 clone Clone bundles from remote HTTP endpoint 74 + rollback Rollback to previous bundle state 72 75 rebuild Rebuild index from existing bundle files 73 76 verify Verify bundle integrity 74 77 info Show bundle information