diff --git a/src/args.rs b/src/args.rs index 488ac0d..d7de61b 100644 --- a/src/args.rs +++ b/src/args.rs @@ -24,6 +24,14 @@ pub struct Opt { )] pub init: Option, + #[arg( + long = "file", + value_name = "FILE", + help = "Read commit message from file", + conflicts_with = "commit_message" + )] + pub commit_file: Option, + /// Outputs enabled rules' description as bash comments for the prepare-commit-msg hook. #[arg(long, num_args = 0, hide = true)] pub prepare_commit_message: bool, diff --git a/src/errors.rs b/src/errors.rs index 70eb7c1..ede312b 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -5,6 +5,9 @@ pub enum SumiError { #[error("{details}")] GeneralError { details: String }, + #[error("Failed to read commit message from file '{path}': {error}")] + CommitFileError { path: String, error: String }, + #[error("{lines_with_errors} out of {total_lines} {line_or_lines} failed linting. See the errors above")] SplitLinesErrors { lines_with_errors: usize, diff --git a/src/lib.rs b/src/lib.rs index bd40770..3cdbe8c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -47,7 +47,7 @@ pub fn run() -> Result<(), SumiError> { return Err(SumiError::NoRulesEnabled); } - let commit_message = get_commit_from_arg_or_stdin(args.commit_message)?; + let commit_message = get_commit_from_arg_or_stdin(args.commit_message, args.commit_file)?; let lint_result = if config.split_lines { run_lint_on_each_line(&commit_message, &config) @@ -85,14 +85,25 @@ fn init_logger_from_config(config: &Config) { .init(); } -fn get_commit_from_arg_or_stdin(commit: Option) -> Result { - if let Some(commit) = commit { - Ok(commit) - } else { - get_commit_from_stdin() +fn get_commit_from_arg_or_stdin( + commit: Option, + commit_file: Option, +) -> Result { + match (commit, commit_file) { + (Some(message), _) => Ok(message), + (None, Some(path)) => get_commit_from_file(&path), + (None, None) => get_commit_from_stdin(), } } +fn get_commit_from_file(path: &str) -> Result { + std::fs::read_to_string(path) + .map(|content| content.trim().to_string()) + .map_err(|e| SumiError::GeneralError { + details: format!("Could not read commit message from '{}': {}", path, e), + }) +} + fn get_commit_from_stdin() -> Result { let mut buffer = String::new(); io::stdin().read_to_string(&mut buffer)?; diff --git a/tests/lint/mod.rs b/tests/lint/mod.rs index bb2bb80..c3307e8 100644 --- a/tests/lint/mod.rs +++ b/tests/lint/mod.rs @@ -3,6 +3,7 @@ mod test_commit_changes; mod test_config; mod test_conventional_commits; mod test_display; +mod test_file_input; mod test_gitmoji; mod test_single_rule; diff --git a/tests/lint/test_file_input.rs b/tests/lint/test_file_input.rs new file mode 100644 index 0000000..9dbcc3e --- /dev/null +++ b/tests/lint/test_file_input.rs @@ -0,0 +1,84 @@ +use crate::run_isolated_git_sumi; +use predicates::str::contains; +use tempfile::tempdir; + +#[test] +fn success_read_from_file() { + let dir = tempdir().unwrap(); + let file_path = dir.path().join("commit-msg.txt"); + std::fs::write(&file_path, "feat: add new feature").unwrap(); + let mut cmd = run_isolated_git_sumi(""); + cmd.arg("--file") + .arg(file_path) + .arg("-C") + .assert() + .success(); +} + +#[test] +fn error_nonexistent_file() { + let mut cmd = run_isolated_git_sumi(""); + cmd.arg("--file") + .arg("nonexistent-file.txt") + .arg("-C") + .assert() + .failure() + .stderr(contains( + "Could not read commit message from 'nonexistent-file.txt'", + )); +} + +#[test] +fn error_empty_file() { + let dir = tempdir().unwrap(); + let file_path = dir.path().join("empty.txt"); + std::fs::write(&file_path, "").unwrap(); + let mut cmd = run_isolated_git_sumi(""); + cmd.arg("--file") + .arg(file_path) + .arg("-C") + .assert() + .failure() + .stderr(contains("Header must not be empty")); +} + +#[test] +fn error_conflict_file_and_message() { + let dir = tempdir().unwrap(); + let file_path = dir.path().join("commit.txt"); + std::fs::write(&file_path, "feat: test").unwrap(); + + let mut cmd = run_isolated_git_sumi(""); + cmd.arg("--file") + .arg(file_path) + .arg("direct message") + .assert() + .failure() + .stderr(contains("cannot be used with")); +} + +#[test] +fn success_file_with_comments() { + let dir = tempdir().unwrap(); + let file_path = dir.path().join("commit-with-comments.txt"); + std::fs::write(&file_path, "feat: add feature\n# This is a comment\n").unwrap(); + let mut cmd = run_isolated_git_sumi(""); + cmd.arg("--file") + .arg(file_path) + .arg("-C") + .assert() + .success(); +} + +#[test] +fn success_file_with_multiline() { + let dir = tempdir().unwrap(); + let file_path = dir.path().join("multiline.txt"); + std::fs::write(&file_path, "feat: add feature\n\nDetailed description").unwrap(); + let mut cmd = run_isolated_git_sumi(""); + cmd.arg("--file") + .arg(file_path) + .arg("-C") + .assert() + .success(); +} diff --git a/website/docs/usage.md b/website/docs/usage.md index 79c6ebd..3191885 100644 --- a/website/docs/usage.md +++ b/website/docs/usage.md @@ -38,6 +38,8 @@ git-sumi [OPTIONS] [--] [COMMIT_MESSAGE] Path to a TOML configuration file [env: GIT_SUMI_CONFIG=] -f, --format Sets display format: cli, json, table, toml [env: GIT_SUMI_FORMAT=] + --file + Read commit message from file ``` ### Rules