Skip to content

Commit fcda1cf

Browse files
authored
Merge pull request #972 from entireio/feat/checkpoints-v2-entire-clean
Checkpoints V2: add retention-based cleanup for v2 transcript generations in `entire clean`
2 parents 616c197 + ca93ec5 commit fcda1cf

8 files changed

Lines changed: 924 additions & 100 deletions

File tree

cmd/entire/cli/clean.go

Lines changed: 95 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,14 @@ import (
1414
"github.com/entireio/cli/cmd/entire/cli/logging"
1515
"github.com/entireio/cli/cmd/entire/cli/paths"
1616
"github.com/entireio/cli/cmd/entire/cli/session"
17+
"github.com/entireio/cli/cmd/entire/cli/settings"
1718
"github.com/entireio/cli/cmd/entire/cli/strategy"
1819
"github.com/go-git/go-git/v6/plumbing"
1920
"github.com/spf13/cobra"
2021
)
2122

22-
func newCleanCmd() *cobra.Command {
23-
var forceFlag bool
24-
var allFlag bool
25-
var dryRunFlag bool
26-
var sessionFlag string
27-
28-
cmd := &cobra.Command{
29-
Use: "clean",
30-
Short: "Clean up Entire session data",
31-
Long: `Clean up Entire session data for the current HEAD commit.
23+
func cleanLongDescription(ctx context.Context) string {
24+
description := `Clean up Entire session data for the current HEAD commit.
3225
3326
By default, cleans session state and shadow branches for the current HEAD:
3427
- Session state files (.git/entire-sessions/<session-id>.json)
@@ -37,13 +30,34 @@ By default, cleans session state and shadow branches for the current HEAD:
3730
Use --all to clean all Entire session data across the repository:
3831
- All session state files (.git/entire-sessions/)
3932
- All shadow branches
40-
- Temporary files (.entire/tmp/)
41-
The entire/checkpoints/v1 branch itself is preserved.
33+
- Temporary files (.entire/tmp/)`
34+
35+
s, err := settings.Load(ctx)
36+
if err == nil && s.IsCheckpointsV2Enabled() {
37+
description += fmt.Sprintf(`
38+
- Archived v2 full transcripts older than the configured %d-day retention window`, s.GetFullTranscriptGenerationRetentionDays())
39+
}
40+
41+
description += `
4242
4343
Use --session <id> to clean a specific session only.
4444
4545
Without --force, prompts for confirmation before deleting.
46-
Use --dry-run to preview what would be deleted without prompting.`,
46+
Use --dry-run to preview what would be deleted without prompting.`
47+
48+
return description
49+
}
50+
51+
func newCleanCmd() *cobra.Command {
52+
var forceFlag bool
53+
var allFlag bool
54+
var dryRunFlag bool
55+
var sessionFlag string
56+
57+
cmd := &cobra.Command{
58+
Use: "clean",
59+
Short: "Clean up Entire session data",
60+
Long: cleanLongDescription(context.Background()),
4761
RunE: func(cmd *cobra.Command, _ []string) error {
4862
ctx := cmd.Context()
4963

@@ -258,12 +272,29 @@ func runCleanSession(ctx context.Context, cmd *cobra.Command, strat *strategy.Ma
258272

259273
// runCleanAll cleans all session data across the repository.
260274
func runCleanAll(ctx context.Context, cmd *cobra.Command, force, dryRun bool) error {
275+
s, err := settings.Load(ctx)
276+
if err != nil {
277+
fmt.Fprintf(cmd.ErrOrStderr(), "Warning: failed to load settings: %v\n", err)
278+
s = &settings.EntireSettings{}
279+
}
280+
261281
// List all items (sessions, shadow branches) — not just orphaned ones
262282
items, err := strategy.ListAllItems(ctx)
263283
if err != nil {
264284
return fmt.Errorf("failed to list items: %w", err)
265285
}
266286

287+
if s.IsCheckpointsV2Enabled() {
288+
v2Items, warnings, err := strategy.ListEligibleV2Generations(ctx, s)
289+
if err != nil {
290+
return fmt.Errorf("failed to list v2 generations: %w", err)
291+
}
292+
items = append(items, v2Items...)
293+
for _, warning := range warnings {
294+
fmt.Fprintf(cmd.ErrOrStderr(), "Warning: %s\n", warning)
295+
}
296+
}
297+
267298
// List temp files — skip active-session filter since --all deletes those sessions
268299
tempFiles, err := listAllTempFiles(ctx)
269300
if err != nil {
@@ -274,6 +305,29 @@ func runCleanAll(ctx context.Context, cmd *cobra.Command, force, dryRun bool) er
274305
return runCleanAllWithItems(ctx, cmd, force, dryRun, items, tempFiles)
275306
}
276307

308+
// printSection prints a titled list of items if the slice is non-empty.
309+
func printSection(w io.Writer, title string, items []string) {
310+
if len(items) == 0 {
311+
return
312+
}
313+
fmt.Fprintf(w, "%s (%d):\n", title, len(items))
314+
for _, item := range items {
315+
fmt.Fprintf(w, " %s\n", item)
316+
}
317+
fmt.Fprintln(w)
318+
}
319+
320+
// printResultSection prints a titled list with a leading newline, for post-deletion output.
321+
func printResultSection(w io.Writer, title string, items []string) {
322+
if len(items) == 0 {
323+
return
324+
}
325+
fmt.Fprintf(w, "\n%s (%d):\n", title, len(items))
326+
for _, item := range items {
327+
fmt.Fprintf(w, " %s\n", item)
328+
}
329+
}
330+
277331
// runCleanAllWithItems is the core logic for cleaning all items.
278332
// Separated for testability — tests pass a cmd without a TTY and use force or dryRun to avoid prompts.
279333
func runCleanAllWithItems(ctx context.Context, cmd *cobra.Command, force, dryRun bool, items []strategy.CleanupItem, tempFiles []string) error {
@@ -286,7 +340,7 @@ func runCleanAllWithItems(ctx context.Context, cmd *cobra.Command, force, dryRun
286340
}
287341

288342
// Group items by type for display
289-
var branches, states, checkpoints []strategy.CleanupItem
343+
var branches, states, checkpoints, v2Generations []strategy.CleanupItem
290344
for _, item := range items {
291345
switch item.Type {
292346
case strategy.CleanupTypeShadowBranch:
@@ -295,6 +349,8 @@ func runCleanAllWithItems(ctx context.Context, cmd *cobra.Command, force, dryRun
295349
states = append(states, item)
296350
case strategy.CleanupTypeCheckpoint:
297351
checkpoints = append(checkpoints, item)
352+
case strategy.CleanupTypeV2Generation:
353+
v2Generations = append(v2Generations, item)
298354
}
299355
}
300356

@@ -303,37 +359,11 @@ func runCleanAllWithItems(ctx context.Context, cmd *cobra.Command, force, dryRun
303359
totalItems := len(items) + len(tempFiles)
304360
fmt.Fprintf(w, "Found %d %s to clean:\n\n", totalItems, itemWord(totalItems))
305361

306-
if len(branches) > 0 {
307-
fmt.Fprintf(w, "Shadow branches (%d):\n", len(branches))
308-
for _, item := range branches {
309-
fmt.Fprintf(w, " %s\n", item.ID)
310-
}
311-
fmt.Fprintln(w)
312-
}
313-
314-
if len(states) > 0 {
315-
fmt.Fprintf(w, "Session states (%d):\n", len(states))
316-
for _, item := range states {
317-
fmt.Fprintf(w, " %s\n", item.ID)
318-
}
319-
fmt.Fprintln(w)
320-
}
321-
322-
if len(checkpoints) > 0 {
323-
fmt.Fprintf(w, "Checkpoint metadata (%d):\n", len(checkpoints))
324-
for _, item := range checkpoints {
325-
fmt.Fprintf(w, " %s\n", item.ID)
326-
}
327-
fmt.Fprintln(w)
328-
}
329-
330-
if len(tempFiles) > 0 {
331-
fmt.Fprintf(w, "Temp files (%d):\n", len(tempFiles))
332-
for _, file := range tempFiles {
333-
fmt.Fprintf(w, " %s\n", file)
334-
}
335-
fmt.Fprintln(w)
336-
}
362+
printSection(w, "Shadow branches", cleanupItemIDs(branches))
363+
printSection(w, "Session states", cleanupItemIDs(states))
364+
printSection(w, "Checkpoint metadata", cleanupItemIDs(checkpoints))
365+
printSection(w, "Archived v2 generations", cleanupItemIDs(v2Generations))
366+
printSection(w, "Temp files", tempFiles)
337367

338368
if dryRun {
339369
fmt.Fprintln(w, "Run without --dry-run to delete these items.")
@@ -370,64 +400,27 @@ func runCleanAllWithItems(ctx context.Context, cmd *cobra.Command, force, dryRun
370400
deletedTempFiles, failedTempFiles := deleteTempFiles(ctx, tempFiles)
371401

372402
// Report results
373-
totalDeleted := len(result.ShadowBranches) + len(result.SessionStates) + len(result.Checkpoints) + len(deletedTempFiles)
374-
totalFailed := len(result.FailedBranches) + len(result.FailedStates) + len(result.FailedCheckpoints) + len(failedTempFiles)
403+
totalDeleted := len(result.ShadowBranches) + len(result.SessionStates) + len(result.Checkpoints) + len(result.V2Generations) + len(deletedTempFiles)
404+
totalFailed := len(result.FailedBranches) + len(result.FailedStates) + len(result.FailedCheckpoints) + len(result.FailedV2Refs) + len(failedTempFiles)
375405

376406
if totalDeleted > 0 {
377407
fmt.Fprintf(w, "✓ Deleted %d %s:\n", totalDeleted, itemWord(totalDeleted))
378408

379-
if len(result.ShadowBranches) > 0 {
380-
fmt.Fprintf(w, "\nShadow branches (%d):\n", len(result.ShadowBranches))
381-
for _, branch := range result.ShadowBranches {
382-
fmt.Fprintf(w, " %s\n", branch)
383-
}
384-
}
409+
printResultSection(w, "Shadow branches", result.ShadowBranches)
410+
printResultSection(w, "Session states", result.SessionStates)
411+
printResultSection(w, "Checkpoints", result.Checkpoints)
412+
printResultSection(w, "Archived v2 generations", result.V2Generations)
385413

386-
if len(result.SessionStates) > 0 {
387-
fmt.Fprintf(w, "\nSession states (%d):\n", len(result.SessionStates))
388-
for _, state := range result.SessionStates {
389-
fmt.Fprintf(w, " %s\n", state)
390-
}
391-
}
392-
393-
if len(result.Checkpoints) > 0 {
394-
fmt.Fprintf(w, "\nCheckpoints (%d):\n", len(result.Checkpoints))
395-
for _, cp := range result.Checkpoints {
396-
fmt.Fprintf(w, " %s\n", cp)
397-
}
398-
}
399-
400-
if len(deletedTempFiles) > 0 {
401-
fmt.Fprintf(w, "\nTemp files (%d):\n", len(deletedTempFiles))
402-
for _, file := range deletedTempFiles {
403-
fmt.Fprintf(w, " %s\n", file)
404-
}
405-
}
414+
printResultSection(w, "Temp files", deletedTempFiles)
406415
}
407416

408417
if totalFailed > 0 {
409418
fmt.Fprintf(errW, "\nFailed to delete %d %s:\n", totalFailed, itemWord(totalFailed))
410419

411-
if len(result.FailedBranches) > 0 {
412-
fmt.Fprintf(errW, "\nShadow branches:\n")
413-
for _, branch := range result.FailedBranches {
414-
fmt.Fprintf(errW, " %s\n", branch)
415-
}
416-
}
417-
418-
if len(result.FailedStates) > 0 {
419-
fmt.Fprintf(errW, "\nSession states:\n")
420-
for _, state := range result.FailedStates {
421-
fmt.Fprintf(errW, " %s\n", state)
422-
}
423-
}
424-
425-
if len(result.FailedCheckpoints) > 0 {
426-
fmt.Fprintf(errW, "\nCheckpoints:\n")
427-
for _, cp := range result.FailedCheckpoints {
428-
fmt.Fprintf(errW, " %s\n", cp)
429-
}
430-
}
420+
printResultSection(errW, "Shadow branches", result.FailedBranches)
421+
printResultSection(errW, "Session states", result.FailedStates)
422+
printResultSection(errW, "Checkpoints", result.FailedCheckpoints)
423+
printResultSection(errW, "Archived v2 generations", result.FailedV2Refs)
431424

432425
if len(failedTempFiles) > 0 {
433426
fmt.Fprintf(errW, "\nTemp files:\n")
@@ -442,6 +435,15 @@ func runCleanAllWithItems(ctx context.Context, cmd *cobra.Command, force, dryRun
442435
return nil
443436
}
444437

438+
// cleanupItemIDs extracts IDs from a slice of CleanupItems.
439+
func cleanupItemIDs(items []strategy.CleanupItem) []string {
440+
ids := make([]string, len(items))
441+
for i, item := range items {
442+
ids[i] = item.ID
443+
}
444+
return ids
445+
}
446+
445447
// listAllTempFiles returns all files in .entire/tmp/ without filtering.
446448
// Used by --all since those sessions are being deleted anyway.
447449
func listAllTempFiles(ctx context.Context) ([]string, error) {

0 commit comments

Comments
 (0)