Skip to main content

memstead_cli/commands/
transport.rs

1//! `memstead fetch` / `memstead pull` / `memstead push` CLI subcommands. The
2//! three engine surfaces share refusal codes and an outcome shape;
3//! the CLI front-end is a thin print of each.
4
5use clap::Args;
6
7use crate::CliError;
8use crate::output::ExitKind;
9use crate::setup::{CliContext, CliEngine};
10
11/// `memstead fetch <mem> [--remote <name>] [<refspec>...]` arguments.
12#[derive(Args, Debug)]
13pub struct FetchArgs {
14    pub mem: String,
15    #[arg(long, default_value = "origin")]
16    pub remote: String,
17    /// Optional refspecs forwarded to the underlying `git fetch`.
18    /// Empty list uses the remote's configured defaults.
19    #[arg(num_args = 0..)]
20    pub refspecs: Vec<String>,
21}
22
23/// `memstead pull <mem> [--remote <name>]` arguments.
24#[derive(Args, Debug)]
25pub struct PullArgs {
26    pub mem: String,
27    #[arg(long, default_value = "origin")]
28    pub remote: String,
29}
30
31/// `memstead push <mem> [--remote <name>] [--force]` arguments.
32#[derive(Args, Debug)]
33pub struct PushArgs {
34    pub mem: String,
35    #[arg(long, default_value = "origin")]
36    pub remote: String,
37    /// Force-push (`--force-with-lease` under the hood). Refused
38    /// non-fast-forward pushes only happen here. Use with care — the
39    /// remote's view of the branch is overwritten.
40    #[arg(long, default_value_t = false)]
41    pub force: bool,
42}
43
44pub fn run_fetch(ctx: &CliContext, args: FetchArgs) -> anyhow::Result<()> {
45    let outcome = match ctx.cli_engine()? {
46        CliEngine::MemRepo(engine) => engine
47            .fetch(&args.mem, &args.remote, &args.refspecs)
48            .map_err(CliError::from_engine_op)?,
49        CliEngine::Filesystem(_) => return Err(folder_refusal("memstead fetch", &args.mem)),
50    };
51    if ctx.json {
52        crate::output::print_json(&outcome)?;
53    } else {
54        let updated = if outcome.updated_refs.is_empty() {
55            "  (no refs changed)".to_string()
56        } else {
57            outcome
58                .updated_refs
59                .iter()
60                .map(|u| {
61                    let prev = if u.previous_sha.is_empty() {
62                        "<new>".to_string()
63                    } else {
64                        u.previous_sha.clone()
65                    };
66                    format!("  - {} : {prev} -> {}", u.ref_name, u.new_sha)
67                })
68                .collect::<Vec<_>>()
69                .join("\n")
70        };
71        crate::output::print_markdown(&format!(
72            "# Fetched from `{}`\n\n- Refspecs: {}\n- Updated refs:\n{}",
73            outcome.remote,
74            if outcome.refspecs.is_empty() {
75                "<defaults>".to_string()
76            } else {
77                outcome.refspecs.join(", ")
78            },
79            updated,
80        ));
81    }
82    Ok(())
83}
84
85pub fn run_pull(ctx: &CliContext, args: PullArgs) -> anyhow::Result<()> {
86    let outcome = match ctx.cli_engine()? {
87        CliEngine::MemRepo(mut engine) => engine
88            .pull(&args.mem, &args.remote)
89            .map_err(CliError::from_engine_op)?,
90        CliEngine::Filesystem(_) => return Err(folder_refusal("memstead pull", &args.mem)),
91    };
92    if ctx.json {
93        crate::output::print_json(&outcome)?;
94    } else {
95        let prev = if outcome.previous_sha.is_empty() {
96            "<new branch>".to_string()
97        } else {
98            outcome.previous_sha.clone()
99        };
100        crate::output::print_markdown(&format!(
101            "# Pulled `{}`\n\n- Branch ref: `{}`\n- Source ref: `{}`\n- Previous: `{prev}`\n- New: `{}`",
102            outcome.mem, outcome.branch_ref, outcome.source_ref, outcome.new_sha,
103        ));
104    }
105    Ok(())
106}
107
108pub fn run_push(ctx: &CliContext, args: PushArgs) -> anyhow::Result<()> {
109    let outcome = match ctx.cli_engine()? {
110        CliEngine::MemRepo(engine) => engine
111            .push(&args.mem, &args.remote, args.force)
112            .map_err(CliError::from_engine_op)?,
113        CliEngine::Filesystem(_) => return Err(folder_refusal("memstead push", &args.mem)),
114    };
115    if ctx.json {
116        crate::output::print_json(&outcome)?;
117    } else {
118        let force_note = if outcome.forced { " (forced)" } else { "" };
119        crate::output::print_markdown(&format!(
120            "# Pushed `{}` to `{}`{force_note}\n\n- Branch ref: `{}`\n- New SHA at remote: `{}`",
121            outcome.mem, outcome.remote, outcome.branch_ref, outcome.new_sha,
122        ));
123    }
124    Ok(())
125}
126
127fn folder_refusal(op: &str, mem: &str) -> anyhow::Error {
128    CliError {
129        code: "INVALID_INPUT",
130        kind: ExitKind::Validation,
131        message: format!("mem '{mem}' is not git-backed — `{op}` requires a git-branch mount",),
132        details: None,
133    }
134    .into()
135}