Skip to main content

memstead_cli/commands/
branch_reset.rs

1//! `memstead branch-reset <mem> <target_sha>` — the engine's history-
2//! rewrite primitive exposed as a CLI subcommand.
3//!
4//! Replay workflows that need to rewind a mem's branch pointer to
5//! retry against a different upstream state consume this. Safety
6//! contract lives on `Engine::branch_reset`: no pushed commit can be
7//! discarded by the reset (the engine's definition of "pushed" is
8//! reachability from any `refs/remotes/*` ref). Refusal surfaces as
9//! `PUSHED_COMMITS_PROTECTED` with the offending SHAs on stderr /
10//! `details.pushed_shas`.
11
12use clap::Args;
13
14use crate::CliError;
15use crate::setup::{CliContext, CliEngine};
16
17/// `memstead branch-reset <mem> <target_sha>` arguments.
18#[derive(Args, Debug)]
19pub struct BranchResetArgs {
20    /// Mem whose branch pointer to reset. Must be git-branch-backed.
21    pub mem: String,
22    /// Target ref or SHA. Accepts anything `git rev-parse` admits —
23    /// branch names, abbreviated SHAs, full SHAs, tags.
24    pub target_sha: String,
25}
26
27pub fn run(ctx: &CliContext, args: BranchResetArgs) -> anyhow::Result<()> {
28    let outcome = match ctx.cli_engine()? {
29        CliEngine::MemRepo(mut engine) => engine
30            .branch_reset(&args.mem, &args.target_sha, None)
31            .map_err(CliError::from_engine_op)?,
32        CliEngine::Filesystem(_) => {
33            // Folder mounts have no git refs — same refusal the
34            // engine surface emits, but the CLI catches it before
35            // wiring an irrelevant call.
36            return Err(CliError {
37                code: "INVALID_INPUT",
38                kind: crate::output::ExitKind::Validation,
39                message: format!(
40                    "mem '{}' is not git-backed — `memstead branch-reset` requires a git-branch mount",
41                    args.mem,
42                ),
43                details: None,
44            }
45            .into());
46        }
47    };
48
49    if ctx.json {
50        crate::output::print_json(&outcome)?;
51    } else {
52        let discarded = if outcome.discarded_commits.is_empty() {
53            "  (no commits discarded — target equalled the current head)".to_string()
54        } else {
55            outcome
56                .discarded_commits
57                .iter()
58                .map(|s| format!("  - {s}"))
59                .collect::<Vec<_>>()
60                .join("\n")
61        };
62        crate::output::print_markdown(&format!(
63            "# Branch reset: `{}`\n\n- Branch ref: `{}`\n- Previous: `{}`\n- New: `{}`\n- Discarded commits:\n{}",
64            outcome.mem, outcome.branch_ref, outcome.previous_sha, outcome.new_sha, discarded,
65        ));
66    }
67    Ok(())
68}