use crate::diagnostic_log::debug;
use crate::git_builtins::is_git_builtin;
use crate::global_arguments::{GlobalLayout, GlobalOutcome, global_layout};
use crate::js_text::trim_javascript;
use crate::transaction_git::{GitContext, GitRequest, run_git};
use std::ffi::OsString;
use std::path::Path;
pub const MAX_ALIAS_EXPANSIONS: usize = 16;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Route {
Direct,
Alias,
ShellAlias,
Unresolved,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ResolvedCommand {
pub arguments: Vec<OsString>,
pub subcommand_index: usize,
pub subcommand: Option<OsString>,
pub route: Route,
}
impl ResolvedCommand {
pub fn word(&self) -> &[u8] {
match &self.subcommand {
Some(name) => return name.as_encoded_bytes(),
None => return b"",
}
}
pub fn region(&self) -> &[OsString] {
return self.arguments.get(self.subcommand_index + 1..).unwrap_or(&[]);
}
pub fn is_builtin(&self) -> bool {
return matches!(self.route, Route::Direct | Route::Alias) && self.subcommand.is_some();
}
}
fn is_space(byte: u8) -> bool {
return matches!(byte, b' ' | b'\t' | b'\n' | b'\r' | 0x0b | 0x0c);
}
pub fn split_alias_command(value: &[u8]) -> Option<Vec<Vec<u8>>> {
let mut words: Vec<Vec<u8>> = Vec::new();
let mut current: Vec<u8> = Vec::new();
let mut started: bool = true;
let mut quote: u8 = 0;
let mut escaped: bool = false;
for &byte in value {
if escaped {
current.push(byte);
escaped = false;
} else if quote == 0 && is_space(byte) {
if started {
words.push(std::mem::take(&mut current));
}
current.clear();
started = false;
} else if quote == 0 && (byte == b'\'' || byte == b'"') {
quote = byte;
started = true;
} else if quote != 0 && byte == quote {
quote = 0;
} else if byte == b'\\' && quote != b'\'' {
escaped = true;
started = true;
} else {
current.push(byte);
started = true;
}
}
if quote != 0 || escaped {
return None;
}
if started {
words.push(current);
}
return Some(words);
}
pub fn trim_alias_value(value: &[u8]) -> Vec<u8> {
if let Ok(text) = std::str::from_utf8(value) {
return trim_javascript(text).as_bytes().to_vec();
}
let start: usize = value.iter().position(is_not_ascii_space).unwrap_or(value.len());
let end: usize = value.iter().rposition(is_not_ascii_space).map_or(start, next_index);
return value[start..end].to_vec();
}
fn is_not_ascii_space(byte: &u8) -> bool {
return !is_space(*byte);
}
fn next_index(index: usize) -> usize {
return index + 1;
}
fn read_config_value(context: &GitContext, global: &[OsString], key: &OsString) -> Option<Vec<u8>> {
let mut arguments: Vec<OsString> = global.to_vec();
arguments.push(OsString::from("config"));
arguments.push(OsString::from("--get"));
arguments.push(key.clone());
let mut request: GitRequest = GitRequest::new(Path::new("."), arguments.as_slice());
request.without_prefix = true;
match run_git(context, &request) {
Ok(output) if output.succeeded() => return Some(output.stdout),
Ok(_) | Err(_) => {
debug(
"readAliasKey",
format!("no {}", key.to_string_lossy()).as_str(),
);
return None;
}
}
}
pub fn lookup_alias(context: &GitContext, global: &[OsString], name: &OsString) -> Option<Vec<u8>> {
let mut plain: OsString = OsString::from("alias.");
plain.push(name);
if let Some(value) = read_config_value(context, global, &plain) {
return Some(value);
}
let mut spelled: OsString = plain;
spelled.push(".command");
return read_config_value(context, global, &spelled);
}
fn resolved(arguments: &[OsString], layout: &GlobalLayout, route: Route) -> ResolvedCommand {
let subcommand: Option<OsString> = if layout.outcome == GlobalOutcome::Command {
arguments.get(layout.prefix_len).cloned()
} else {
None
};
return ResolvedCommand {
arguments: arguments.to_vec(),
subcommand_index: layout.prefix_len,
subcommand,
route,
};
}
fn os_words(words: Vec<Vec<u8>>) -> Vec<OsString> {
let mut owned: Vec<OsString> = Vec::with_capacity(words.len());
for word in words {
#[cfg(unix)]
{
use std::os::unix::ffi::OsStringExt;
owned.push(OsString::from_vec(word));
}
#[cfg(not(unix))]
{
owned.push(OsString::from(String::from_utf8_lossy(word.as_slice()).into_owned()));
}
}
return owned;
}
pub fn resolve_forwarded_command(context: &GitContext, arguments: &[OsString]) -> ResolvedCommand {
let mut current: Vec<OsString> = arguments.to_vec();
let mut seen: Vec<OsString> = Vec::new();
for _ in 0..=MAX_ALIAS_EXPANSIONS {
let layout: GlobalLayout = global_layout(current.as_slice());
let route: Route = if seen.is_empty() { Route::Direct } else { Route::Alias };
if layout.outcome != GlobalOutcome::Command {
return resolved(current.as_slice(), &layout, route);
}
let name: OsString = current[layout.prefix_len].clone();
if is_git_builtin(name.as_encoded_bytes()) {
return resolved(current.as_slice(), &layout, route);
}
if seen.contains(&name) {
debug(
"resolveForwardedCommand",
format!("alias loop at {}", name.to_string_lossy()).as_str(),
);
return resolved(current.as_slice(), &layout, Route::Unresolved);
}
seen.push(name.clone());
let global: &[OsString] = ¤t[..layout.prefix_len];
let Some(value) = lookup_alias(context, global, &name) else {
return resolved(current.as_slice(), &layout, Route::Unresolved);
};
let trimmed: Vec<u8> = trim_alias_value(value.as_slice());
if trimmed.first() == Some(&b'!') {
return resolved(current.as_slice(), &layout, Route::ShellAlias);
}
let Some(words) = split_alias_command(trimmed.as_slice()) else {
return resolved(current.as_slice(), &layout, Route::Unresolved);
};
if words.is_empty() {
return resolved(current.as_slice(), &layout, Route::Unresolved);
}
let mut expanded: Vec<OsString> = global.to_vec();
expanded.extend(os_words(words));
expanded.extend(current[layout.prefix_len + 1..].iter().cloned());
debug(
"resolveForwardedCommand",
format!("expanded alias {}", name.to_string_lossy()).as_str(),
);
current = expanded;
}
let layout: GlobalLayout = global_layout(current.as_slice());
return resolved(current.as_slice(), &layout, Route::Unresolved);
}
pub fn creates_worktrees(command: &ResolvedCommand) -> bool {
if !command.is_builtin() || command.word() != b"worktree" {
return false;
}
for token in command.region() {
let bytes: &[u8] = token.as_encoded_bytes();
if !bytes.starts_with(b"-") {
return bytes == b"add";
}
}
return false;
}
#[cfg(test)]
#[path = "forwarded_command_tests.rs"]
mod tests;