use std::path::PathBuf;
use anyhow::{bail, Result};
use clap::{CommandFactory, Parser, Subcommand};
use super::index::Tier;
use super::operations::{Code, Filter, IngestOptions, Revision};
use super::render;
use crate::out::Out;
#[derive(Parser)]
#[command(
version = crate::GIT_VERSION,
name = "code",
about = "A source catalogue: what exists, what uses what, and what is defined where"
)]
pub struct Cli {
#[arg(long, env = "PILE")]
pile: PathBuf,
#[arg(long, env = "TRIBLESPACE_KEY")]
key: Option<PathBuf>,
#[command(subcommand)]
command: Option<Command>,
}
#[derive(Subcommand)]
enum Command {
Ingest {
dirs: Vec<PathBuf>,
#[arg(long)]
commit: Option<String>,
#[arg(long)]
dry_run: bool,
},
Find {
name: String,
#[command(flatten)]
filter: FilterArgs,
},
Uses {
identifier: String,
#[arg(long)]
imports: bool,
#[command(flatten)]
filter: FilterArgs,
},
Show {
selector: String,
#[arg(long)]
source: bool,
},
Dup {
#[arg(long, default_value_t = 8)]
min_lines: u64,
#[arg(long)]
cross_repo: bool,
#[command(flatten)]
filter: FilterArgs,
},
Stats {
#[command(flatten)]
filter: FilterArgs,
},
Index,
Search {
query: String,
#[arg(long, default_value_t = 8)]
top: usize,
#[arg(long, default_value = "both")]
r#in: String,
#[command(flatten)]
filter: FilterArgs,
},
Blame {
identifier: String,
dirs: Vec<PathBuf>,
#[arg(long, default_value_t = 10)]
limit: usize,
},
}
#[derive(clap::Args)]
struct FilterArgs {
#[arg(long)]
kind: Option<String>,
#[arg(long)]
repo: Option<String>,
#[arg(long)]
at: Option<String>,
}
impl From<FilterArgs> for Filter {
fn from(args: FilterArgs) -> Self {
Filter {
kind: args.kind,
repo: args.repo,
revision: Revision::parse(args.at.as_deref()),
}
}
}
fn dirs_or_cwd(dirs: Vec<PathBuf>) -> Result<Vec<PathBuf>> {
if dirs.is_empty() {
return Ok(vec![std::env::current_dir()?]);
}
Ok(dirs)
}
pub fn run() -> Result<()> {
let cli = Cli::parse();
if cli.command.is_none() {
Cli::command().print_help()?;
println!();
return Ok(());
}
crate::cli::with_output("code", |out| execute(cli, out))
}
pub fn execute(cli: Cli, out: &mut Out<'_>) -> Result<()> {
let Some(command) = cli.command else {
return out.text(Cli::command().render_help().to_string());
};
let code = Code::new(cli.pile, cli.key);
match command {
Command::Ingest {
dirs,
commit,
dry_run,
} => {
let dirs = dirs_or_cwd(dirs)?;
let report = code.ingest(&dirs, &IngestOptions { commit, dry_run })?;
render::ingest(&report, out)
}
Command::Find { name, filter } => {
let answer = code.find(&name, &filter.into())?;
render::definition(&answer, out)
}
Command::Uses {
identifier,
imports,
filter,
} => {
let filter = Filter::from(filter);
if imports {
let (count, provenance) = code.imports(&identifier, &filter)?;
out.line(format!(
"{identifier} — first segment of a `use` path in {count} unit(s)"
))?;
out.line("")?;
return render::provenance(&provenance, out);
}
let answer = code.uses(&identifier, &filter)?;
render::usage(&answer, out)
}
Command::Show { selector, source } => {
let detail = code.show(&selector, source)?;
render::detail(&detail, out)
}
Command::Dup {
min_lines,
cross_repo,
filter,
} => {
let report = code.duplicates(min_lines, cross_repo, &filter.into())?;
render::duplicates(&report, out)
}
Command::Stats { filter } => {
let report = code.stats(&filter.into())?;
render::stats(&report, out)
}
Command::Index => {
let report = code.index()?;
render::index(&report, out)
}
Command::Search {
query,
top,
r#in,
filter,
} => {
let Some(tier) = Tier::from_name(&r#in) else {
bail!("--in must be one of: docs, text, both");
};
let report = code.search(&query, tier, top, &filter.into())?;
render::search(&report, out)
}
Command::Blame {
identifier,
dirs,
limit,
} => {
let dirs = dirs_or_cwd(dirs)?;
let report = code.blame(&identifier, &dirs, limit)?;
render::blame(&report, out)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_verb_list_is_what_this_faculty_promises() {
let command = Cli::command();
let verbs: Vec<String> = command
.get_subcommands()
.map(|sub| sub.get_name().to_owned())
.collect();
assert_eq!(
verbs,
vec!["ingest", "find", "uses", "show", "dup", "stats", "index", "search", "blame",]
);
}
#[test]
fn no_subcommand_is_a_help_request_and_never_a_write() {
let parsed = Cli::try_parse_from(["code", "--pile", "/tmp/nonexistent.pile"])
.expect("the pile argument alone parses");
assert!(parsed.command.is_none());
}
#[test]
fn a_revision_selector_reaches_the_filter() {
let parsed = Cli::try_parse_from([
"code",
"--pile",
"/tmp/nonexistent.pile",
"uses",
"maintain_admitted_fact_targets",
"--at",
"faculties@336a8765",
])
.expect("parses");
let Some(Command::Uses { filter, .. }) = parsed.command else {
panic!("expected uses");
};
assert_eq!(
Filter::from(filter).revision,
Revision::Selector("faculties@336a8765".to_owned())
);
}
}