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