gabriel / muse public
gc.py python
109 lines 3.5 KB
95b86799 feat: add --format json to all porcelain commands for agent-first output Gabriel Cardona <gabriel@tellurstori.com> 2d ago
1 """``muse gc`` — garbage-collect unreachable objects.
2
3 Content-addressed storage accumulates blobs that no live commit can reach.
4 These orphaned objects are safe to delete. ``muse gc`` walks the full commit
5 graph from every live branch and tag, marks every referenced object as
6 reachable, then removes the rest.
7
8 Usage::
9
10 muse gc # remove unreachable objects
11 muse gc --dry-run # show what would be removed, touch nothing
12 muse gc --verbose # print each removed object ID
13
14 Exit codes::
15
16 0 — success (even if nothing was collected)
17 1 — internal error (e.g. corrupt store)
18 """
19
20 from __future__ import annotations
21
22 import json
23 import logging
24 from typing import Annotated
25
26 import typer
27
28 from muse.core.gc import run_gc
29 from muse.core.repo import require_repo
30
31 logger = logging.getLogger(__name__)
32 app = typer.Typer(help="Remove unreachable objects from the object store.")
33
34
35 def _fmt_bytes(n: int) -> str:
36 """Human-readable byte count."""
37 if n < 1024:
38 return f"{n} B"
39 if n < 1024 * 1024:
40 return f"{n / 1024:.1f} KiB"
41 return f"{n / (1024 * 1024):.1f} MiB"
42
43
44 @app.callback(invoke_without_command=True)
45 def gc(
46 dry_run: Annotated[
47 bool,
48 typer.Option("--dry-run", "-n", help="Show what would be removed without removing anything."),
49 ] = False,
50 verbose: Annotated[
51 bool,
52 typer.Option("--verbose", "-v", help="Print each collected object ID."),
53 ] = False,
54 fmt: Annotated[
55 str,
56 typer.Option("--format", "-f", help="Output format: text or json."),
57 ] = "text",
58 ) -> None:
59 """Remove unreachable objects from the Muse object store.
60
61 Muse stores every tracked file as a content-addressed blob. Blobs that are
62 no longer referenced by any commit, snapshot, branch, or tag are *garbage*.
63 This command identifies and removes them, reclaiming disk space.
64
65 Safety: the reachability walk always runs before any deletion. Use
66 ``--dry-run`` to preview the impact before committing to a sweep.
67 Agents should pass ``--format json`` to receive a machine-readable result
68 with ``collected_count``, ``collected_bytes``, ``reachable_count``,
69 ``elapsed_seconds``, ``dry_run``, and ``collected_ids``.
70
71 Examples::
72
73 muse gc # safe cleanup
74 muse gc --dry-run # preview only
75 muse gc --verbose # show every removed object
76 muse gc --format json # machine-readable
77 """
78 if fmt not in ("text", "json"):
79 typer.echo(f"❌ Unknown --format '{fmt}'. Choose text or json.", err=True)
80 raise typer.Exit(code=1)
81
82 repo_root = require_repo()
83 result = run_gc(repo_root, dry_run=dry_run)
84
85 if fmt == "json":
86 typer.echo(json.dumps({
87 "collected_count": result.collected_count,
88 "collected_bytes": result.collected_bytes,
89 "reachable_count": result.reachable_count,
90 "elapsed_seconds": result.elapsed_seconds,
91 "dry_run": result.dry_run,
92 "collected_ids": sorted(result.collected_ids),
93 }))
94 return
95
96 prefix = "[dry-run] " if dry_run else ""
97
98 if verbose and result.collected_ids:
99 typer.echo(f"{prefix}Unreachable objects:")
100 for oid in sorted(result.collected_ids):
101 typer.echo(f" {oid}")
102
103 action = "Would remove" if dry_run else "Removed"
104 typer.echo(
105 f"{prefix}{action} {result.collected_count} object(s) "
106 f"({_fmt_bytes(result.collected_bytes)}) "
107 f"in {result.elapsed_seconds:.3f}s "
108 f"[{result.reachable_count} reachable]"
109 )