use super::{
CommitOnlyDecision, CommitOnlyViolationCode, decide_commit_only, resolve_index_state,
resolve_sequencer_state,
};
use crate::command_test_support::os_arguments;
use crate::rule_commit_index::IndexVsHead;
use crate::rule_commit_sequencer::SequencerState;
use std::ffi::OsString;
fn decide(values: &[&str]) -> CommitOnlyDecision {
return decide_commit_only(os_arguments(values).as_slice(), &[]).expect("Git accepts");
}
fn rewritten(values: &[&str]) -> CommitOnlyDecision {
return CommitOnlyDecision::Rewritten(os_arguments(values));
}
fn decide_with_index(values: &[&str], state: IndexVsHead) -> CommitOnlyDecision {
let arguments: Vec<OsString> = os_arguments(values);
let first: CommitOnlyDecision =
decide_commit_only(arguments.as_slice(), &[]).expect("Git accepts");
if let CommitOnlyDecision::NeedsIndexState(pending) = &first {
return resolve_index_state(arguments.as_slice(), pending, state);
}
panic!("{values:?} was decided without the index state: {first:?}");
}
fn decide_with_sequencer(values: &[&str], state: SequencerState) -> CommitOnlyDecision {
let first: CommitOnlyDecision = decide(values);
assert_eq!(first, CommitOnlyDecision::NeedsSequencerState, "{values:?}");
return resolve_sequencer_state(state);
}
fn rejection(decision: CommitOnlyDecision) -> (CommitOnlyViolationCode, String) {
if let CommitOnlyDecision::Rejected(violation) = &decision {
return (violation.code, violation.message.clone());
}
panic!("expected a rejection, got {decision:?}");
}
#[test]
fn injects_only_after_commit_and_nowhere_else() {
assert_eq!(
decide(&["status", "--short"]),
CommitOnlyDecision::Unchanged
);
assert_eq!(
decide(&["commit", "-m", "message", "file.ts"]),
rewritten(&["commit", "-o", "-m", "message", "file.ts"])
);
assert_eq!(
decide(&["-C", "/tmp/repo", "commit", "-m", "message", "file.ts"]),
rewritten(&[
"-C",
"/tmp/repo",
"commit",
"-o",
"-m",
"message",
"file.ts"
])
);
}
#[test]
fn strips_the_escape_hatch_and_skips_validation() {
assert_eq!(
decide(&["commit", "--no-enforce-only", "-am", "message"]),
rewritten(&["commit", "-am", "message"])
);
}
#[test]
fn skips_injection_for_an_explicit_only_choice() {
for values in [
vec!["commit", "-o", "-m", "message", "file.ts"],
vec!["commit", "-om", "message", "file.ts"],
vec!["commit", "--no-only", "-m", "message"],
] {
assert_eq!(
decide(values.as_slice()),
CommitOnlyDecision::Unchanged,
"{values:?}"
);
}
}
#[test]
fn reads_values_and_pathspecs_by_position() {
for (values, expected) in [
(
vec!["commit", "-m", "-a", "file.ts"],
vec!["commit", "-o", "-m", "-a", "file.ts"],
),
(
vec!["commit", "-m", "message", "--", "-dash-file"],
vec!["commit", "-o", "-m", "message", "--", "-dash-file"],
),
(
vec!["commit", "-m", "message", "-"],
vec!["commit", "-o", "-m", "message", "-"],
),
] {
assert_eq!(
decide(values.as_slice()),
rewritten(expected.as_slice()),
"{values:?}"
);
}
}
#[test]
fn rejects_a_pathless_amend_or_empty_commit_over_a_dirty_index() {
let (amend_code, amend_message): (CommitOnlyViolationCode, String) = rejection(
decide_with_index(&["commit", "--amend", "--no-edit"], IndexVsHead::Differs),
);
assert_eq!(amend_code, CommitOnlyViolationCode::StagedChangesIgnored);
assert!(amend_message.contains(
"git commit --amend without pathspecs would silently ignore your staged changes"
));
assert!(amend_message.contains("--no-only"));
let (_, empty_message): (CommitOnlyViolationCode, String) = rejection(decide_with_index(
&["commit", "--allow-empty", "-m", "message"],
IndexVsHead::Differs,
));
assert!(empty_message.contains(
"git commit --allow-empty without pathspecs would silently ignore your staged changes"
));
}
#[test]
fn injects_for_a_pathless_amend_over_a_clean_or_unknown_index() {
for (values, state, expected) in [
(
vec!["commit", "--amend", "-m", "message"],
IndexVsHead::Unknown,
vec!["commit", "-o", "--amend", "-m", "message"],
),
(
vec!["commit", "--amend", "-m", "message"],
IndexVsHead::Matches,
vec!["commit", "-o", "--amend", "-m", "message"],
),
(
vec!["commit", "--allow-empty", "-m", "message"],
IndexVsHead::Matches,
vec!["commit", "-o", "--allow-empty", "-m", "message"],
),
] {
assert_eq!(
decide_with_index(values.as_slice(), state),
rewritten(expected.as_slice()),
"{values:?}"
);
}
}
#[test]
fn skips_the_index_check_when_paths_or_an_only_choice_are_given() {
for line in [
"commit --amend --no-edit file.ts",
"commit --amend --pathspec-from-file paths.txt -m message",
"commit --pathspec-from-file paths.txt -m message",
"commit --pathspec-from-file=paths.txt -m message",
] {
let values: Vec<&str> = line.split(' ').collect();
let mut expected: Vec<&str> = vec!["commit", "-o"];
expected.extend_from_slice(&values[1..]);
assert_eq!(
decide(values.as_slice()),
rewritten(expected.as_slice()),
"{line}"
);
}
for values in [
vec!["commit", "--amend", "--no-only", "-m", "message"],
vec!["commit", "-o", "--amend", "-m", "message"],
] {
assert_eq!(
decide(values.as_slice()),
CommitOnlyDecision::Unchanged,
"{values:?}"
);
}
}
#[test]
fn skips_injection_in_include_mode() {
for values in [
vec!["commit", "-i", "-m", "message", "file.ts"],
vec!["commit", "--include", "-m", "message", "file.ts"],
vec!["commit", "--inc", "-m", "message", "file.ts"],
vec!["commit", "-im", "message", "file.ts"],
] {
assert_eq!(
decide(values.as_slice()),
CommitOnlyDecision::Unchanged,
"{values:?}"
);
}
}
#[test]
fn asks_for_the_sequencer_state_only_for_a_pathless_commit() {
assert_eq!(
decide_with_sequencer(
&["commit", "-m", "merge main into feature"],
SequencerState::InProgress
),
CommitOnlyDecision::Unchanged
);
for values in [
vec!["commit", "-m", "message"],
vec![
"commit",
"--author",
"Author <author@example.invalid>",
"-m",
"message",
],
] {
let (code, message): (CommitOnlyViolationCode, String) = rejection(decide_with_sequencer(
values.as_slice(),
SequencerState::NotInProgress,
));
assert_eq!(
code,
CommitOnlyViolationCode::PathspecRequired,
"{values:?}"
);
assert!(
message.contains("requires an explicit pathspec"),
"{values:?}"
);
assert!(message.contains("--no-only"), "{values:?}");
}
}
#[test]
fn rejects_the_all_flag_from_arguments_alone() {
for values in [
vec!["commit", "-a", "-m", "message"],
vec!["commit", "--all", "-m", "message"],
vec!["commit", "-am", "message"],
] {
let (code, message): (CommitOnlyViolationCode, String) =
rejection(decide(values.as_slice()));
assert_eq!(code, CommitOnlyViolationCode::AllFlag, "{values:?}");
assert!(message.contains("rejects -a/--all"), "{values:?}");
}
}
#[test]
fn detects_a_pathspec_after_flags_and_value_options() {
for values in [
vec!["commit", "-q", "file.ts", "-F", "message.txt"],
vec!["commit", "-v", "file.ts", "-m", "message"],
vec!["commit", "-n", "file.ts", "-m", "message"],
vec!["commit", "--no-verify", "file.ts", "-m", "message"],
vec!["commit", "--dry-run", "file.ts", "-m", "message"],
vec![
"commit",
"--author",
"Author <author@example.invalid>",
"file.ts",
"-m",
"message",
],
vec!["commit", "--cleanup", "strip", "file.ts", "-m", "message"],
vec![
"commit",
"--trailer",
"Reviewed-by: A <a@example.invalid>",
"file.ts",
"-m",
"m",
],
vec!["commit", "--fixup", "HEAD", "file.ts"],
] {
let mut expected: Vec<&str> = vec!["commit", "-o"];
expected.extend_from_slice(&values[1..]);
assert_eq!(
decide(values.as_slice()),
rewritten(expected.as_slice()),
"{values:?}"
);
}
}