const PATHSPEC_SEPARATOR: &str = "--";
use crate::transaction_journal::TransactionMode;
const REDIRECTING_VALUE_OPTIONS: [&str; 3] = ["-C", "--git-dir", "--work-tree"];
const VALUE_TAKING_GLOBAL_OPTIONS: [&str; 7] = [
"-c",
"-C",
"--git-dir",
"--work-tree",
"--namespace",
"--super-prefix",
"--attr-source",
];
const COMMIT_VALUE_OPTIONS: [&str; 20] = [
"-m",
"-F",
"-C",
"-c",
"-t",
"-U",
"--message",
"--file",
"--reuse-message",
"--reedit-message",
"--squash",
"--fixup",
"--author",
"--date",
"--cleanup",
"--trailer",
"--template",
"--unified",
"--inter-hunk-context",
"--pathspec-from-file",
];
const SHORT_VALUE_OPTIONS: [&str; 6] = ["-m", "-F", "-C", "-c", "-t", "-U"];
const OPTIONAL_VALUE_LETTERS: [char; 2] = ['S', 'u'];
fn find_value_option_index(letters: &[char]) -> Option<usize> {
for (index, letter) in letters.iter().enumerate().skip(1) {
let option: String = format!("-{letter}");
if SHORT_VALUE_OPTIONS.contains(&option.as_str()) {
return Some(index);
}
}
return None;
}
fn normalise_inline_short(token: &str) -> Vec<String> {
if !token.starts_with('-') || token.starts_with("--") || token.len() <= 2 {
return vec![String::from(token)];
}
let letters: Vec<char> = token.chars().collect();
let Some(value_index) = find_value_option_index(letters.as_slice()) else {
return vec![String::from(token)];
};
let mut tokens: Vec<String> = letters[1..value_index]
.iter()
.map(|letter| format!("-{letter}"))
.collect();
tokens.push(format!("-{}", letters[value_index]));
let inline: String = letters[value_index + 1..].iter().collect();
if !inline.is_empty() {
tokens.push(inline);
}
return tokens;
}
fn takes_next_token(token: &str) -> bool {
if token.starts_with("--") {
return COMMIT_VALUE_OPTIONS.contains(&token);
}
if !token.starts_with('-') || token.len() < 2 {
return false;
}
let letters: Vec<char> = token.chars().skip(1).collect();
for (index, letter) in letters.iter().enumerate() {
if OPTIONAL_VALUE_LETTERS.contains(letter) {
return false;
}
if SHORT_VALUE_OPTIONS.contains(&format!("-{letter}").as_str()) {
return index + 1 == letters.len();
}
}
return false;
}
pub fn separator_index(region: &[String]) -> Option<usize> {
let mut index: usize = 0;
while index < region.len() {
let token: &str = region[index].as_str();
if token == PATHSPEC_SEPARATOR {
return Some(index);
}
if takes_next_token(token) {
index += 1;
}
index += 1;
}
return None;
}
pub fn normalise_commit_args(args: &[String]) -> Vec<String> {
let separator: Option<usize> = separator_index(args);
let (region, tail): (&[String], &[String]) = match separator {
Some(index) => (&args[..index], &args[index..]),
None => (args, &[]),
};
let mut normalised: Vec<String> = Vec::new();
let mut value_next: bool = false;
for token in region {
if value_next {
normalised.push(token.clone());
value_next = false;
continue;
}
normalised.extend(normalise_inline_short(token));
value_next = takes_next_token(token);
}
normalised.extend(tail.iter().cloned());
return normalised;
}
pub fn split_commit_invocation(args: &[String], subcommand_index: usize) -> (Vec<String>, Vec<String>) {
let globals: &[String] = &args[..subcommand_index];
let mut kept: Vec<String> = Vec::new();
let mut index: usize = 0;
while index < globals.len() {
let token: &str = globals[index].as_str();
let name: &str = if token.starts_with("--") && token.contains('=') {
&token[..token.find('=').unwrap_or(token.len())]
} else {
token
};
let separated: bool = VALUE_TAKING_GLOBAL_OPTIONS.contains(&token);
if REDIRECTING_VALUE_OPTIONS.contains(&name) || token == "--bare" {
if separated {
index += 1;
}
index += 1;
continue;
}
kept.push(String::from(token));
if separated {
if let Some(value) = globals.get(index + 1) {
kept.push(value.clone());
}
index += 1;
}
index += 1;
}
return (kept, args[subcommand_index + 1..].to_vec());
}
pub fn without_all_flag(commit_args: &[String]) -> Vec<String> {
let normalised: Vec<String> = normalise_commit_args(commit_args);
let mut kept: Vec<String> = Vec::new();
let mut value_next: bool = false;
let mut after_separator: bool = false;
for token in normalised {
if after_separator || value_next {
kept.push(token);
value_next = false;
continue;
}
if token == PATHSPEC_SEPARATOR {
after_separator = true;
kept.push(token);
continue;
}
if token == "--all" {
continue;
}
if token.starts_with('-') && !token.starts_with("--") && token.len() > 1 {
let letters: Vec<char> = token[1..].chars().collect();
let value_start: Option<usize> = letters
.iter()
.position(|letter| { return OPTIONAL_VALUE_LETTERS.contains(letter); });
let flags: &[char] = match value_start {
Some(index) => &letters[..index],
None => &letters[..],
};
let mut cluster: String = String::from("-");
for letter in flags.iter().filter(|letter| { return **letter != 'a'; }) {
cluster.push(*letter);
}
if let Some(index) = value_start {
for letter in &letters[index..] {
cluster.push(*letter);
}
}
if cluster == "-" {
continue;
}
value_next = COMMIT_VALUE_OPTIONS.contains(&cluster.as_str());
kept.push(cluster);
continue;
}
value_next = COMMIT_VALUE_OPTIONS.contains(&token.as_str());
kept.push(token);
}
return kept;
}
pub fn private_explicit_commit_args(args: &[String], pathspecs: &[String]) -> Vec<String> {
let mut without_file: Vec<String> = Vec::new();
let mut index: usize = 0;
while index < args.len() {
let token: &str = args[index].as_str();
if token == "--pathspec-from-file" {
index += 2;
continue;
}
if token.starts_with("--pathspec-from-file=") || token == "--pathspec-file-nul" {
index += 1;
continue;
}
without_file.push(String::from(token));
index += 1;
}
for pathspec in pathspecs.iter().rev() {
if let Some(position) = without_file.iter().rposition(|token| { return token == pathspec; }) {
without_file.remove(position);
}
}
let separator: Option<usize> = separator_index(without_file.as_slice());
let mut kept: Vec<String> = Vec::new();
let mut value_next: bool = false;
for (position, token) in without_file.into_iter().enumerate() {
if separator.is_some_and(|at| { return position > at; }) {
kept.push(token);
continue;
}
if separator == Some(position) {
continue;
}
if value_next {
kept.push(token);
value_next = false;
continue;
}
value_next = takes_next_token(token.as_str());
if token == "-o" || token == "--only" {
continue;
}
kept.push(token);
}
return kept;
}
pub fn private_selection_commit_args(args: &[String], pathspecs: &[String]) -> Vec<String> {
return private_explicit_commit_args(args, pathspecs)
.into_iter()
.filter(|token| { return !["-i", "-p", "--include", "--interactive", "--patch"].contains(&token.as_str()); })
.collect();
}
pub fn resolve_private_commit_args(
args: &[String],
pathspecs: &[String],
mode: TransactionMode,
selected_privately: bool,
) -> Vec<String> {
if mode == TransactionMode::ExplicitPath {
return private_explicit_commit_args(args, pathspecs);
}
if selected_privately {
return private_selection_commit_args(args, pathspecs);
}
return args.to_vec();
}
#[cfg(test)]
#[path = "commit_args_tests.rs"]
mod tests;