use clap::{Parser, Subcommand};
use std::path::PathBuf;
#[derive(Debug, Parser)]
#[command(
name = "codedna",
version,
about = "Analyze a repository and reveal its codebase DNA",
long_about = None
)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
}
#[derive(Debug, Subcommand)]
pub enum Commands {
Scan {
#[arg(default_value = ".")]
path: String,
},
Stack {
#[arg(default_value = ".")]
path: String,
},
Files {
#[arg(default_value = ".")]
path: String,
},
Framework {
name: String,
#[arg(default_value = ".")]
path: String,
},
Deadcode {
#[arg(default_value = ".")]
path: String,
},
Analyze {
#[arg(default_value = ".")]
path: String,
#[arg(long, default_value_t = false)]
time: bool,
},
Json {
#[arg(default_value = ".")]
path: String,
#[arg(long, default_value_t = false)]
compact: bool,
#[arg(long, default_value_t = false)]
time: bool,
},
Map {
#[arg(default_value = ".")]
path: String,
#[arg(long, default_value_t = crate::repo_map::DEFAULT_MAX_DEPTH)]
depth: usize,
},
}
fn resolve(input: &str) -> (PathBuf, Option<tempfile::TempDir>) {
match crate::git_handler::resolve_input(input) {
Ok(pair) => pair,
Err(msg) => {
eprintln!("error: {msg}");
std::process::exit(1);
}
}
}
pub fn run() {
let cli = Cli::parse();
match cli.command {
Commands::Scan { path } => {
let (local_path, _guard) = resolve(&path);
let files = crate::scanner::scan_repository(&local_path);
println!();
println!(
" Scanned {} file{} in '{}'",
files.len(),
if files.len() == 1 { "" } else { "s" },
path
);
println!();
for f in &files {
println!(" {}", f.display());
}
println!();
}
Commands::Stack { path } => {
eprintln!("Analysing '{path}'…");
let (local_path, _guard) = resolve(&path);
let result = crate::analysis::analyze(&local_path);
crate::reporter::print_stack(&result);
}
Commands::Files { path } => {
eprintln!("Analysing '{path}'…");
let (local_path, _guard) = resolve(&path);
let result = crate::analysis::analyze(&local_path);
crate::reporter::print_files(&result);
}
Commands::Framework { name, path } => {
eprintln!("Analysing '{path}'…");
let (local_path, _guard) = resolve(&path);
let files = crate::scanner::scan_repository(&local_path);
let matching = crate::framework_detector::detect_files_using_framework(&files, &name);
crate::reporter::print_framework_usage(&name, &matching);
}
Commands::Deadcode { path } => {
eprintln!("Analysing '{path}'…");
let (local_path, _guard) = resolve(&path);
let result = crate::analysis::analyze(&local_path);
crate::reporter::print_dead_code(&result);
}
Commands::Analyze { path, time } => {
eprintln!("Analysing '{path}'…");
let t0 = std::time::Instant::now();
let (local_path, _guard) = resolve(&path);
let result = crate::analysis::analyze(&local_path);
crate::reporter::print_report(&result);
if time {
eprintln!("\n ⏱ Completed in {:.2?}", t0.elapsed());
}
}
Commands::Json {
path,
compact,
time,
} => {
let t0 = std::time::Instant::now();
let (local_path, _guard) = resolve(&path);
let result = crate::analysis::analyze(&local_path);
crate::reporter::print_json(&result, compact);
if time {
eprintln!("\n ⏱ Completed in {:.2?}", t0.elapsed());
}
}
Commands::Map { path, depth } => {
let (local_path, _guard) = resolve(&path);
let tree = crate::repo_map::render_tree(&local_path, depth);
println!();
print!("{tree}");
println!();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
#[test]
fn parses_analyze_with_time_flag() {
let cli = Cli::try_parse_from(["codedna", "analyze", ".", "--time"]).unwrap();
match cli.command {
Commands::Analyze { path, time } => {
assert_eq!(path, ".");
assert!(time);
}
_ => panic!("expected analyze command"),
}
}
#[test]
fn parses_json_with_compact_and_time_flags() {
let cli = Cli::try_parse_from(["codedna", "json", ".", "--compact", "--time"]).unwrap();
match cli.command {
Commands::Json {
path,
compact,
time,
} => {
assert_eq!(path, ".");
assert!(compact);
assert!(time);
}
_ => panic!("expected json command"),
}
}
#[test]
fn parses_map_with_custom_depth() {
let cli = Cli::try_parse_from(["codedna", "map", ".", "--depth", "3"]).unwrap();
match cli.command {
Commands::Map { path, depth } => {
assert_eq!(path, ".");
assert_eq!(depth, 3);
}
_ => panic!("expected map command"),
}
}
#[test]
fn parses_map_with_default_depth() {
let cli = Cli::try_parse_from(["codedna", "map", "."]).unwrap();
match cli.command {
Commands::Map { path, depth } => {
assert_eq!(path, ".");
assert_eq!(depth, crate::repo_map::DEFAULT_MAX_DEPTH);
}
_ => panic!("expected map command"),
}
}
#[test]
fn parses_analyze_with_git_url() {
let cli =
Cli::try_parse_from(["codedna", "analyze", "https://github.com/user/repo"]).unwrap();
match cli.command {
Commands::Analyze { path, time } => {
assert_eq!(path, "https://github.com/user/repo");
assert!(!time);
}
_ => panic!("expected analyze command"),
}
}
#[test]
fn parses_stack_with_git_url() {
let cli =
Cli::try_parse_from(["codedna", "stack", "git@github.com:user/repo.git"]).unwrap();
match cli.command {
Commands::Stack { path } => {
assert_eq!(path, "git@github.com:user/repo.git");
}
_ => panic!("expected stack command"),
}
}
}