From 4be883e8ef8a9fff8cea585049b71f46c8300915 Mon Sep 17 00:00:00 2001 From: Sandijigs Date: Thu, 18 Jun 2026 22:07:09 +0100 Subject: [PATCH 1/4] Parse --repo owner/name input in analyze command --- Cargo.lock | 7 +++++++ Cargo.toml | 2 ++ src/main.rs | 15 ++++++++++++--- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a636e94..3411e2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,6 +52,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + [[package]] name = "clap" version = "4.6.1" @@ -102,6 +108,7 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" name = "crabwatch" version = "0.1.0" dependencies = [ + "anyhow", "clap", ] diff --git a/Cargo.toml b/Cargo.toml index 7a923c6..dfa0f10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,3 +5,5 @@ edition = "2024" [dependencies] clap = { version = "4.5", features = ["derive", "env"] } +anyhow = "1" + diff --git a/src/main.rs b/src/main.rs index 6ceb311..019acf9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +use anyhow::{bail, Result}; use clap::{ArgGroup, Parser, Subcommand}; use std::path::PathBuf; @@ -40,11 +41,19 @@ enum Command { }, } -fn main() { +fn main() -> Result<()> { let cli = Cli::parse(); match cli.command { - Command::Analyze { repo, org, check } => { - println!("analyze repo={repo:?} org={org:?} check={check:?}"); + Command::Analyze { repo: repo_arg, org, check } => { + if let Some(repo_arg) = repo_arg { + let parts: Vec<&str> = repo_arg.split('/').collect(); + if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() { + bail!("--repo must be in the form owner/name"); + } + let (org, repo) = (parts[0], parts[1]); + println!("parsed org={org} repo={repo}"); + } } } + Ok(()) } From df0e348e333d4d5c2ec683287a60c36af63c2a75 Mon Sep 17 00:00:00 2001 From: Sandijigs Date: Thu, 18 Jun 2026 22:24:49 +0100 Subject: [PATCH 2/4] fixed CI failling errors --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 019acf9..6d27d4e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -44,7 +44,7 @@ enum Command { fn main() -> Result<()> { let cli = Cli::parse(); match cli.command { - Command::Analyze { repo: repo_arg, org, check } => { + Command::Analyze { repo: repo_arg, org: _, check: _ } => { if let Some(repo_arg) = repo_arg { let parts: Vec<&str> = repo_arg.split('/').collect(); if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() { From e6bf30fc47e0377accbe3f2f4e41815ffb6674b1 Mon Sep 17 00:00:00 2001 From: Sandijigs Date: Thu, 18 Jun 2026 22:38:38 +0100 Subject: [PATCH 3/4] fix linter errors --- src/main.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 6d27d4e..05fd909 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -use anyhow::{bail, Result}; +use anyhow::{Result, bail}; use clap::{ArgGroup, Parser, Subcommand}; use std::path::PathBuf; @@ -44,7 +44,11 @@ enum Command { fn main() -> Result<()> { let cli = Cli::parse(); match cli.command { - Command::Analyze { repo: repo_arg, org: _, check: _ } => { + Command::Analyze { + repo: repo_arg, + org: _, + check: _, + } => { if let Some(repo_arg) = repo_arg { let parts: Vec<&str> = repo_arg.split('/').collect(); if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() { From b0e25851eab5b760f576aa2040e9d581909a6b7b Mon Sep 17 00:00:00 2001 From: Sandijigs Date: Fri, 19 Jun 2026 18:51:43 +0100 Subject: [PATCH 4/4] Move analyze logic out of main and add parse_repo tests --- src/command/analyze.rs | 65 ++++++++++++++++++++++++++++++++++++++++++ src/command/mod.rs | 1 + src/main.rs | 18 ++++-------- 3 files changed, 72 insertions(+), 12 deletions(-) create mode 100644 src/command/analyze.rs create mode 100644 src/command/mod.rs diff --git a/src/command/analyze.rs b/src/command/analyze.rs new file mode 100644 index 0000000..7c524ef --- /dev/null +++ b/src/command/analyze.rs @@ -0,0 +1,65 @@ +use anyhow::bail; + +#[derive(Debug, PartialEq)] +pub struct ParsedRepo { + pub org: String, + pub repo: String, +} + +pub fn parse_repo(input: &str) -> anyhow::Result { + let parts: Vec<&str> = input.split('/').collect(); + if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() { + bail!("--repo must be in the form owner/name"); + } + Ok(ParsedRepo { + org: parts[0].to_string(), + repo: parts[1].to_string(), + }) +} + +pub fn run(repo_arg: Option, org_arg: Option) -> anyhow::Result<()> { + if let Some(repo_arg) = repo_arg { + let parsed = parse_repo(&repo_arg)?; + println!("parsed org={} repo={}", parsed.org, parsed.repo); + } else if org_arg.is_some() { + bail!("--org is not yet supported"); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_valid_input() { + let parsed = parse_repo("rust-lang/crabwatch").unwrap(); + assert_eq!( + parsed, + ParsedRepo { + org: "rust-lang".to_string(), + repo: "crabwatch".to_string(), + } + ); + } + + #[test] + fn rejects_input_without_slash() { + assert!(parse_repo("rust-lang").is_err()); + } + + #[test] + fn rejects_empty_owner() { + assert!(parse_repo("/crabwatch").is_err()); + } + + #[test] + fn rejects_empty_name() { + assert!(parse_repo("rust-lang/").is_err()); + } + + #[test] + fn rejects_too_many_parts() { + assert!(parse_repo("a/b/c").is_err()); + } +} diff --git a/src/command/mod.rs b/src/command/mod.rs new file mode 100644 index 0000000..92d19eb --- /dev/null +++ b/src/command/mod.rs @@ -0,0 +1 @@ +pub mod analyze; diff --git a/src/main.rs b/src/main.rs index 05fd909..55a1082 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,8 @@ -use anyhow::{Result, bail}; use clap::{ArgGroup, Parser, Subcommand}; use std::path::PathBuf; +mod command; + /// Analyze CI and best practices across Rust project repos #[derive(Parser)] #[command(name = "crabwatch", version, about, long_about = None)] @@ -41,22 +42,15 @@ enum Command { }, } -fn main() -> Result<()> { +fn main() -> anyhow::Result<()> { let cli = Cli::parse(); match cli.command { Command::Analyze { - repo: repo_arg, - org: _, + repo, + org, check: _, } => { - if let Some(repo_arg) = repo_arg { - let parts: Vec<&str> = repo_arg.split('/').collect(); - if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() { - bail!("--repo must be in the form owner/name"); - } - let (org, repo) = (parts[0], parts[1]); - println!("parsed org={org} repo={repo}"); - } + command::analyze::run(repo, org)?; } } Ok(())