gabriel / muse public
revert.py python
108 lines 3.6 KB
cc9bbc18 feat: complete MuseDomainPlugin integration — apply(), incremental checkout Gabriel Cardona <gabriel@tellurstori.com> 7d ago
1 """muse revert — create a new commit that undoes a prior commit."""
2 from __future__ import annotations
3
4 import datetime
5 import json
6 import logging
7 import pathlib
8 import shutil
9
10 import typer
11
12 from muse.core.errors import ExitCode
13 from muse.core.object_store import restore_object
14 from muse.core.repo import require_repo
15 from muse.core.snapshot import compute_commit_id
16 from muse.core.store import (
17 CommitRecord,
18 get_head_commit_id,
19 read_commit,
20 read_snapshot,
21 resolve_commit_ref,
22 write_commit,
23 )
24
25 logger = logging.getLogger(__name__)
26
27 app = typer.Typer()
28
29
30 def _read_branch(root: pathlib.Path) -> str:
31 head_ref = (root / ".muse" / "HEAD").read_text().strip()
32 return head_ref.removeprefix("refs/heads/").strip()
33
34
35 def _read_repo_id(root: pathlib.Path) -> str:
36 return str(json.loads((root / ".muse" / "repo.json").read_text())["repo_id"])
37
38
39 @app.callback(invoke_without_command=True)
40 def revert(
41 ctx: typer.Context,
42 ref: str = typer.Argument(..., help="Commit to revert."),
43 message: str | None = typer.Option(None, "-m", "--message", help="Override revert commit message."),
44 no_commit: bool = typer.Option(False, "--no-commit", "-n", help="Apply changes but do not commit."),
45 ) -> None:
46 """Create a new commit that undoes a prior commit."""
47 root = require_repo()
48 repo_id = _read_repo_id(root)
49 branch = _read_branch(root)
50
51 target = resolve_commit_ref(root, repo_id, branch, ref)
52 if target is None:
53 typer.echo(f"❌ Commit '{ref}' not found.")
54 raise typer.Exit(code=ExitCode.USER_ERROR)
55
56 # The revert of a commit restores its parent snapshot
57 if target.parent_commit_id is None:
58 typer.echo("❌ Cannot revert the root commit (no parent to restore).")
59 raise typer.Exit(code=ExitCode.USER_ERROR)
60
61 parent_commit = read_commit(root, target.parent_commit_id)
62 if parent_commit is None:
63 typer.echo(f"❌ Parent commit {target.parent_commit_id[:8]} not found.")
64 raise typer.Exit(code=ExitCode.INTERNAL_ERROR)
65
66 target_snapshot = read_snapshot(root, parent_commit.snapshot_id)
67 if target_snapshot is None:
68 typer.echo(f"❌ Snapshot {parent_commit.snapshot_id[:8]} not found.")
69 raise typer.Exit(code=ExitCode.INTERNAL_ERROR)
70
71 # Restore parent snapshot to muse-work/
72 workdir = root / "muse-work"
73 if workdir.exists():
74 shutil.rmtree(workdir)
75 workdir.mkdir()
76 for rel_path, object_id in target_snapshot.manifest.items():
77 restore_object(root, object_id, workdir / rel_path)
78
79 if no_commit:
80 typer.echo(f"Reverted changes from {target.commit_id[:8]} applied to muse-work/. Run 'muse commit' to record.")
81 return
82
83 revert_message = message or f"Revert \"{target.message}\""
84 head_commit_id = get_head_commit_id(root, branch)
85
86 # The parent snapshot is already content-addressed in the object store —
87 # reuse its snapshot_id directly rather than re-scanning the workdir.
88 snapshot_id = parent_commit.snapshot_id
89 committed_at = datetime.datetime.now(datetime.timezone.utc)
90 commit_id = compute_commit_id(
91 parent_ids=[head_commit_id] if head_commit_id else [],
92 snapshot_id=snapshot_id,
93 message=revert_message,
94 committed_at_iso=committed_at.isoformat(),
95 )
96
97 write_commit(root, CommitRecord(
98 commit_id=commit_id,
99 repo_id=repo_id,
100 branch=branch,
101 snapshot_id=snapshot_id,
102 message=revert_message,
103 committed_at=committed_at,
104 parent_commit_id=head_commit_id,
105 ))
106 (root / ".muse" / "refs" / "heads" / branch).write_text(commit_id)
107
108 typer.echo(f"[{branch} {commit_id[:8]}] {revert_message}")