use std::io::Write;
use anyhow::Result;
use clap::Subcommand;
use crate::mcp::BasemindServer;
use crate::mcp::params::*;
use super::render::{Emit, emit};
use super::run_tool;
#[derive(Subcommand, Debug)]
pub enum GitCmd {
Status,
Recent {
#[arg(long)]
limit: Option<u32>,
#[arg(long)]
no_files: bool,
},
Search {
pattern: String,
#[arg(long)]
field: Option<String>,
#[arg(long)]
limit: Option<u32>,
},
Touching {
path: String,
#[arg(long)]
limit: Option<u32>,
},
ByPath {
pattern: String,
#[arg(long)]
window: Option<u32>,
#[arg(long)]
limit: Option<u32>,
},
Churn {
#[arg(long)]
window: Option<u32>,
#[arg(long)]
top_k: Option<u32>,
},
Diff {
path: String,
rev_old: String,
rev_new: String,
},
DiffOutline {
path: String,
#[arg(long)]
rev: Option<String>,
},
Blame {
path: String,
#[arg(long)]
line_start: Option<u32>,
#[arg(long)]
line_end: Option<u32>,
#[arg(long)]
rev: Option<String>,
#[arg(long)]
limit: Option<u32>,
},
BlameSymbol {
path: String,
name: String,
#[arg(long)]
kind: Option<String>,
#[arg(long)]
rev: Option<String>,
#[arg(long)]
limit: Option<u32>,
},
SymbolHistory {
path: String,
name: String,
#[arg(long)]
kind: Option<String>,
#[arg(long)]
limit: Option<u32>,
#[arg(long)]
hash_mode: Option<String>,
},
}
pub async fn run(server: &BasemindServer, cmd: GitCmd, opts: &Emit, out: &mut impl Write) -> Result<()> {
let p = match cmd {
GitCmd::Status => GitParams::new(GitMode::Status),
GitCmd::Recent { limit, no_files } => GitParams {
limit,
include_files: no_files.then_some(false),
..GitParams::new(GitMode::Recent)
},
GitCmd::Search { pattern, field, limit } => GitParams {
pattern: Some(pattern),
field,
limit,
..GitParams::new(GitMode::Search)
},
GitCmd::Touching { path, limit } => GitParams {
path: Some(path.as_str().into()),
limit,
..GitParams::new(GitMode::Touching)
},
GitCmd::ByPath { pattern, window, limit } => GitParams {
pattern: Some(pattern),
window,
limit,
..GitParams::new(GitMode::ByPath)
},
GitCmd::Churn { window, top_k } => GitParams {
window,
top_k,
..GitParams::new(GitMode::Churn)
},
GitCmd::Diff { path, rev_old, rev_new } => GitParams {
path: Some(path.as_str().into()),
rev_old: Some(rev_old),
rev_new: Some(rev_new),
..GitParams::new(GitMode::Diff)
},
GitCmd::DiffOutline { path, rev } => GitParams {
path: Some(path.as_str().into()),
rev,
..GitParams::new(GitMode::DiffOutline)
},
GitCmd::Blame {
path,
line_start,
line_end,
rev,
limit,
} => GitParams {
path: Some(path.as_str().into()),
line_start,
line_end,
rev,
limit,
..GitParams::new(GitMode::Blame)
},
GitCmd::BlameSymbol {
path,
name,
kind,
rev,
limit,
} => GitParams {
path: Some(path.as_str().into()),
name: Some(name),
kind,
rev,
limit,
..GitParams::new(GitMode::BlameSymbol)
},
GitCmd::SymbolHistory {
path,
name,
kind,
limit,
hash_mode,
} => GitParams {
path: Some(path.as_str().into()),
name: Some(name),
kind,
limit,
hash_mode,
..GitParams::new(GitMode::SymbolHistory)
},
};
let key = p.mode.telemetry_key();
let r = run_tool(key, server.git(Parameters(Lenient(p))).await)?;
emit(key, &r, opts, out)
}
#[cfg(test)]
mod tests {
use clap::{CommandFactory, Parser, Subcommand as _};
use super::GitCmd;
#[derive(Parser)]
struct Harness {
#[command(subcommand)]
cmd: GitCmd,
}
#[test]
fn should_expose_one_subcommand_per_advertised_git_mode() {
let command = GitCmd::augment_subcommands(Harness::command());
let names: Vec<String> = command.get_subcommands().map(|s| s.get_name().to_string()).collect();
for mode in crate::mcp::mode::GitMode::ALL_MODES {
let expected = mode.replace('_', "-");
assert!(
names.contains(&expected),
"`git` mode `{mode}` has no `basemind git {expected}` subcommand; got {names:?}"
);
}
}
}