use crate::transaction_git_request_and_reply_types::GitRequest;
use std::ffi::OsString;
const RERUNNABLE: [&str; 15] = [
"rev-parse",
"for-each-ref",
"ls-files",
"diff",
"diff-index",
"diff-tree",
"cat-file",
"show",
"hash-object",
"write-tree",
"ls-remote",
"merge-tree",
"pack-objects",
"index-pack",
"version",
];
pub(crate) fn subcommand_of(arguments: &[OsString]) -> Option<(usize, String)> {
let mut index: usize = 0;
while index < arguments.len() {
let text: String = arguments[index].to_string_lossy().into_owned();
if text == "-c" || text == "-C" {
index += 2;
continue;
}
if text.starts_with('-') {
index += 1;
continue;
}
return Some((index, text));
}
return None;
}
fn object_format_is_known(option: &str) -> bool {
return match option.strip_prefix("--object-format=") {
Some(name) => matches!(name, "sha1" | "sha256"),
None => false,
};
}
fn is_shadow_creation(words: &[String]) -> bool {
return match words {
[bare, template, format, refs, path] => {
bare == "--bare"
&& template == "--template="
&& object_format_is_known(format)
&& refs == "--ref-format=reftable"
&& !path.starts_with('-')
}
_ => false,
};
}
fn symbolic_ref_may_be_rerun(words: &Words<'_>, shadow: bool) -> bool {
if words.after.is_some() {
return false;
}
let read: bool = words.only_options(&["--quiet", "-q", "--short", "--no-recurse"]) && words.before.len() == 1;
let write_in_shadow: bool = shadow && words.options.is_empty() && words.before.len() == 2;
return read || write_in_shadow;
}
fn words_after(request: &GitRequest, position: usize) -> Vec<String> {
let mut words: Vec<String> = Vec::new();
for argument in &request.arguments[position + 1..] {
words.push(argument.to_string_lossy().into_owned());
}
return words;
}
fn is_read_option(option: &str) -> bool {
return matches!(option, "--get" | "--get-all" | "--get-regexp" | "--list" | "-l");
}
pub(crate) fn may_be_rerun(request: &GitRequest) -> bool {
let Some((position, name)) = subcommand_of(request.arguments.as_slice()) else {
return false;
};
let rest: Vec<String> = words_after(request, position);
match name.as_str() {
"config" => return rest.iter().map(String::as_str).any(is_read_option),
"remote" => return rest.first().map(String::as_str) == Some("get-url"),
"add" | "reset" | "read-tree" | "update-index" => return request.index.is_some(),
"symbolic-ref" => return symbolic_ref_may_be_rerun(&Words::of(rest.as_slice()), request.without_prefix),
other => return RERUNNABLE.contains(&other),
}
}
const READS_AND_OBJECT_WRITERS: [&str; 15] = [
"rev-list",
"merge-base",
"log",
"ls-tree",
"status",
"var",
"diff-files",
"check-attr",
"check-ignore",
"stripspace",
"fmt-merge-msg",
"show-ref",
"name-rev",
"commit-tree",
"mktree",
];
struct Words<'a> {
options: Vec<&'a str>,
before: Vec<&'a str>,
after: Option<Vec<&'a str>>,
}
impl<'a> Words<'a> {
fn of(words: &'a [String]) -> Self {
let mut sorted: Words<'a> = Words { options: Vec::new(), before: Vec::new(), after: None };
for word in words.iter().map(String::as_str) {
match sorted.after.as_mut() {
Some(after) => after.push(word),
None if word == "--" => sorted.after = Some(Vec::new()),
None if word.starts_with('-') && word.len() > 1 => sorted.options.push(word),
None => sorted.before.push(word),
}
}
return sorted;
}
fn only_options(&self, allowed: &[&str]) -> bool {
for option in &self.options {
if !allowed.contains(option) {
return false;
}
}
return true;
}
fn names_paths(&self) -> bool {
return match &self.after {
Some(paths) => !paths.is_empty(),
None => false,
};
}
fn count_present(&self, wanted: &[&str]) -> usize {
let mut present: usize = 0;
for option in wanted {
if self.has(option) {
present += 1;
}
}
return present;
}
fn has(&self, wanted: &str) -> bool {
return self.options.contains(&wanted);
}
}
pub(crate) const NO_REFLOG_SETTING: &str = "core.logAllRefUpdates=false";
fn is_restore_option(option: &&str) -> bool {
return matches!(*option, "--staged" | "--worktree" | "--quiet" | "-q") || option.starts_with("--source=");
}
fn is_proven_to_run_twice(name: &str, rest: &[String]) -> bool {
let words: Words<'_> = Words::of(rest);
let no_paths = words.after.is_none();
return match name {
"stash" => rest.first().map(String::as_str) == Some("list") && rest.len() == 2 && rest[1] == "--format=%H",
"clean" => no_paths && words.before.is_empty() && words.only_options(&["--force", "-d"]) && words.has("--force") && words.has("-d"),
"read-tree" => no_paths && words.before.len() == 1 && words.only_options(&["--reset", "-u"]) && words.has("--reset") && words.has("-u"),
"update-index" => {
let refresh: bool = no_paths && words.before.is_empty() && words.options == ["--refresh"];
let index_info: bool = no_paths && words.before.is_empty() && words.has("--index-info") && words.only_options(&["--index-info", "-z"]);
let cache_info: bool = no_paths && words.before.len() == 1 && words.only_options(&["--add", "--cacheinfo"]) && words.has("--add") && words.has("--cacheinfo");
refresh || index_info || cache_info
}
"add" => {
let one_of: usize = words.count_present(&["--update", "-u", "--all", "-A"]);
words.only_options(&["--update", "-u", "--all", "-A"]) && one_of == 1
}
"checkout-index" => no_paths && words.before.is_empty() && words.only_options(&["--all", "-a", "--force", "-f"]) && (words.has("--all") || words.has("-a")) && (words.has("--force") || words.has("-f")),
"restore" => {
let options_known: bool = words.options.iter().all(is_restore_option);
options_known && words.before.is_empty() && words.names_paths()
}
"reset" => words.only_options(&["--quiet", "-q"]) && words.before.len() == 1 && words.names_paths(),
"config" => no_paths && words.only_options(&["--local"]) && words.before.len() == 2,
_ => false,
};
}
pub(crate) fn may_be_ended(request: &GitRequest) -> bool {
if may_be_rerun(request) {
return true;
}
let Some((position, name)) = subcommand_of(request.arguments.as_slice()) else {
return false;
};
if READS_AND_OBJECT_WRITERS.contains(&name.as_str()) {
return true;
}
let rest: Vec<String> = words_after(request, position);
if name == "init" {
return request.without_prefix && is_shadow_creation(&rest);
}
return is_proven_to_run_twice(name.as_str(), rest.as_slice());
}
pub(crate) fn is_side_ref_write(request: &GitRequest) -> bool {
let Some((position, name)) = subcommand_of(request.arguments.as_slice()) else { return false };
if name != "update-ref" || request.input.is_some() {
return false;
}
let rest: Vec<String> = words_after(request, position);
let words: Words<'_> = Words::of(rest.as_slice());
let Some(first_word) = words.before.first() else {
return false;
};
return words.after.is_none() && words.only_options(&["-d", "--no-deref"]) && is_side_ref_name(first_word);
}
fn is_side_ref_name(word: &str) -> bool {
for prefix in ["refs/cli-git/kept/", "refs/worktree/cli-git-service/"] {
if let Some(number) = word.strip_prefix(prefix)
&& !number.is_empty()
&& number.as_bytes().iter().all(u8::is_ascii_digit)
{
return true;
}
}
return false;
}