Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 110 additions & 4 deletions src/commands/git_handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,69 @@ struct CommandHooksContext {
pre_commit_hook_result: Option<bool>,
}

/// Return the alias definition for a given command name (if any) by consulting
/// `git config alias.<name>` with the same global args as the invocation.
/// Returns `None` if no alias is configured.
fn get_alias_for_command(global_args: &[String], name: &str) -> Option<String> {
// Build: <global_args> + ["config", "--get", format!("alias.{}", name)]
let mut args: Vec<String> = Vec::with_capacity(global_args.len() + 4);
args.extend(global_args.iter().cloned());
args.push("config".to_string());
args.push("--get".to_string());
args.push(format!("alias.{}", name));

match Command::new(config::Config::get().git_cmd()).args(&args).output() {
Ok(output) if output.status.success() => {
let s = String::from_utf8_lossy(&output.stdout).trim().to_string();
if s.is_empty() { None } else { Some(s) }
}
_ => None,
}
}

/// Tokenize a git alias definition into argv-like tokens, handling simple
/// shell-style quotes and backslash escapes similarly to git's split_cmdline.
/// Returns None on unterminated quotes to avoid unsafe rewrites.
fn tokenize_alias(definition: &str) -> Option<Vec<String>> {
let mut tokens: Vec<String> = Vec::new();
let mut current = String::new();
let mut in_single = false;
let mut in_double = false;
let mut chars = definition.chars().peekable();

while let Some(ch) = chars.next() {
match ch {
'\'' => {
if !in_double { in_single = !in_single; } else { current.push(ch); }
}
'"' => {
if !in_single { in_double = !in_double; } else { current.push(ch); }
}
'\\' => {
if in_single {
// Backslash is literal inside single quotes
current.push('\\');
} else {
if let Some(next) = chars.next() { current.push(next); } else { current.push('\\'); }
}
}
c if c.is_whitespace() => {
if in_single || in_double {
current.push(c);
} else if !current.is_empty() {
tokens.push(current.clone());
current.clear();
}
}
_ => current.push(ch),
}
}

if in_single || in_double { return None; }
if !current.is_empty() { tokens.push(current); }
Some(tokens)
}

pub fn handle_git(args: &[String]) {
// If we're being invoked from a shell completion context, bypass git-ai logic
// and delegate directly to the real git so existing completion scripts work.
Expand All @@ -60,11 +123,54 @@ pub fn handle_git(args: &[String]) {
return;
}

let mut command_hooks_context = CommandHooksContext {
pre_commit_hook_result: None,
};
let mut command_hooks_context = CommandHooksContext { pre_commit_hook_result: None };

// First parse of raw args (may contain an alias as the command token)
let initial_parsed = parse_git_cli_args(args);

let parsed_args = parse_git_cli_args(args);
// Single-pass alias expansion: if the command is an alias, expand it once.
// For external aliases (starting with '!'), bypass hooks entirely and
// delegate to git immediately with the original args.
let parsed_args = if let Some(cmd) = initial_parsed.command.as_deref() {
if let Some(alias_def) = get_alias_for_command(&initial_parsed.global_args, cmd) {
let trimmed = alias_def.trim_start();
if trimmed.starts_with('!') {
// External command alias: run real git immediately, no hooks.
debug_log("Detected external git alias; bypassing hooks and delegating to git");
let orig = initial_parsed.to_invocation_vec();
let status = proxy_to_git(&orig, false);
exit_with_status(status);
}
// Tokenize alias and build a new argv: globals + [alias tokens] + original command args
if let Some(mut alias_tokens) = tokenize_alias(trimmed) {
if !alias_tokens.is_empty() {
let mut expanded: Vec<String> = Vec::with_capacity(
initial_parsed.global_args.len()
+ usize::from(initial_parsed.saw_end_of_opts)
+ alias_tokens.len()
+ initial_parsed.command_args.len(),
);
expanded.extend(initial_parsed.global_args.iter().cloned());
if initial_parsed.saw_end_of_opts {
expanded.push("--".to_string());
}
expanded.append(&mut alias_tokens);
expanded.extend(initial_parsed.command_args.iter().cloned());
// Re-parse the expanded argv once; do not attempt to expand again.
parse_git_cli_args(&expanded)
} else {
initial_parsed.clone()
}
} else {
// Failed to safely tokenize; fall back to original to avoid incorrect behavior.
initial_parsed.clone()
}
} else {
initial_parsed.clone()
}
} else {
initial_parsed.clone()
};
// println!("command: {:?}", parsed_args.command);
// println!("global_args: {:?}", parsed_args.global_args);
// println!("command_args: {:?}", parsed_args.command_args);
Expand Down
Loading