pub mod hooks;
use regex::Regex;
use crate::branch::Branch;
use crate::command::{run_command, run_command_with_stdin};
use crate::commit::Commit;
const SCISSORS: &str = "------------------------ >8 ------------------------";
const COMMIT_DELIMITER: &str = "------------------------ COMMIT >! ------------------------";
const COMMIT_BODY_DELIMITER: &str = "------------------------ BODY >! ------------------------";
const COMMIT_TRAILERS_DELIMITER: &str =
"------------------------ TRAILERS >! ------------------------";
lazy_static! {
pub static ref SUBJECT_WITH_MERGE_REMOTE_BRANCH: Regex =
Regex::new(r"^Merge branch '.+' of .+ into .+").unwrap();
static ref SUBJECT_WITH_SQUASH_PR: Regex = Regex::new(r".+ \(#\d+\)$").unwrap();
static ref MESSAGE_CONTAINS_MERGE_REQUEST_REFERENCE: Regex =
Regex::new(r"^See merge request .+/.+!\d+$").unwrap();
static ref SUBJECT_WITH_MERGE_ONLY: Regex =
Regex::new(r"Merge [a-z0-9]{40} into [a-z0-9]{40}").unwrap();
}
#[derive(Debug, PartialEq)]
enum CleanupMode {
Strip,
Whitespace,
Verbatim,
Scissors,
Default,
}
pub fn fetch_and_parse_branch() -> Result<Branch, String> {
let output = match run_command("git", &["rev-parse", "--abbrev-ref", "HEAD"]) {
Ok(o) => o,
Err(e) => {
debug!("Failed to fetch Git branch: {:?}", e);
return Err(e.message());
}
};
let name = output.trim().to_string();
let mut branch = Branch::new(name);
branch.validate();
Ok(branch)
}
pub fn fetch_and_parse_commits(selector: &Option<String>) -> Result<Vec<Commit>, String> {
let mut commits = Vec::<Commit>::new();
let commit_format = "%H%n%ae%n%B";
let mut args = vec![
"log".to_string(),
format!(
"--pretty=\
{COMMIT_DELIMITER}%n\
{commit_format}%n\
{COMMIT_TRAILERS_DELIMITER}%n\
%(trailers)%n\
{COMMIT_BODY_DELIMITER}"
),
"--name-only".to_string(),
];
match selector {
Some(selection) => {
let selection = selection.trim().to_string();
if !selection.contains("..") {
args.push("-n 1".to_string());
}
args.push(selection);
}
None => {
args.push("-n 1".to_string());
args.push("HEAD".to_string());
}
};
let output = match run_command("git", &args) {
Ok(o) => o,
Err(e) => {
debug!("Failed to fetch Git log: {:?}", e);
return Err(e.message());
}
};
let messages = output.split(COMMIT_DELIMITER);
for message in messages {
let trimmed_message = message.trim();
if !trimmed_message.is_empty() {
match parse_commit(trimmed_message) {
Some(commit) => commits.push(commit),
None => debug!("Commit ignored: {:?}", message),
}
}
}
Ok(commits)
}
fn parse_commit(message: &str) -> Option<Commit> {
let mut long_sha = None;
let mut email = None;
let mut subject = None;
let mut message_lines = vec![];
let mut message_parts = message.split(COMMIT_TRAILERS_DELIMITER);
match message_parts.next() {
Some(body) => {
for (index, line) in body.lines().enumerate() {
match index {
0 => long_sha = Some(line),
1 => email = Some(line.to_string()),
2 => subject = Some(line),
_ => message_lines.push(line.to_string()),
}
}
}
None => error!("No commit body found!"),
}
let mut extras_str = message_parts
.next()
.unwrap_or("")
.split(COMMIT_BODY_DELIMITER);
let trailers = extras_str.next().unwrap_or("").trim().to_string();
let file_changes_str = extras_str.next().unwrap_or("").trim();
let file_changes = file_changes_str
.lines()
.map(std::string::ToString::to_string)
.collect::<Vec<String>>();
if file_changes.is_empty() {
debug!("No stats found for commit '{}'", long_sha.unwrap_or(""));
} else {
debug!(
"Stats line found for commit '{}': {}",
long_sha.unwrap_or(""),
file_changes.join(", ")
);
}
let message_body_str = message_lines.join("\n");
let message_body = strip_trailers_from_message(&message_body_str, &trailers);
match (long_sha, subject) {
(Some(long_sha), subject) => {
let used_subject = subject.unwrap_or_else(|| {
debug!("Commit subject not present in message: {:?}", message);
""
});
Some(Commit::new(
Some(long_sha.to_string()),
email,
used_subject,
message_body,
trailers,
file_changes,
))
}
_ => {
debug!("Commit ignored: SHA was not present: {}", message);
None
}
}
}
pub fn parse_commit_file(contents: &str) -> Commit {
let (subject, message) = parse_commit_hook_format(contents, &cleanup_mode(), &comment_char());
let trailers = parse_trailers_from_message([subject.to_owned(), message.to_owned()].join("\n"));
let message = strip_trailers_from_message(&message, &trailers);
let file_changes = current_file_changes();
Commit::new(None, None, &subject, message, trailers, file_changes)
}
fn parse_commit_hook_format(
file_contents: &str,
cleanup_mode: &CleanupMode,
comment_char: &str,
) -> (String, String) {
let mut subject = None;
let mut message_lines = vec![];
let scissor_line = format!("{} {}", comment_char, SCISSORS);
debug!("Using clean up mode: {:?}", cleanup_mode);
debug!("Using config core.commentChar: {:?}", comment_char);
for line in file_contents.lines() {
if line == scissor_line {
debug!("Found scissors line. Stop parsing message.");
break;
}
if subject.is_none() {
if cleanup_mode == &CleanupMode::Verbatim {
subject = Some(line.to_string());
} else if let Some(cleaned_line) = cleanup_line(line, cleanup_mode, comment_char) {
if !cleaned_line.is_empty() {
subject = Some(cleaned_line);
}
}
continue;
}
if let Some(cleaned_line) = cleanup_line(line, cleanup_mode, comment_char) {
message_lines.push(cleaned_line);
}
}
let used_subject = subject.unwrap_or_else(|| {
debug!("Commit subject not present in message: {:?}", file_contents);
"".to_string()
});
(used_subject, message_lines.join("\n"))
}
fn parse_trailers_from_message(message: String) -> String {
match run_command_with_stdin("git", &["interpret-trailers", "--only-trailers"], message) {
Ok(stdout) => stdout.trim().to_string(),
Err(e) => {
error!(
"Unable to determine commit message trailers.\nError: {:?}",
e
);
"".to_string()
}
}
}
fn cleanup_line(line: &str, cleanup_mode: &CleanupMode, comment_char: &str) -> Option<String> {
match cleanup_mode {
CleanupMode::Default | CleanupMode::Strip => {
if line.starts_with(comment_char) {
return None;
}
Some(line.trim_end().to_string())
}
CleanupMode::Whitespace | CleanupMode::Scissors => Some(line.trim_end().to_string()),
CleanupMode::Verbatim => Some(line.to_string()),
}
}
pub fn is_commit_ignored(commit: &Commit) -> bool {
let subject = &commit.subject;
let message = &commit.message;
if let Some(email) = &commit.email {
if email.ends_with("[bot]@users.noreply.github.com") {
debug!(
"Ignoring commit because it is from a bot account: {}",
email
);
return true;
}
}
if subject.starts_with("Merge tag ") {
debug!(
"Ignoring commit because it's a merge commit of a tag: {}",
subject
);
return true;
}
if subject.starts_with("Merge pull request") {
debug!(
"Ignoring commit because it's a 'merge pull request' commit: {}",
subject
);
return true;
}
if subject.starts_with("Merge branch ")
&& MESSAGE_CONTAINS_MERGE_REQUEST_REFERENCE.is_match(message)
{
debug!(
"Ignoring commit because it's a 'merge request' commit: {}",
subject
);
return true;
}
if SUBJECT_WITH_SQUASH_PR.is_match(subject) {
debug!(
"Ignoring commit because it's a 'merge pull request' squash commit: {}",
subject
);
return true;
}
if subject.starts_with("Merge branch ") && !SUBJECT_WITH_MERGE_REMOTE_BRANCH.is_match(subject) {
debug!(
"Ignoring commit because it's a local merge commit: {}",
subject
);
return true;
}
if subject.starts_with("Revert \"")
&& subject.ends_with('"')
&& message.contains("This reverts commit ")
{
debug!("Ignoring commit because it's a revert commit: {}", subject);
return true;
}
if SUBJECT_WITH_MERGE_ONLY.is_match(subject) {
debug!(
"Ignoring commit because it's a merge into commit: {}",
subject
);
return true;
}
false
}
fn cleanup_mode() -> CleanupMode {
match run_command("git", &["config", "commit.cleanup"]) {
Ok(stdout) => match stdout.trim() {
"default" | "" => CleanupMode::Default,
"scissors" => CleanupMode::Scissors,
"strip" => CleanupMode::Strip,
"verbatim" => CleanupMode::Verbatim,
"whitespace" => CleanupMode::Whitespace,
option => {
info!(
"Unsupported Git commit.cleanup config: {}\nFalling back on 'default'.",
option
);
CleanupMode::Default
}
},
Err(e) => {
let message = format!(
"Unable to determine Git's commit.cleanup config. \
Falling back on default commit.cleanup config.\nError: {:?}",
e
);
if e.error.is_exit_code(1) {
debug!("{}", message);
} else {
error!("{}", message);
}
CleanupMode::Default
}
}
}
fn comment_char() -> String {
match run_command("git", &["config", "core.commentChar"]) {
Ok(stdout) => {
let character = stdout.trim().to_string();
if character.is_empty() {
debug!("No Git core.commentChar config found. Using default `#` character.");
"#".to_string()
} else {
character
}
}
Err(e) => {
let message = format!(
"Unable to determine Git's core.commentChar config. \
Falling back on default core.commentChar: `#`\nError: {:?}",
e
);
if e.error.is_exit_code(1) {
debug!("{}", message);
} else {
error!("{}", message);
}
"#".to_string()
}
}
}
fn current_file_changes() -> Vec<String> {
match run_command("git", &["diff", "--cached", "--name-only"]) {
Ok(stdout) => stdout
.trim()
.lines()
.map(std::string::ToString::to_string)
.collect::<Vec<String>>(),
Err(e) => {
error!("Unable to determine commit changes.\nError: {:?}", e);
vec![]
}
}
}
pub fn repo_has_changesets() -> bool {
match run_command(
"git",
&[
"ls-files",
"--cached", "--ignored", "--exclude=.changesets/",
"--exclude=**/*/.changesets/", "--exclude=.changeset/",
"--exclude=**/*/.changeset/", ],
) {
Ok(stdout) => {
!stdout.is_empty()
}
Err(e) => {
let message = format!("Unable to read files from Git repository.\nError: {:?}", e);
error!("{}", message);
false
}
}
}
pub fn strip_trailers_from_message(message: &str, trailers: &str) -> String {
let (body, _removed_trailers) = message
.rsplit_once(&trailers) .unwrap_or(("", ""));
body.trim_end().to_string()
}
#[cfg(test)]
mod tests {
use super::Commit;
use super::{
is_commit_ignored, parse_commit, parse_commit_hook_format, strip_trailers_from_message,
CleanupMode, COMMIT_BODY_DELIMITER, COMMIT_TRAILERS_DELIMITER,
};
use crate::config::ValidationContext;
use crate::issue::IssueType;
fn default_context() -> ValidationContext {
ValidationContext { changesets: false }
}
fn assert_commit_is_invalid(commit: &mut Commit) {
commit.validate(&default_context());
assert!(!commit.issues.is_empty());
}
fn assert_commit_is_ignored(result: &Option<Commit>) {
match result {
Some(commit) => assert!(is_commit_ignored(commit)),
None => panic!("Result is not a commit!"),
}
}
fn assert_commit_is_not_ignored(result: &Option<Commit>) {
match result {
Some(commit) => assert!(!is_commit_ignored(commit)),
None => panic!("Result is not a commit!"),
}
}
fn commit_with_file_changes(message: &str) -> String {
format!(
"{}\n{COMMIT_TRAILERS_DELIMITER}\n{COMMIT_BODY_DELIMITER}\n{}",
message, "\nsrc/main.rs\nsrc/utils.rs\n"
)
}
fn commit_without_file_changes(message: &str) -> String {
format!(
"{}\n{COMMIT_TRAILERS_DELIMITER}\n{COMMIT_BODY_DELIMITER}\n{}",
message, ""
)
}
fn commit_with_trailers(message: &str, trailers: &str) -> String {
format!(
"{message}\n\
\n\
{trailers}
{COMMIT_TRAILERS_DELIMITER}\n\
{trailers}\n\
{COMMIT_BODY_DELIMITER}\n\
src/main.rs\n\
README.md\n",
)
}
#[test]
fn test_parse_commit() {
let result = parse_commit(&commit_with_file_changes(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\
test@example.com\n\
This is a subject\n\
\n\
This is my multi line message.\n\
Line 2.",
));
assert_commit_is_not_ignored(&result);
let commit = result.unwrap();
assert_eq!(
commit.long_sha,
Some("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string())
);
assert_eq!(commit.short_sha, Some("aaaaaaa".to_string()));
assert_eq!(commit.email, Some("test@example.com".to_string()));
assert_eq!(commit.subject, "This is a subject");
assert_eq!(commit.message, "\nThis is my multi line message.\nLine 2.");
assert_eq!(commit.trailers, "");
assert_eq!(
commit.file_changes,
vec!["src/main.rs".to_string(), "src/utils.rs".to_string()]
);
assert!(!commit
.issues
.into_iter()
.any(|i| i.r#type == IssueType::Error));
}
#[test]
fn test_parse_commit_with_trailers() {
let result = parse_commit(&commit_with_trailers(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\
test@example.com\n\
This is a subject\n\
\n\
This is a message\n",
"Co-authored-by: Person A <name@domain.com>\n\
Co-authored-by: Person B <name@domain.com>\n",
));
assert_commit_is_not_ignored(&result);
let commit = result.unwrap();
assert_eq!(
commit.long_sha,
Some("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string())
);
assert_eq!(commit.short_sha, Some("aaaaaaa".to_string()));
assert_eq!(commit.email, Some("test@example.com".to_string()));
assert_eq!(commit.subject, "This is a subject");
assert_eq!(commit.message, "\nThis is a message");
assert_eq!(
commit.trailers,
"Co-authored-by: Person A <name@domain.com>\n\
Co-authored-by: Person B <name@domain.com>"
);
assert!(commit.has_changes());
}
#[test]
fn test_parse_commit_with_errors() {
let result = parse_commit(&commit_with_file_changes(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\
test@example.com\n\
This is a subject",
));
assert_commit_is_not_ignored(&result);
let mut commit = result.unwrap();
assert_eq!(
commit.long_sha,
Some("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string())
);
assert_eq!(commit.short_sha, Some("aaaaaaa".to_string()));
assert_eq!(commit.email, Some("test@example.com".to_string()));
assert_eq!(commit.subject, "This is a subject");
assert_eq!(commit.message, "");
assert!(commit.has_changes());
assert_commit_is_invalid(&mut commit);
}
#[test]
fn test_parse_commit_empty() {
let result = parse_commit("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n");
assert_commit_is_not_ignored(&result);
let mut commit = result.unwrap();
assert_eq!(
commit.long_sha,
Some("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string())
);
assert_eq!(commit.short_sha, Some("aaaaaaa".to_string()));
assert_eq!(commit.email, None);
assert_eq!(commit.subject, "");
assert_eq!(commit.message, "");
assert!(!commit.has_changes());
assert_commit_is_invalid(&mut commit);
}
#[test]
fn test_parse_commit_without_file_changes() {
let result = parse_commit(&commit_without_file_changes(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\
test@example.com\n\
This is a subject\n\
\n\
This is a message.",
));
assert_commit_is_not_ignored(&result);
let mut commit = result.unwrap();
assert_eq!(
commit.long_sha,
Some("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string())
);
assert_eq!(commit.short_sha, Some("aaaaaaa".to_string()));
assert_eq!(commit.email, Some("test@example.com".to_string()));
assert_eq!(commit.subject, "This is a subject");
assert_eq!(commit.message, "\nThis is a message.");
assert_eq!(commit.file_changes, Vec::<String>::new());
assert!(!commit.has_changes());
assert_commit_is_invalid(&mut commit);
}
#[test]
fn test_parse_commit_ignore_bot_commit() {
let result = parse_commit(&commit_with_file_changes(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\
12345678+bot-name[bot]@users.noreply.github.com\n\
Commit by bot without description",
));
assert_commit_is_ignored(&result);
}
#[test]
fn test_parse_commit_ignore_tag_merge_commit() {
let result = parse_commit(&commit_with_file_changes(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\
test@example.com\n\
Merge tag 'v1.2.3' into main",
));
assert_commit_is_ignored(&result);
}
#[test]
fn test_parse_commit_ignore_merge_commit_pull_request() {
let result = parse_commit(&commit_with_file_changes(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\
test@example.com\n\
Merge pull request #123 from tombruijn/repo\n\
\n\
This is my multi line message.\n\
Line 2.",
));
assert_commit_is_ignored(&result);
}
#[test]
fn test_parse_commit_ignore_squash_merge_commit_pull_request() {
let result = parse_commit(&commit_with_file_changes(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\
test@example.com\n\
Fix some issue that's squashed (#123)\n\
\n\
This is my multi line message.\n\
Line 2.",
));
assert_commit_is_ignored(&result);
}
#[test]
fn test_parse_commit_ignore_merge_commits_merge_request() {
let result = parse_commit(&commit_with_file_changes(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\
test@example.com\n\
Merge branch 'branch' into main\n\
\n\
This is my multi line message.\n\
Line 2.\n\
\n\
See merge request gitlab-org/repo!123",
));
assert_commit_is_ignored(&result);
let result = parse_commit(&commit_with_file_changes(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\
test@example.com\n\
Fix some issue\n\
\n\
This is my multi line message.\n\
Line 2.\n\
\n\
See merge request !123 for more info about the orignal fix",
));
assert_commit_is_not_ignored(&result);
let result = parse_commit(&commit_with_file_changes(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\
test@example.com\n\
Fix some issue\n\
\n\
This is my multi line message.\n\
Line 2.\n\
\n\
Also See merge request !123",
));
assert_commit_is_not_ignored(&result);
let result = parse_commit(&commit_with_file_changes(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\
test@example.com\n\
Fix some issue\n\
\n\
This is my multi line message.\n\
Line 2. See merge request org/repo!123",
));
assert_commit_is_not_ignored(&result);
}
#[test]
fn test_parse_commit_ignore_merge_commits_without_into() {
let result = parse_commit(&commit_with_file_changes(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\
test@example.com\n\
Merge branch 'branch'",
));
assert_commit_is_ignored(&result);
}
#[test]
fn test_parse_commit_ignore_revert_commit() {
let result = parse_commit(&commit_with_file_changes(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\
test@example.com\n\
Revert \"Some commit\"\n\
\n\
This reverts commit 0d02b90cbf0c79acf9c0b56de00d52389272ec6f",
));
assert_commit_is_ignored(&result);
}
#[test]
fn test_parse_commit_merge_remote_commits() {
let result = parse_commit(&commit_with_file_changes(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\
test@example.com\n\
Merge branch 'branch' of github.com/org/repo into branch",
));
assert_commit_is_not_ignored(&result);
}
#[test]
fn test_parse_commit_merge_into_commit() {
let result = parse_commit(&commit_with_file_changes(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n\
test@example.com\n\
Merge 3af48fbbdf7c2bd77c35e829bc7561fb7c660b21 into 17e2def8fbb2a0d500bffb79c7fe85381f24d415",
));
assert_commit_is_ignored(&result);
}
#[test]
fn test_parse_commit_hook_format() {
let (subject, message) = parse_commit_hook_format(
"This is a subject\n\nThis is a message.",
&CleanupMode::Default,
"#",
);
assert_eq!(subject, "This is a subject");
assert_eq!(message, "\nThis is a message.");
}
#[test]
fn test_parse_commit_hook_format_without_message() {
let (subject, message) =
parse_commit_hook_format("This is a subject", &CleanupMode::Default, "#");
assert_eq!(subject, "This is a subject");
assert_eq!(message, "");
}
#[test]
fn test_parse_commit_hook_format_with_strip() {
let (subject, message) = parse_commit_hook_format(
"This is a subject \n\
\n\
This is the message body. \n\
# This is a commented line.\n\
\n\
Another line.\n\
\n\
# Other things that are not part of the message.\n\
",
&CleanupMode::Strip,
"#",
);
assert_eq!(subject, "This is a subject");
assert_eq!(message, "\nThis is the message body.\n\nAnother line.\n");
}
#[test]
fn test_parse_commit_hook_format_with_leading_empty_lines() {
let (subject, message) = parse_commit_hook_format(
" \n\
This is a subject \n\
\n\
This is the message body. \n\
# This is a commented line.\n\
\n\
Another line.\n\
\n\
# Other things that are not part of the message.\n\
",
&CleanupMode::Strip,
"#",
);
assert_eq!(subject, "This is a subject");
assert_eq!(message, "\nThis is the message body.\n\nAnother line.\n");
}
#[test]
fn test_parse_commit_hook_format_with_leading_comment_lines() {
let (subject, message) = parse_commit_hook_format(
"# This is a comment\n\
This is a subject \n\
\n\
This is the message body. \n\
# This is a commented line.\n\
\n\
Another line.\n\
\n\
# Other things that are not part of the message.\n\
",
&CleanupMode::Strip,
"#",
);
assert_eq!(subject, "This is a subject");
assert_eq!(message, "\nThis is the message body.\n\nAnother line.\n");
}
#[test]
fn test_parse_commit_hook_format_with_strip_custom_comment_char() {
let (subject, message) = parse_commit_hook_format(
"This is a subject \n\
\n\
This is the message body. \n\
- This is a commented line.\n\
\n\
Another line.\n\
\n\
- Other things that are not part of the message.\n\
",
&CleanupMode::Strip,
"-",
);
assert_eq!(subject, "This is a subject");
assert_eq!(message, "\nThis is the message body.\n\nAnother line.\n");
}
#[test]
fn test_parse_commit_hook_format_with_scissors() {
let (subject, message) = parse_commit_hook_format(
"This is a subject \n\
\n\
This is the message body. \n\
\n\
This is line 2.\n\
# ------------------------ >8 ------------------------\n\
Other things that are not part of the message.\n\
",
&CleanupMode::Scissors,
"#",
);
assert_eq!(subject, "This is a subject");
assert_eq!(message, "\nThis is the message body.\n\nThis is line 2.");
}
#[test]
fn test_parse_commit_hook_format_with_scissors_empty_message() {
let (subject, message) = parse_commit_hook_format(
"# ------------------------ >8 ------------------------\n\
Other things that are not part of the message.\n\
",
&CleanupMode::Scissors,
"#",
);
assert_eq!(subject, "");
assert_eq!(message, "");
}
#[test]
fn test_parse_commit_hook_format_with_verbatim() {
let (subject, message) = parse_commit_hook_format(
"This is a subject \n\
\n\
This is the message body.\n\
# This is a comment\n\
# Other things that are not part of the message.\n\
Extra suprise!\
",
&CleanupMode::Verbatim,
"#",
);
assert_eq!(subject, "This is a subject ");
assert_eq!(
message,
"\nThis is the message body.\n\
# This is a comment\n\
# Other things that are not part of the message.\n\
Extra suprise!\
"
);
}
#[test]
fn test_parse_commit_hook_format_with_verbatim_leading_empty_lines() {
let (subject, message) = parse_commit_hook_format(
" \n\
This is the message body.\n\
# This is a comment\n\
# Other things that are not part of the message.\n\
Extra suprise!\
",
&CleanupMode::Verbatim,
"#",
);
assert_eq!(subject, " ");
assert_eq!(
message,
"This is the message body.\n\
# This is a comment\n\
# Other things that are not part of the message.\n\
Extra suprise!\
"
);
}
#[test]
fn test_parse_commit_hook_format_with_whitespace() {
let (subject, message) = parse_commit_hook_format(
"This is a subject \n\
\n\
This is the message body. \n\
\n\
This is line 2.\n\
# This is a comment\n\
# Other things that are not part of the message.\n\
Extra suprise!\
",
&CleanupMode::Whitespace,
"#",
);
assert_eq!(subject, "This is a subject");
assert_eq!(
message,
"\nThis is the message body.\n\
\n\
This is line 2.\n\
# This is a comment\n\
# Other things that are not part of the message.\n\
Extra suprise!\
"
);
}
#[test]
fn test_parse_commit_hook_format_with_whitespace_leading_comment_lines() {
let (subject, message) = parse_commit_hook_format(
"# This is a comment\n\
This is a subject \n\
\n\
This is the message body.",
&CleanupMode::Whitespace,
"#",
);
assert_eq!(subject, "# This is a comment");
assert_eq!(message, "This is a subject\n\nThis is the message body.");
}
#[test]
fn test_parse_commit_hook_format_with_strip_and_scissor_line() {
let (subject, message) = parse_commit_hook_format(
"This is a subject \n\
\n\
This is the message body. \n\
# This is a comment before scissor line\n\
# ------------------------ >8 ------------------------\n\
# Other things that are not part of the message.\n\
List of file changes",
&CleanupMode::Strip,
"#",
);
assert_eq!(subject, "This is a subject");
assert_eq!(message, "\nThis is the message body.");
}
#[test]
fn strip_trailers_from_message_without_trailers() {
let result = strip_trailers_from_message("Subject\n\nMy message body\n", "");
assert_eq!(result, "Subject\n\nMy message body");
}
#[test]
fn strip_trailers_from_message_with_trailers() {
let result = strip_trailers_from_message(
"Subject\n\
\n\
My message body\n
\n\
Co-authored-by: Person A\n",
"Co-authored-by: Person A",
);
assert_eq!(result, "Subject\n\nMy message body");
}
#[test]
fn strip_trailers_from_message_with_multiple_trailers() {
let result = strip_trailers_from_message(
"Subject\n\
\n\
My message body\n
\n\
Co-authored-by: Person A\n\
Signed-off-by: Person B\n\
Fix: #123\n",
"Co-authored-by: Person A\n\
Signed-off-by: Person B\n\
Fix: #123",
);
assert_eq!(result, "Subject\n\nMy message body");
}
#[test]
fn strip_trailers_from_message_with_duplicate_trailers() {
let result = strip_trailers_from_message(
"Subject\n\
\n\
Co-authored-by: Person A\n\
Signed-off-by: Person B\n
My message body\n
\n\
Co-authored-by: Person A\n\
Signed-off-by: Person B\n",
"Co-authored-by: Person A\n\
Signed-off-by: Person B",
);
assert_eq!(
result,
"Subject\n\
\n\
Co-authored-by: Person A\n\
Signed-off-by: Person B\n
My message body"
);
}
}