use clap::{Parser, Subcommand, ValueEnum};
use std::path::PathBuf;
#[derive(Parser, Debug)]
#[command(name = "kodo")]
#[command(version, about, long_about = None)]
pub struct Args {
#[command(subcommand)]
pub command: Option<Command>,
#[arg(short, long, env = "KODO_CONFIG", global = true)]
pub config: Option<PathBuf>,
#[arg(short, long)]
pub repo: Option<PathBuf>,
#[arg(short, long, default_value = "7")]
pub days: u32,
#[arg(long)]
pub include_merges: bool,
#[arg(short, long, value_enum, default_value_t = OutputFormat::Table)]
pub output: OutputFormat,
#[arg(short, long, value_enum, default_value = "daily")]
pub period: Period,
#[arg(short, long)]
pub branch: Option<String>,
#[arg(long, value_delimiter = ',')]
pub ext: Option<Vec<String>>,
#[arg(long)]
pub single_metric: bool,
#[arg(long, default_value = "local")]
pub timezone: String,
#[arg(long, value_delimiter = ',')]
pub repo_name: Option<Vec<String>>,
}
#[derive(Subcommand, Debug)]
pub enum Command {
Add(AddArgs),
Remove(RemoveArgs),
List(ListArgs),
}
#[derive(Parser, Debug)]
pub struct AddArgs {
pub path: PathBuf,
#[arg(short, long)]
pub name: Option<String>,
#[arg(short, long)]
pub branch: Option<String>,
}
#[derive(Parser, Debug)]
pub struct RemoveArgs {
pub identifier: String,
}
#[derive(Parser, Debug)]
pub struct ListArgs {
#[arg(long)]
pub json: bool,
}
#[derive(ValueEnum, Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum OutputFormat {
Tui,
#[default]
Table,
Json,
Csv,
}
impl std::fmt::Display for OutputFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Tui => write!(f, "tui"),
Self::Table => write!(f, "table"),
Self::Json => write!(f, "json"),
Self::Csv => write!(f, "csv"),
}
}
}
#[derive(ValueEnum, Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Period {
#[default]
Daily,
Weekly,
Monthly,
Yearly,
}
impl std::fmt::Display for Period {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Daily => write!(f, "daily"),
Self::Weekly => write!(f, "weekly"),
Self::Monthly => write!(f, "monthly"),
Self::Yearly => write!(f, "yearly"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn test_output_format_display() {
assert_eq!(OutputFormat::Tui.to_string(), "tui");
assert_eq!(OutputFormat::Table.to_string(), "table");
assert_eq!(OutputFormat::Json.to_string(), "json");
assert_eq!(OutputFormat::Csv.to_string(), "csv");
}
#[test]
fn test_period_display() {
assert_eq!(Period::Daily.to_string(), "daily");
assert_eq!(Period::Weekly.to_string(), "weekly");
assert_eq!(Period::Monthly.to_string(), "monthly");
assert_eq!(Period::Yearly.to_string(), "yearly");
}
#[test]
fn test_args_defaults() {
let args = Args::parse_from(["kodo"]);
assert_eq!(args.days, 7);
assert!(!args.include_merges);
assert_eq!(args.output, OutputFormat::Table);
assert_eq!(args.period, Period::Daily);
assert!(args.command.is_none());
}
#[test]
fn test_args_with_repo() {
let args = Args::parse_from(["kodo", "--repo", "/tmp/repo"]);
assert_eq!(args.repo, Some(PathBuf::from("/tmp/repo")));
}
#[test]
fn test_args_with_days() {
let args = Args::parse_from(["kodo", "--days", "30"]);
assert_eq!(args.days, 30);
}
#[test]
fn test_args_output_tui_explicit() {
let args = Args::parse_from(["kodo", "--output", "tui"]);
assert_eq!(args.output, OutputFormat::Tui);
}
#[test]
fn test_args_output_tui_short() {
let args = Args::parse_from(["kodo", "-o", "tui"]);
assert_eq!(args.output, OutputFormat::Tui);
}
#[test]
fn test_args_output_json_explicit() {
let args = Args::parse_from(["kodo", "--output", "json"]);
assert_eq!(args.output, OutputFormat::Json);
}
#[test]
fn test_args_output_csv_explicit() {
let args = Args::parse_from(["kodo", "--output", "csv"]);
assert_eq!(args.output, OutputFormat::Csv);
}
#[test]
fn test_args_with_extensions() {
let args = Args::parse_from(["kodo", "--ext", "rs,ts,js"]);
assert_eq!(
args.ext,
Some(vec!["rs".to_string(), "ts".to_string(), "js".to_string()])
);
}
#[test]
fn test_add_command() {
let args = Args::parse_from(["kodo", "add", "."]);
assert!(matches!(args.command, Some(Command::Add(_))));
if let Some(Command::Add(add_args)) = args.command {
assert_eq!(add_args.path, PathBuf::from("."));
assert!(add_args.name.is_none());
assert!(add_args.branch.is_none());
}
}
#[test]
fn test_add_command_with_options() {
let args = Args::parse_from([
"kodo",
"add",
"/tmp/repo",
"--name",
"my-repo",
"--branch",
"main",
]);
if let Some(Command::Add(add_args)) = args.command {
assert_eq!(add_args.path, PathBuf::from("/tmp/repo"));
assert_eq!(add_args.name, Some("my-repo".to_string()));
assert_eq!(add_args.branch, Some("main".to_string()));
}
}
#[test]
fn test_list_command() {
let args = Args::parse_from(["kodo", "list"]);
assert!(matches!(args.command, Some(Command::List(_))));
if let Some(Command::List(list_args)) = args.command {
assert!(!list_args.json);
}
}
#[test]
fn test_list_command_with_json() {
let args = Args::parse_from(["kodo", "list", "--json"]);
assert!(matches!(args.command, Some(Command::List(_))));
if let Some(Command::List(list_args)) = args.command {
assert!(list_args.json);
}
}
#[test]
fn test_help_includes_output_short() {
let help = Args::command().render_help().to_string();
assert!(help.contains("-o, --output <OUTPUT>"));
}
}