diff --git a/src/bin/main.rs b/src/bin/main.rs index ee3b42c..b466086 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -95,6 +95,14 @@ struct Args { )] sign: Option, + #[arg( + long, + value_name = "LENGTH", + help_heading = Some("Configuration"), + help = "Maximum character length for commit summary" + )] + max_summary_length: Option, + #[arg(short, long, help = "Stage all tracked modified or deleted files")] all: bool, @@ -133,6 +141,7 @@ fn main() -> Result<()> { stdout, issues, sign, + max_summary_length, all, yes, current_workdir, @@ -191,6 +200,7 @@ fn main() -> Result<()> { issues, path: config, sign, + max_summary_length, _user_config_path: None, _current_dir: Some(current_dir.clone()), }))?; diff --git a/src/lib/config.rs b/src/lib/config.rs index 0ca30c6..2d4a513 100644 --- a/src/lib/config.rs +++ b/src/lib/config.rs @@ -16,6 +16,7 @@ pub struct Config { pub emoji: bool, pub issues: bool, pub sign: bool, + pub max_summary_length: Option, pub workdir: PathBuf, } @@ -35,6 +36,7 @@ struct ConfigTOML { pub emoji: bool, pub issues: bool, pub sign: bool, + pub max_summary_length: Option, } #[derive(Default)] @@ -45,6 +47,7 @@ pub struct ConfigArgs { pub emoji: Option, pub issues: Option, pub sign: Option, + pub max_summary_length: Option, pub _user_config_path: Option, pub _current_dir: Option, } @@ -59,6 +62,7 @@ impl Config { emoji, issues, sign, + max_summary_length, _user_config_path, _current_dir, } = args.unwrap_or_default(); @@ -108,6 +112,7 @@ impl Config { emoji: emoji.unwrap_or(config.emoji), issues: issues.unwrap_or(config.issues), sign: sign.unwrap_or(config.sign), + max_summary_length: max_summary_length.or(config.max_summary_length), workdir, }) } @@ -285,4 +290,40 @@ mod tests { Ok(()) } + + #[test] + fn test_max_summary_length() -> Result<(), Box> { + // Test default (None) + let config = Config::new(None)?; + assert_eq!(config.max_summary_length, None); + + // Test from config file + let tempdir = tempfile::tempdir()?; + std::fs::write(tempdir.path().join(".koji.toml"), "max_summary_length = 72")?; + + let config = Config::new(Some(ConfigArgs { + _current_dir: Some(tempdir.path().to_path_buf()), + ..Default::default() + }))?; + + assert_eq!(config.max_summary_length, Some(72)); + + tempdir.close()?; + + // Test from args (overrides config file) + let tempdir = tempfile::tempdir()?; + std::fs::write(tempdir.path().join(".koji.toml"), "max_summary_length = 72")?; + + let config = Config::new(Some(ConfigArgs { + _current_dir: Some(tempdir.path().to_path_buf()), + max_summary_length: Some(50), + ..Default::default() + }))?; + + assert_eq!(config.max_summary_length, Some(50)); + + tempdir.close()?; + + Ok(()) + } } diff --git a/src/lib/questions.rs b/src/lib/questions.rs index 841e442..3523ac1 100644 --- a/src/lib/questions.rs +++ b/src/lib/questions.rs @@ -54,11 +54,28 @@ fn format_commit_type_choice( format!("{name}:{emoji:>width$}{description}") } -fn validate_summary(input: &str) -> Result { - match input.trim().is_empty() { - false => Ok(Validation::Valid), - true => Ok(Validation::Invalid("A summary is required".into())), +fn validate_summary_with_max_length( + input: &str, + max_length: Option, +) -> Result { + if input.trim().is_empty() { + return Ok(Validation::Invalid("A summary is required".into())); + } + + if let Some(max_len) = max_length { + if input.chars().count() > max_len { + return Ok(Validation::Invalid( + format!( + "Summary must be {} characters or less (current: {})", + max_len, + input.chars().count() + ) + .into(), + )); + } } + + Ok(Validation::Valid) } fn validate_issue_reference(input: &str) -> Result { @@ -183,7 +200,7 @@ fn prompt_scope(config: &Config) -> Result> { } } -fn prompt_summary(msg: String) -> Result { +fn prompt_summary(msg: String, max_length: Option) -> Result { let previous_summary = match parse_summary(&msg) { Ok(parsed) => parsed.summary, Err(_) => "".into(), @@ -192,7 +209,7 @@ fn prompt_summary(msg: String) -> Result { let summary = Text::new("Write a short, imperative tense description of the change:") .with_render_config(get_render_config()) .with_placeholder(&previous_summary) - .with_validator(validate_summary) + .with_validator(move |input: &str| validate_summary_with_max_length(input, max_length)) .prompt()?; Ok(summary) @@ -277,7 +294,7 @@ pub struct Answers { pub fn create_prompt(last_message: String, config: &Config) -> Result { let commit_type = prompt_type(config)?; let scope = prompt_scope(config)?; - let summary = prompt_summary(last_message)?; + let summary = prompt_summary(last_message, config.max_summary_length)?; let body = prompt_body()?; let mut breaking = false; @@ -377,14 +394,14 @@ mod tests { #[test] fn test_validate_summary() { - let validated = validate_summary("needed more badges :badger:"); + let validated = validate_summary_with_max_length("needed more badges :badger:", None); assert!(validated.is_ok()); assert!(validated .expect("Summary should be OK") .eq(&Validation::Valid)); - let validated = validate_summary(""); + let validated = validate_summary_with_max_length("", None); assert!(validated.is_ok()); assert!(validated @@ -410,4 +427,37 @@ mod tests { "An issue reference is required".into() ))); } + + #[test] + fn test_validate_summary_with_max_length() { + // Test within limit + let validated = validate_summary_with_max_length("short summary", Some(72)); + assert!(validated.is_ok()); + assert!(validated + .expect("Summary should be OK") + .eq(&Validation::Valid)); + + // Test exceeding limit + let long_summary = "a".repeat(73); + let validated = validate_summary_with_max_length(&long_summary, Some(72)); + assert!(validated.is_ok()); + assert!(matches!( + validated.expect("Summary should be OK"), + Validation::Invalid(_) + )); + + // Test no limit + let validated = validate_summary_with_max_length(&long_summary, None); + assert!(validated.is_ok()); + assert!(validated + .expect("Summary should be OK") + .eq(&Validation::Valid)); + + // Test empty with limit + let validated = validate_summary_with_max_length("", Some(72)); + assert!(validated.is_ok()); + assert!(validated + .expect("Summary should be OK") + .eq(&Validation::Invalid("A summary is required".into()))); + } }