Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/bin/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ struct Args {
)]
sign: Option<bool>,

#[arg(
long,
value_name = "LENGTH",
help_heading = Some("Configuration"),
help = "Maximum character length for commit summary"
)]
max_summary_length: Option<usize>,

#[arg(short, long, help = "Stage all tracked modified or deleted files")]
all: bool,

Expand Down Expand Up @@ -133,6 +141,7 @@ fn main() -> Result<()> {
stdout,
issues,
sign,
max_summary_length,
all,
yes,
current_workdir,
Expand Down Expand Up @@ -191,6 +200,7 @@ fn main() -> Result<()> {
issues,
path: config,
sign,
max_summary_length,
_user_config_path: None,
_current_dir: Some(current_dir.clone()),
}))?;
Expand Down
41 changes: 41 additions & 0 deletions src/lib/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub struct Config {
pub emoji: bool,
pub issues: bool,
pub sign: bool,
pub max_summary_length: Option<usize>,
pub workdir: PathBuf,
}

Expand All @@ -35,6 +36,7 @@ struct ConfigTOML {
pub emoji: bool,
pub issues: bool,
pub sign: bool,
pub max_summary_length: Option<usize>,
}

#[derive(Default)]
Expand All @@ -45,6 +47,7 @@ pub struct ConfigArgs {
pub emoji: Option<bool>,
pub issues: Option<bool>,
pub sign: Option<bool>,
pub max_summary_length: Option<usize>,
pub _user_config_path: Option<PathBuf>,
pub _current_dir: Option<PathBuf>,
}
Expand All @@ -59,6 +62,7 @@ impl Config {
emoji,
issues,
sign,
max_summary_length,
_user_config_path,
_current_dir,
} = args.unwrap_or_default();
Expand Down Expand Up @@ -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,
})
}
Expand Down Expand Up @@ -285,4 +290,40 @@ mod tests {

Ok(())
}

#[test]
fn test_max_summary_length() -> Result<(), Box<dyn Error>> {
// 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(())
}
}
68 changes: 59 additions & 9 deletions src/lib/questions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,28 @@ fn format_commit_type_choice(
format!("{name}:{emoji:>width$}{description}")
}

fn validate_summary(input: &str) -> Result<Validation, CustomUserError> {
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<usize>,
) -> Result<Validation, CustomUserError> {
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<Validation, CustomUserError> {
Expand Down Expand Up @@ -183,7 +200,7 @@ fn prompt_scope(config: &Config) -> Result<Option<String>> {
}
}

fn prompt_summary(msg: String) -> Result<String> {
fn prompt_summary(msg: String, max_length: Option<usize>) -> Result<String> {
let previous_summary = match parse_summary(&msg) {
Ok(parsed) => parsed.summary,
Err(_) => "".into(),
Expand All @@ -192,7 +209,7 @@ fn prompt_summary(msg: String) -> Result<String> {
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)
Expand Down Expand Up @@ -277,7 +294,7 @@ pub struct Answers {
pub fn create_prompt(last_message: String, config: &Config) -> Result<Answers> {
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;
Expand Down Expand Up @@ -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
Expand All @@ -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())));
}
}