tangled
alpha
login
or
join now
atscan.net
/
plcbundle-go
1
fork
atom
[DEPRECATED] Go implementation of plcbundle
1
fork
atom
overview
issues
pulls
pipelines
cmd rollback
tree.fail
4 months ago
0385bd08
0fd76d16
+485
-151
4 changed files
expand all
collapse all
unified
split
cmd
plcbundle
commands
common.go
diff.go
rollback.go
main.go
+25
-1
cmd/plcbundle/commands/common.go
···
30
30
RefreshMempool() error
31
31
ClearMempool() error
32
32
FetchNextBundle(ctx context.Context, quiet bool) (*bundle.Bundle, error)
33
33
-
SaveBundle(ctx context.Context, b *bundle.Bundle, quiet bool) (time.Duration, error) // ✨ Updated signature
33
33
+
SaveBundle(ctx context.Context, b *bundle.Bundle, quiet bool) (time.Duration, error)
34
34
+
SaveIndex() error
34
35
GetDIDIndexStats() map[string]interface{}
35
36
GetDIDIndex() *didindex.Manager
36
37
BuildDIDIndex(ctx context.Context, progress func(int, int)) error
···
212
213
fmt.Fprintln(os.Stderr, v...)
213
214
}
214
215
}
216
216
+
217
217
+
// formatCount formats count with color coding
218
218
+
func formatCount(count int) string {
219
219
+
if count == 0 {
220
220
+
return "\033[32m0 ✓\033[0m"
221
221
+
}
222
222
+
return fmt.Sprintf("\033[33m%d ⚠️\033[0m", count)
223
223
+
}
224
224
+
225
225
+
// formatCountCritical formats count with critical color coding
226
226
+
func formatCountCritical(count int) string {
227
227
+
if count == 0 {
228
228
+
return "\033[32m0 ✓\033[0m"
229
229
+
}
230
230
+
return fmt.Sprintf("\033[31m%d ✗\033[0m", count)
231
231
+
}
232
232
+
233
233
+
func min(a, b int) int {
234
234
+
if a < b {
235
235
+
return a
236
236
+
}
237
237
+
return b
238
238
+
}
-23
cmd/plcbundle/commands/diff.go
···
776
776
return len(ic.MissingBundles) > 0 || len(ic.ExtraBundles) > 0 ||
777
777
len(ic.HashMismatches) > 0 || len(ic.ContentMismatches) > 0
778
778
}
779
779
-
780
780
-
// formatCount formats count with color coding
781
781
-
func formatCount(count int) string {
782
782
-
if count == 0 {
783
783
-
return "\033[32m0 ✓\033[0m"
784
784
-
}
785
785
-
return fmt.Sprintf("\033[33m%d ⚠️\033[0m", count)
786
786
-
}
787
787
-
788
788
-
// formatCountCritical formats count with critical color coding
789
789
-
func formatCountCritical(count int) string {
790
790
-
if count == 0 {
791
791
-
return "\033[32m0 ✓\033[0m"
792
792
-
}
793
793
-
return fmt.Sprintf("\033[31m%d ✗\033[0m", count)
794
794
-
}
795
795
-
796
796
-
func min(a, b int) int {
797
797
-
if a < b {
798
798
-
return a
799
799
-
}
800
800
-
return b
801
801
-
}
+458
-125
cmd/plcbundle/commands/rollback.go
···
7
7
"path/filepath"
8
8
"strings"
9
9
10
10
-
flag "github.com/spf13/pflag"
11
11
-
10
10
+
"github.com/spf13/cobra"
11
11
+
"tangled.org/atscan.net/plcbundle/cmd/plcbundle/ui"
12
12
"tangled.org/atscan.net/plcbundle/internal/bundleindex"
13
13
)
14
14
15
15
-
// RollbackCommand handles the rollback subcommand
16
16
-
func RollbackCommand(args []string) error {
17
17
-
fs := flag.NewFlagSet("rollback", flag.ExitOnError)
18
18
-
toBundle := fs.Int("to", 0, "rollback TO this bundle (keeps it, removes everything after)")
19
19
-
last := fs.Int("last", 0, "rollback last N bundles")
20
20
-
force := fs.Bool("force", false, "skip confirmation")
21
21
-
rebuildDIDIndex := fs.Bool("rebuild-did-index", false, "rebuild DID index after rollback")
15
15
+
func NewRollbackCommand() *cobra.Command {
16
16
+
var (
17
17
+
toBundle int
18
18
+
last int
19
19
+
force bool
20
20
+
rebuildDIDIndex bool
21
21
+
keepFiles bool
22
22
+
)
23
23
+
24
24
+
cmd := &cobra.Command{
25
25
+
Use: "rollback [flags]",
26
26
+
Short: "Rollback repository to earlier state",
27
27
+
Long: `Rollback repository to an earlier bundle state
28
28
+
29
29
+
Removes bundles from the repository and resets the state to a specific
30
30
+
bundle. This is useful for:
31
31
+
• Fixing corrupted bundles
32
32
+
• Testing and development
33
33
+
• Reverting unwanted changes
34
34
+
• Chain integrity issues
35
35
+
36
36
+
IMPORTANT: This is a destructive operation that permanently deletes data.
37
37
+
Always verify your target bundle before proceeding.
38
38
+
39
39
+
The rollback process:
40
40
+
1. Validates target state
41
41
+
2. Shows detailed rollback plan
42
42
+
3. Requires confirmation (unless --force)
43
43
+
4. Deletes bundle files
44
44
+
5. Clears mempool (incompatible with new state)
45
45
+
6. Updates bundle index
46
46
+
7. Optionally rebuilds DID index`,
47
47
+
48
48
+
Example: ` # Rollback TO bundle 100 (keeps 1-100, removes 101+)
49
49
+
plcbundle rollback --to 100
22
50
23
23
-
if err := fs.Parse(args); err != nil {
24
24
-
return err
51
51
+
# Remove last 5 bundles
52
52
+
plcbundle rollback --last 5
53
53
+
54
54
+
# Rollback without confirmation
55
55
+
plcbundle rollback --to 50 --force
56
56
+
57
57
+
# Rollback and rebuild DID index
58
58
+
plcbundle rollback --to 100 --rebuild-did-index
59
59
+
60
60
+
# Rollback but keep bundle files (index-only)
61
61
+
plcbundle rollback --to 100 --keep-files`,
62
62
+
63
63
+
RunE: func(cmd *cobra.Command, args []string) error {
64
64
+
verbose, _ := cmd.Root().PersistentFlags().GetBool("verbose")
65
65
+
66
66
+
mgr, dir, err := getManagerFromCommand(cmd, "")
67
67
+
if err != nil {
68
68
+
return err
69
69
+
}
70
70
+
defer mgr.Close()
71
71
+
72
72
+
return executeRollback(cmd.Context(), mgr, dir, rollbackOptions{
73
73
+
toBundle: toBundle,
74
74
+
last: last,
75
75
+
force: force,
76
76
+
rebuildDIDIndex: rebuildDIDIndex,
77
77
+
keepFiles: keepFiles,
78
78
+
verbose: verbose,
79
79
+
})
80
80
+
},
25
81
}
26
82
27
27
-
if *toBundle == 0 && *last == 0 {
28
28
-
return fmt.Errorf("usage: plcbundle rollback [options]\n\n" +
29
29
-
"Options:\n" +
30
30
-
" --to <N> Rollback TO bundle N (keeps N, removes after)\n" +
31
31
-
" --last <N> Rollback last N bundles\n" +
32
32
-
" --force Skip confirmation\n" +
33
33
-
" --rebuild-did-index Rebuild DID index after rollback\n\n" +
34
34
-
"Examples:\n" +
35
35
-
" plcbundle rollback --to 100 # Keep bundles 1-100\n" +
36
36
-
" plcbundle rollback --last 5 # Remove last 5 bundles\n" +
37
37
-
" plcbundle rollback --to 50 --force --rebuild-did-index")
83
83
+
// Flags
84
84
+
cmd.Flags().IntVar(&toBundle, "to", 0, "Rollback TO this bundle (keeps it)")
85
85
+
cmd.Flags().IntVar(&last, "last", 0, "Rollback last N bundles")
86
86
+
cmd.Flags().BoolVarP(&force, "force", "f", false, "Skip confirmation prompt")
87
87
+
cmd.Flags().BoolVar(&rebuildDIDIndex, "rebuild-did-index", false, "Rebuild DID index after rollback")
88
88
+
cmd.Flags().BoolVar(&keepFiles, "keep-files", false, "Update index only (don't delete bundle files)")
89
89
+
90
90
+
return cmd
91
91
+
}
92
92
+
93
93
+
type rollbackOptions struct {
94
94
+
toBundle int
95
95
+
last int
96
96
+
force bool
97
97
+
rebuildDIDIndex bool
98
98
+
keepFiles bool
99
99
+
verbose bool
100
100
+
}
101
101
+
102
102
+
// rollbackPlan contains the calculated rollback plan
103
103
+
type rollbackPlan struct {
104
104
+
targetBundle int
105
105
+
toKeep []*bundleindex.BundleMetadata
106
106
+
toDelete []*bundleindex.BundleMetadata
107
107
+
deletedOps int
108
108
+
deletedSize int64
109
109
+
hasMempool bool
110
110
+
hasDIDIndex bool
111
111
+
affectedPeriod string
112
112
+
}
113
113
+
114
114
+
func executeRollback(ctx context.Context, mgr BundleManager, dir string, opts rollbackOptions) error {
115
115
+
// Step 1: Validate options and calculate plan
116
116
+
plan, err := calculateRollbackPlan(mgr, opts)
117
117
+
if err != nil {
118
118
+
return err
38
119
}
39
120
40
40
-
if *toBundle > 0 && *last > 0 {
41
41
-
return fmt.Errorf("cannot use both --to and --last together")
121
121
+
// Step 2: Display plan and get confirmation
122
122
+
displayRollbackPlan(dir, plan)
123
123
+
124
124
+
if !opts.force {
125
125
+
if !confirmRollback(opts.keepFiles) {
126
126
+
fmt.Println("Cancelled")
127
127
+
return nil
128
128
+
}
129
129
+
fmt.Println()
42
130
}
43
131
44
44
-
mgr, dir, err := getManager("")
45
45
-
if err != nil {
132
132
+
// Step 3: Execute rollback
133
133
+
if err := performRollback(ctx, mgr, dir, plan, opts); err != nil {
46
134
return err
47
135
}
48
48
-
defer mgr.Close()
136
136
+
137
137
+
// Step 4: Display success summary
138
138
+
displayRollbackSuccess(plan, opts)
139
139
+
140
140
+
return nil
141
141
+
}
142
142
+
143
143
+
// calculateRollbackPlan determines what will be affected
144
144
+
func calculateRollbackPlan(mgr BundleManager, opts rollbackOptions) (*rollbackPlan, error) {
145
145
+
// Validate options
146
146
+
if opts.toBundle == 0 && opts.last == 0 {
147
147
+
return nil, fmt.Errorf("either --to or --last must be specified")
148
148
+
}
149
149
+
150
150
+
if opts.toBundle > 0 && opts.last > 0 {
151
151
+
return nil, fmt.Errorf("cannot use both --to and --last together")
152
152
+
}
49
153
50
154
index := mgr.GetIndex()
51
155
bundles := index.GetBundles()
52
156
53
157
if len(bundles) == 0 {
54
54
-
fmt.Println("No bundles to rollback")
55
55
-
return nil
158
158
+
return nil, fmt.Errorf("no bundles to rollback")
56
159
}
57
160
58
58
-
// Determine target bundle
161
161
+
// Calculate target bundle
59
162
var targetBundle int
60
60
-
if *toBundle > 0 {
61
61
-
targetBundle = *toBundle
163
163
+
if opts.toBundle > 0 {
164
164
+
targetBundle = opts.toBundle
62
165
} else {
63
63
-
targetBundle = bundles[len(bundles)-1].BundleNumber - *last
166
166
+
targetBundle = bundles[len(bundles)-1].BundleNumber - opts.last
64
167
}
65
168
66
66
-
if targetBundle < 1 {
67
67
-
return fmt.Errorf("invalid rollback: would result in no bundles (target: %d)", targetBundle)
169
169
+
// Validate target
170
170
+
if targetBundle < 0 {
171
171
+
return nil, fmt.Errorf("invalid target: would result in negative bundle number")
68
172
}
69
173
70
70
-
// Find bundles to delete (everything AFTER target)
71
71
-
var toDelete []*bundleindex.BundleMetadata
72
72
-
var toKeep []*bundleindex.BundleMetadata
174
174
+
if targetBundle < 1 && len(bundles) > 0 {
175
175
+
return nil, fmt.Errorf("invalid rollback: would delete all bundles (use --to 0 explicitly if intended)")
176
176
+
}
177
177
+
178
178
+
// Build plan
179
179
+
plan := &rollbackPlan{
180
180
+
targetBundle: targetBundle,
181
181
+
toKeep: make([]*bundleindex.BundleMetadata, 0),
182
182
+
toDelete: make([]*bundleindex.BundleMetadata, 0),
183
183
+
}
73
184
74
185
for _, meta := range bundles {
75
186
if meta.BundleNumber > targetBundle {
76
76
-
toDelete = append(toDelete, meta)
187
187
+
plan.toDelete = append(plan.toDelete, meta)
188
188
+
plan.deletedOps += meta.OperationCount
189
189
+
plan.deletedSize += meta.CompressedSize
77
190
} else {
78
78
-
toKeep = append(toKeep, meta)
191
191
+
plan.toKeep = append(plan.toKeep, meta)
79
192
}
80
193
}
81
194
82
82
-
if len(toDelete) == 0 {
83
83
-
fmt.Printf("Nothing to rollback (already at bundle %d)\n", targetBundle)
84
84
-
return nil
195
195
+
// Check if nothing to do
196
196
+
if len(plan.toDelete) == 0 {
197
197
+
return nil, fmt.Errorf("already at bundle %d (nothing to rollback)", targetBundle)
85
198
}
86
199
87
87
-
// Display rollback plan
88
88
-
fmt.Printf("╔════════════════════════════════════════════════════════╗\n")
89
89
-
fmt.Printf("║ ROLLBACK PLAN ║\n")
90
90
-
fmt.Printf("╚════════════════════════════════════════════════════════╝\n\n")
91
91
-
fmt.Printf(" Directory: %s\n", dir)
92
92
-
fmt.Printf(" Current state: %d bundles (%06d - %06d)\n",
93
93
-
len(bundles), bundles[0].BundleNumber, bundles[len(bundles)-1].BundleNumber)
94
94
-
fmt.Printf(" Target: bundle %06d\n", targetBundle)
95
95
-
fmt.Printf(" Will DELETE: %d bundle(s)\n\n", len(toDelete))
200
200
+
// Check for mempool
201
201
+
mempoolStats := mgr.GetMempoolStats()
202
202
+
plan.hasMempool = mempoolStats["count"].(int) > 0
203
203
+
204
204
+
// Check for DID index
205
205
+
didStats := mgr.GetDIDIndexStats()
206
206
+
plan.hasDIDIndex = didStats["exists"].(bool)
96
207
97
97
-
fmt.Printf("Bundles to delete:\n")
98
98
-
for i, meta := range toDelete {
99
99
-
if i < 10 || i >= len(toDelete)-5 {
100
100
-
fmt.Printf(" %06d (%s, %s)\n",
208
208
+
// Calculate affected time period
209
209
+
if len(plan.toDelete) > 0 {
210
210
+
start := plan.toDelete[0].StartTime
211
211
+
end := plan.toDelete[len(plan.toDelete)-1].EndTime
212
212
+
plan.affectedPeriod = fmt.Sprintf("%s to %s",
213
213
+
start.Format("2006-01-02 15:04"),
214
214
+
end.Format("2006-01-02 15:04"))
215
215
+
}
216
216
+
217
217
+
return plan, nil
218
218
+
}
219
219
+
220
220
+
// displayRollbackPlan shows what will happen
221
221
+
func displayRollbackPlan(dir string, plan *rollbackPlan) {
222
222
+
fmt.Printf("╔════════════════════════════════════════════════════════════════╗\n")
223
223
+
fmt.Printf("║ ROLLBACK PLAN ║\n")
224
224
+
fmt.Printf("╚════════════════════════════════════════════════════════════════╝\n\n")
225
225
+
226
226
+
fmt.Printf("📁 Repository\n")
227
227
+
fmt.Printf(" Directory: %s\n", dir)
228
228
+
if len(plan.toKeep) > 0 {
229
229
+
fmt.Printf(" Current state: %d bundles (%06d → %06d)\n",
230
230
+
len(plan.toKeep)+len(plan.toDelete),
231
231
+
plan.toKeep[0].BundleNumber,
232
232
+
plan.toDelete[len(plan.toDelete)-1].BundleNumber)
233
233
+
}
234
234
+
fmt.Printf(" Target: bundle %06d\n\n", plan.targetBundle)
235
235
+
236
236
+
fmt.Printf("🗑️ Will Delete\n")
237
237
+
fmt.Printf(" Bundles: %d\n", len(plan.toDelete))
238
238
+
fmt.Printf(" Operations: %s\n", formatNumber(plan.deletedOps))
239
239
+
fmt.Printf(" Data size: %s\n", formatBytes(plan.deletedSize))
240
240
+
if plan.affectedPeriod != "" {
241
241
+
fmt.Printf(" Time period: %s\n", plan.affectedPeriod)
242
242
+
}
243
243
+
fmt.Printf("\n")
244
244
+
245
245
+
// Show sample of deleted bundles
246
246
+
if len(plan.toDelete) > 0 {
247
247
+
fmt.Printf(" Bundles to delete:\n")
248
248
+
displayCount := min(10, len(plan.toDelete))
249
249
+
for i := 0; i < displayCount; i++ {
250
250
+
meta := plan.toDelete[i]
251
251
+
fmt.Printf(" • %06d (%s, %s, %s ops)\n",
101
252
meta.BundleNumber,
102
253
meta.CreatedAt.Format("2006-01-02 15:04"),
103
103
-
formatBytes(meta.CompressedSize))
104
104
-
} else if i == 10 {
105
105
-
fmt.Printf(" ... (%d more bundles)\n", len(toDelete)-15)
254
254
+
formatBytes(meta.CompressedSize),
255
255
+
formatNumber(meta.OperationCount))
256
256
+
}
257
257
+
if len(plan.toDelete) > displayCount {
258
258
+
fmt.Printf(" ... and %d more\n", len(plan.toDelete)-displayCount)
106
259
}
260
260
+
fmt.Printf("\n")
261
261
+
}
262
262
+
263
263
+
// Show impacts
264
264
+
fmt.Printf("⚠️ Additional Impacts\n")
265
265
+
if plan.hasMempool {
266
266
+
fmt.Printf(" • Mempool will be cleared\n")
267
267
+
}
268
268
+
if plan.hasDIDIndex {
269
269
+
fmt.Printf(" • DID index will need rebuilding\n")
270
270
+
}
271
271
+
if len(plan.toKeep) == 0 {
272
272
+
fmt.Printf(" • Repository will be EMPTY after rollback\n")
107
273
}
108
274
fmt.Printf("\n")
275
275
+
}
109
276
110
110
-
// Calculate what will be deleted
111
111
-
var deletedOps int
112
112
-
var deletedSize int64
113
113
-
for _, meta := range toDelete {
114
114
-
deletedOps += meta.OperationCount
115
115
-
deletedSize += meta.CompressedSize
277
277
+
// confirmRollback prompts user for confirmation
278
278
+
func confirmRollback(keepFiles bool) bool {
279
279
+
if keepFiles {
280
280
+
fmt.Printf("Type 'rollback-index' to confirm (index-only mode): ")
281
281
+
} else {
282
282
+
fmt.Printf("⚠️ This will permanently DELETE data!\n")
283
283
+
fmt.Printf("Type 'rollback' to confirm: ")
116
284
}
117
285
118
118
-
fmt.Printf("⚠️ WARNING: This will delete:\n")
119
119
-
fmt.Printf(" • %s operations\n", formatNumber(deletedOps))
120
120
-
fmt.Printf(" • %s of data\n", formatBytes(deletedSize))
121
121
-
fmt.Printf(" • Mempool will be cleared\n")
122
122
-
if didStats := mgr.GetDIDIndexStats(); didStats["exists"].(bool) {
123
123
-
fmt.Printf(" • DID index will be invalidated\n")
286
286
+
var response string
287
287
+
fmt.Scanln(&response)
288
288
+
289
289
+
expectedResponse := "rollback"
290
290
+
if keepFiles {
291
291
+
expectedResponse = "rollback-index"
124
292
}
125
125
-
fmt.Printf("\n")
293
293
+
294
294
+
return strings.TrimSpace(response) == expectedResponse
295
295
+
}
126
296
127
127
-
if !*force {
128
128
-
fmt.Printf("Type 'rollback' to confirm: ")
129
129
-
var response string
130
130
-
fmt.Scanln(&response)
131
131
-
if strings.TrimSpace(response) != "rollback" {
132
132
-
fmt.Println("Cancelled")
133
133
-
return nil
297
297
+
// performRollback executes the rollback operations
298
298
+
func performRollback(ctx context.Context, mgr BundleManager, dir string, plan *rollbackPlan, opts rollbackOptions) error {
299
299
+
totalSteps := 4
300
300
+
currentStep := 0
301
301
+
302
302
+
// Step 1: Delete bundle files (or skip if keepFiles)
303
303
+
currentStep++
304
304
+
if !opts.keepFiles {
305
305
+
fmt.Printf("[%d/%d] Deleting bundle files...\n", currentStep, totalSteps)
306
306
+
if err := deleteBundleFiles(dir, plan.toDelete, opts.verbose); err != nil {
307
307
+
return fmt.Errorf("failed to delete bundles: %w", err)
134
308
}
309
309
+
fmt.Printf(" ✓ Deleted %d file(s)\n\n", len(plan.toDelete))
310
310
+
} else {
311
311
+
fmt.Printf("[%d/%d] Skipping file deletion (--keep-files)...\n", currentStep, totalSteps)
312
312
+
fmt.Printf(" ℹ Bundle files remain on disk\n\n")
135
313
}
136
314
137
137
-
fmt.Printf("\n")
315
315
+
// Step 2: Clear mempool
316
316
+
currentStep++
317
317
+
fmt.Printf("[%d/%d] Clearing mempool...\n", currentStep, totalSteps)
318
318
+
if err := clearMempool(mgr, plan.hasMempool); err != nil {
319
319
+
return err
320
320
+
}
321
321
+
fmt.Printf(" ✓ Mempool cleared\n\n")
138
322
139
139
-
// Step 1: Delete bundle files
140
140
-
fmt.Printf("[1/4] Deleting bundle files...\n")
323
323
+
// Step 3: Update index
324
324
+
currentStep++
325
325
+
fmt.Printf("[%d/%d] Updating bundle index...\n", currentStep, totalSteps)
326
326
+
if err := updateBundleIndex(mgr, plan); err != nil {
327
327
+
return err
328
328
+
}
329
329
+
fmt.Printf(" ✓ Index updated (%d bundles)\n\n", len(plan.toKeep))
330
330
+
331
331
+
// Step 4: Handle DID index
332
332
+
currentStep++
333
333
+
fmt.Printf("[%d/%d] DID index...\n", currentStep, totalSteps)
334
334
+
if err := handleDIDIndex(ctx, mgr, plan, opts); err != nil {
335
335
+
return err
336
336
+
}
337
337
+
338
338
+
return nil
339
339
+
}
340
340
+
341
341
+
// deleteBundleFiles removes bundle files from disk
342
342
+
func deleteBundleFiles(dir string, bundles []*bundleindex.BundleMetadata, verbose bool) error {
141
343
deletedCount := 0
142
142
-
for _, meta := range toDelete {
344
344
+
failedCount := 0
345
345
+
var firstError error
346
346
+
347
347
+
var progress *ui.ProgressBar
348
348
+
if !verbose && len(bundles) > 10 {
349
349
+
progress = ui.NewProgressBar(len(bundles))
350
350
+
}
351
351
+
352
352
+
for i, meta := range bundles {
143
353
bundlePath := filepath.Join(dir, fmt.Sprintf("%06d.jsonl.zst", meta.BundleNumber))
354
354
+
144
355
if err := os.Remove(bundlePath); err != nil {
145
356
if !os.IsNotExist(err) {
146
146
-
fmt.Printf(" ⚠️ Failed to delete %06d: %v\n", meta.BundleNumber, err)
357
357
+
failedCount++
358
358
+
if firstError == nil {
359
359
+
firstError = err
360
360
+
}
361
361
+
if verbose {
362
362
+
fmt.Printf(" ⚠️ Failed to delete %06d: %v\n", meta.BundleNumber, err)
363
363
+
}
147
364
continue
148
365
}
149
366
}
367
367
+
150
368
deletedCount++
369
369
+
370
370
+
if verbose {
371
371
+
fmt.Printf(" ✓ Deleted %06d (%s)\n",
372
372
+
meta.BundleNumber,
373
373
+
formatBytes(meta.CompressedSize))
374
374
+
}
375
375
+
376
376
+
if progress != nil {
377
377
+
progress.Set(i + 1)
378
378
+
}
151
379
}
152
152
-
fmt.Printf(" ✓ Deleted %d bundle file(s)\n\n", deletedCount)
380
380
+
381
381
+
if progress != nil {
382
382
+
progress.Finish()
383
383
+
}
384
384
+
385
385
+
if failedCount > 0 {
386
386
+
return fmt.Errorf("failed to delete %d bundles (deleted %d successfully): %w",
387
387
+
failedCount, deletedCount, firstError)
388
388
+
}
389
389
+
390
390
+
return nil
391
391
+
}
153
392
154
154
-
// Step 2: Clear mempool
155
155
-
fmt.Printf("[2/4] Clearing mempool...\n")
393
393
+
// clearMempool clears the mempool
394
394
+
func clearMempool(mgr BundleManager, hasMempool bool) error {
395
395
+
if !hasMempool {
396
396
+
return nil
397
397
+
}
398
398
+
156
399
if err := mgr.ClearMempool(); err != nil {
157
157
-
fmt.Printf(" ⚠️ Warning: failed to clear mempool: %v\n", err)
158
158
-
} else {
159
159
-
fmt.Printf(" ✓ Mempool cleared\n\n")
400
400
+
return fmt.Errorf("failed to clear mempool: %w", err)
160
401
}
161
402
162
162
-
// Step 3: Update bundle index
163
163
-
fmt.Printf("[3/4] Updating bundle index...\n")
164
164
-
index.Rebuild(toKeep)
403
403
+
return nil
404
404
+
}
405
405
+
406
406
+
// updateBundleIndex updates the index to reflect rollback
407
407
+
func updateBundleIndex(mgr BundleManager, plan *rollbackPlan) error {
408
408
+
index := mgr.GetIndex()
409
409
+
index.Rebuild(plan.toKeep)
410
410
+
165
411
if err := mgr.SaveIndex(); err != nil {
166
412
return fmt.Errorf("failed to save index: %w", err)
167
413
}
168
168
-
fmt.Printf(" ✓ Index updated (%d bundles)\n\n", len(toKeep))
414
414
+
415
415
+
return nil
416
416
+
}
417
417
+
418
418
+
// handleDIDIndex manages DID index state after rollback
419
419
+
func handleDIDIndex(ctx context.Context, mgr BundleManager, plan *rollbackPlan, opts rollbackOptions) error {
420
420
+
if !plan.hasDIDIndex {
421
421
+
fmt.Printf(" (no DID index)\n")
422
422
+
return nil
423
423
+
}
424
424
+
425
425
+
if opts.rebuildDIDIndex {
426
426
+
fmt.Printf(" Rebuilding DID index...\n")
427
427
+
428
428
+
bundleCount := len(plan.toKeep)
429
429
+
if bundleCount == 0 {
430
430
+
fmt.Printf(" ℹ No bundles to index\n")
431
431
+
return nil
432
432
+
}
433
433
+
434
434
+
var progress *ui.ProgressBar
435
435
+
if !opts.verbose {
436
436
+
progress = ui.NewProgressBar(bundleCount)
437
437
+
}
169
438
170
170
-
// Step 4: Handle DID index
171
171
-
fmt.Printf("[4/4] DID index...\n")
172
172
-
didStats := mgr.GetDIDIndexStats()
173
173
-
if didStats["exists"].(bool) {
174
174
-
if *rebuildDIDIndex {
175
175
-
fmt.Printf(" Rebuilding DID index...\n")
176
176
-
ctx := context.Background()
177
177
-
if err := mgr.BuildDIDIndex(ctx, func(current, total int) {
178
178
-
if current%100 == 0 || current == total {
179
179
-
fmt.Printf(" Progress: %d/%d (%.1f%%) \r",
180
180
-
current, total, float64(current)/float64(total)*100)
181
181
-
}
182
182
-
}); err != nil {
183
183
-
return fmt.Errorf("failed to rebuild DID index: %w", err)
439
439
+
err := mgr.BuildDIDIndex(ctx, func(current, total int) {
440
440
+
if progress != nil {
441
441
+
progress.Set(current)
442
442
+
} else if current%100 == 0 || current == total {
443
443
+
fmt.Printf(" Progress: %d/%d (%.1f%%) \r",
444
444
+
current, total, float64(current)/float64(total)*100)
184
445
}
185
185
-
fmt.Printf("\n ✓ DID index rebuilt\n")
186
186
-
} else {
187
187
-
// Just mark as needing rebuild
188
188
-
fmt.Printf(" ⚠️ DID index is out of date\n")
189
189
-
fmt.Printf(" Run: plcbundle index build\n")
446
446
+
})
447
447
+
448
448
+
if progress != nil {
449
449
+
progress.Finish()
190
450
}
451
451
+
452
452
+
if err != nil {
453
453
+
return fmt.Errorf("failed to rebuild DID index: %w", err)
454
454
+
}
455
455
+
456
456
+
fmt.Printf(" ✓ DID index rebuilt (%d bundles)\n", bundleCount)
191
457
} else {
192
192
-
fmt.Printf(" (no DID index)\n")
458
458
+
// Get DID index config to show which bundle it's at
459
459
+
didIndex := mgr.GetDIDIndex()
460
460
+
config := didIndex.GetConfig()
461
461
+
462
462
+
fmt.Printf(" ⚠️ DID index is out of date\n")
463
463
+
if config.LastBundle > plan.targetBundle {
464
464
+
fmt.Printf(" Index has bundle %06d, but repository now at %06d\n",
465
465
+
config.LastBundle, plan.targetBundle)
466
466
+
}
467
467
+
fmt.Printf(" Run: plcbundle index build\n")
193
468
}
194
469
470
470
+
return nil
471
471
+
}
472
472
+
473
473
+
// displayRollbackSuccess shows the final state
474
474
+
func displayRollbackSuccess(plan *rollbackPlan, opts rollbackOptions) {
195
475
fmt.Printf("\n")
196
196
-
fmt.Printf("╔════════════════════════════════════════════════════════╗\n")
197
197
-
fmt.Printf("║ ROLLBACK COMPLETE ║\n")
198
198
-
fmt.Printf("╚════════════════════════════════════════════════════════╝\n\n")
476
476
+
fmt.Printf("╔════════════════════════════════════════════════════════════════╗\n")
477
477
+
fmt.Printf("║ ROLLBACK COMPLETE ║\n")
478
478
+
fmt.Printf("╚════════════════════════════════════════════════════════════════╝\n\n")
199
479
200
200
-
if len(toKeep) > 0 {
201
201
-
lastBundle := toKeep[len(toKeep)-1]
202
202
-
fmt.Printf(" New state:\n")
203
203
-
fmt.Printf(" Bundles: %d (%06d - %06d)\n",
204
204
-
len(toKeep), toKeep[0].BundleNumber, lastBundle.BundleNumber)
205
205
-
fmt.Printf(" Chain head: %s\n", lastBundle.Hash[:16]+"...")
206
206
-
fmt.Printf(" Last update: %s\n", lastBundle.EndTime.Format("2006-01-02 15:04:05"))
480
480
+
if len(plan.toKeep) > 0 {
481
481
+
lastBundle := plan.toKeep[len(plan.toKeep)-1]
482
482
+
firstBundle := plan.toKeep[0]
483
483
+
484
484
+
fmt.Printf("📦 New State\n")
485
485
+
fmt.Printf(" Bundles: %d (%06d → %06d)\n",
486
486
+
len(plan.toKeep),
487
487
+
firstBundle.BundleNumber,
488
488
+
lastBundle.BundleNumber)
489
489
+
490
490
+
totalOps := 0
491
491
+
totalSize := int64(0)
492
492
+
for _, meta := range plan.toKeep {
493
493
+
totalOps += meta.OperationCount
494
494
+
totalSize += meta.CompressedSize
495
495
+
}
496
496
+
497
497
+
fmt.Printf(" Operations: %s\n", formatNumber(totalOps))
498
498
+
fmt.Printf(" Data size: %s\n", formatBytes(totalSize))
499
499
+
fmt.Printf(" Chain head: %s...\n", lastBundle.Hash[:16])
500
500
+
fmt.Printf(" Last updated: %s\n", lastBundle.EndTime.Format("2006-01-02 15:04:05"))
207
501
} else {
208
208
-
fmt.Printf(" State: empty (all bundles removed)\n")
502
502
+
fmt.Printf("📦 New State\n")
503
503
+
fmt.Printf(" Repository: EMPTY (all bundles removed)\n")
504
504
+
if opts.keepFiles {
505
505
+
fmt.Printf(" Note: Bundle files remain on disk\n")
506
506
+
}
209
507
}
210
508
211
509
fmt.Printf("\n")
510
510
+
511
511
+
// Show what was removed
512
512
+
fmt.Printf("🗑️ Removed\n")
513
513
+
fmt.Printf(" Bundles: %d\n", len(plan.toDelete))
514
514
+
fmt.Printf(" Operations: %s\n", formatNumber(plan.deletedOps))
515
515
+
fmt.Printf(" Data freed: %s\n", formatBytes(plan.deletedSize))
516
516
+
517
517
+
if opts.keepFiles {
518
518
+
fmt.Printf(" Files: kept on disk\n")
519
519
+
}
520
520
+
521
521
+
fmt.Printf("\n")
522
522
+
523
523
+
// Next steps
524
524
+
if !opts.rebuildDIDIndex && plan.hasDIDIndex {
525
525
+
fmt.Printf("💡 Next Steps\n")
526
526
+
fmt.Printf(" DID index is out of date. Rebuild with:\n")
527
527
+
fmt.Printf(" plcbundle index build\n\n")
528
528
+
}
529
529
+
}
530
530
+
531
531
+
// Validation helpers
532
532
+
533
533
+
// validateRollbackSafety performs additional safety checks
534
534
+
func validateRollbackSafety(mgr BundleManager, plan *rollbackPlan) error {
535
535
+
// Check for chain integrity issues
536
536
+
if len(plan.toKeep) > 1 {
537
537
+
// Verify the target bundle exists and has valid hash
538
538
+
lastKeep := plan.toKeep[len(plan.toKeep)-1]
539
539
+
if lastKeep.Hash == "" {
540
540
+
return fmt.Errorf("target bundle %06d has no chain hash - may be corrupted",
541
541
+
lastKeep.BundleNumber)
542
542
+
}
543
543
+
}
544
544
+
212
545
return nil
213
546
}
+2
-2
cmd/plcbundle/main.go
···
49
49
/*cmd.AddCommand(commands.NewPullCommand())
50
50
cmd.AddCommand(commands.NewExportCommand())*/
51
51
cmd.AddCommand(commands.NewStreamCommand())
52
52
-
/*cmd.AddCommand(commands.NewGetCommand())
53
53
-
cmd.AddCommand(commands.NewRollbackCommand())*/
52
52
+
//cmd.AddCommand(commands.NewGetCommand())
53
53
+
cmd.AddCommand(commands.NewRollbackCommand())
54
54
55
55
// Status & info (root level)
56
56
cmd.AddCommand(commands.NewStatusCommand())