diff --git a/src/git.rs b/src/git.rs index 8f8d891..be51db5 100644 --- a/src/git.rs +++ b/src/git.rs @@ -19,24 +19,47 @@ pub enum GitCommand { Worktree, } -pub fn run(cmd: GitCommand, args: &[String], max_lines: Option, verbose: u8) -> Result<()> { +/// Create a git Command with global options (e.g. -C, -c, --git-dir, --work-tree) +/// prepended before any subcommand arguments. +fn git_cmd(global_args: &[String]) -> Command { + let mut cmd = Command::new("git"); + for arg in global_args { + cmd.arg(arg); + } + cmd +} + +pub fn run( + cmd: GitCommand, + args: &[String], + max_lines: Option, + verbose: u8, + global_args: &[String], +) -> Result<()> { match cmd { - GitCommand::Diff => run_diff(args, max_lines, verbose), - GitCommand::Log => run_log(args, max_lines, verbose), - GitCommand::Status => run_status(args, verbose), - GitCommand::Show => run_show(args, max_lines, verbose), - GitCommand::Add => run_add(args, verbose), - GitCommand::Commit { message } => run_commit(&message, verbose), - GitCommand::Push => run_push(args, verbose), - GitCommand::Pull => run_pull(args, verbose), - GitCommand::Branch => run_branch(args, verbose), - GitCommand::Fetch => run_fetch(args, verbose), - GitCommand::Stash { subcommand } => run_stash(subcommand.as_deref(), args, verbose), - GitCommand::Worktree => run_worktree(args, verbose), + GitCommand::Diff => run_diff(args, max_lines, verbose, global_args), + GitCommand::Log => run_log(args, max_lines, verbose, global_args), + GitCommand::Status => run_status(args, verbose, global_args), + GitCommand::Show => run_show(args, max_lines, verbose, global_args), + GitCommand::Add => run_add(args, verbose, global_args), + GitCommand::Commit { message } => run_commit(&message, verbose, global_args), + GitCommand::Push => run_push(args, verbose, global_args), + GitCommand::Pull => run_pull(args, verbose, global_args), + GitCommand::Branch => run_branch(args, verbose, global_args), + GitCommand::Fetch => run_fetch(args, verbose, global_args), + GitCommand::Stash { subcommand } => { + run_stash(subcommand.as_deref(), args, verbose, global_args) + } + GitCommand::Worktree => run_worktree(args, verbose, global_args), } } -fn run_diff(args: &[String], max_lines: Option, verbose: u8) -> Result<()> { +fn run_diff( + args: &[String], + max_lines: Option, + verbose: u8, + global_args: &[String], +) -> Result<()> { let timer = tracking::TimedExecution::start(); // Check if user wants stat output @@ -49,7 +72,7 @@ fn run_diff(args: &[String], max_lines: Option, verbose: u8) -> Result<() if wants_stat || !wants_compact { // User wants stat or explicitly no compacting - pass through directly - let mut cmd = Command::new("git"); + let mut cmd = git_cmd(global_args); cmd.arg("diff"); for arg in args { cmd.arg(arg); @@ -77,7 +100,7 @@ fn run_diff(args: &[String], max_lines: Option, verbose: u8) -> Result<() } // Default RTK behavior: stat first, then compacted diff - let mut cmd = Command::new("git"); + let mut cmd = git_cmd(global_args); cmd.arg("diff").arg("--stat"); for arg in args { @@ -95,7 +118,7 @@ fn run_diff(args: &[String], max_lines: Option, verbose: u8) -> Result<() println!("{}", stat_stdout.trim()); // Now get actual diff but compact it - let mut diff_cmd = Command::new("git"); + let mut diff_cmd = git_cmd(global_args); diff_cmd.arg("diff"); for arg in args { diff_cmd.arg(arg); @@ -123,7 +146,12 @@ fn run_diff(args: &[String], max_lines: Option, verbose: u8) -> Result<() Ok(()) } -fn run_show(args: &[String], max_lines: Option, verbose: u8) -> Result<()> { +fn run_show( + args: &[String], + max_lines: Option, + verbose: u8, + global_args: &[String], +) -> Result<()> { let timer = tracking::TimedExecution::start(); // If user wants --stat or --format only, pass through @@ -136,7 +164,7 @@ fn run_show(args: &[String], max_lines: Option, verbose: u8) -> Result<() .any(|arg| arg.starts_with("--pretty") || arg.starts_with("--format")); if wants_stat_only || wants_format { - let mut cmd = Command::new("git"); + let mut cmd = git_cmd(global_args); cmd.arg("show"); for arg in args { cmd.arg(arg); @@ -161,7 +189,7 @@ fn run_show(args: &[String], max_lines: Option, verbose: u8) -> Result<() } // Get raw output for tracking - let mut raw_cmd = Command::new("git"); + let mut raw_cmd = git_cmd(global_args); raw_cmd.arg("show"); for arg in args { raw_cmd.arg(arg); @@ -172,7 +200,7 @@ fn run_show(args: &[String], max_lines: Option, verbose: u8) -> Result<() .unwrap_or_default(); // Step 1: one-line commit summary - let mut summary_cmd = Command::new("git"); + let mut summary_cmd = git_cmd(global_args); summary_cmd.args(["show", "--no-patch", "--pretty=format:%h %s (%ar) <%an>"]); for arg in args { summary_cmd.arg(arg); @@ -187,7 +215,7 @@ fn run_show(args: &[String], max_lines: Option, verbose: u8) -> Result<() println!("{}", summary.trim()); // Step 2: --stat summary - let mut stat_cmd = Command::new("git"); + let mut stat_cmd = git_cmd(global_args); stat_cmd.args(["show", "--stat", "--pretty=format:"]); for arg in args { stat_cmd.arg(arg); @@ -200,7 +228,7 @@ fn run_show(args: &[String], max_lines: Option, verbose: u8) -> Result<() } // Step 3: compacted diff - let mut diff_cmd = Command::new("git"); + let mut diff_cmd = git_cmd(global_args); diff_cmd.args(["show", "--pretty=format:"]); for arg in args { diff_cmd.arg(arg); @@ -295,10 +323,15 @@ pub(crate) fn compact_diff(diff: &str, max_lines: usize) -> String { result.join("\n") } -fn run_log(args: &[String], _max_lines: Option, verbose: u8) -> Result<()> { +fn run_log( + args: &[String], + _max_lines: Option, + verbose: u8, + global_args: &[String], +) -> Result<()> { let timer = tracking::TimedExecution::start(); - let mut cmd = Command::new("git"); + let mut cmd = git_cmd(global_args); cmd.arg("log"); // Check if user provided format flags @@ -523,12 +556,12 @@ fn filter_status_with_args(output: &str) -> String { } } -fn run_status(args: &[String], verbose: u8) -> Result<()> { +fn run_status(args: &[String], verbose: u8, global_args: &[String]) -> Result<()> { let timer = tracking::TimedExecution::start(); // If user provided flags, apply minimal filtering if !args.is_empty() { - let output = Command::new("git") + let output = git_cmd(global_args) .arg("status") .args(args) .output() @@ -557,13 +590,13 @@ fn run_status(args: &[String], verbose: u8) -> Result<()> { // Default RTK compact mode (no args provided) // Get raw git status for tracking - let raw_output = Command::new("git") + let raw_output = git_cmd(global_args) .args(["status"]) .output() .map(|o| String::from_utf8_lossy(&o.stdout).to_string()) .unwrap_or_default(); - let output = Command::new("git") + let output = git_cmd(global_args) .args(["status", "--porcelain", "-b"]) .output() .context("Failed to run git status")?; @@ -585,10 +618,10 @@ fn run_status(args: &[String], verbose: u8) -> Result<()> { Ok(()) } -fn run_add(args: &[String], verbose: u8) -> Result<()> { +fn run_add(args: &[String], verbose: u8, global_args: &[String]) -> Result<()> { let timer = tracking::TimedExecution::start(); - let mut cmd = Command::new("git"); + let mut cmd = git_cmd(global_args); cmd.arg("add"); // Pass all arguments directly to git (flags like -A, -p, --all, etc.) @@ -614,7 +647,7 @@ fn run_add(args: &[String], verbose: u8) -> Result<()> { if output.status.success() { // Count what was added - let status_output = Command::new("git") + let status_output = git_cmd(global_args) .args(["diff", "--cached", "--stat", "--shortstat"]) .output() .context("Failed to check staged files")?; @@ -657,14 +690,14 @@ fn run_add(args: &[String], verbose: u8) -> Result<()> { Ok(()) } -fn run_commit(message: &str, verbose: u8) -> Result<()> { +fn run_commit(message: &str, verbose: u8, global_args: &[String]) -> Result<()> { let timer = tracking::TimedExecution::start(); if verbose > 0 { eprintln!("git commit -m \"{}\"", message); } - let output = Command::new("git") + let output = git_cmd(global_args) .args(["commit", "-m", message]) .output() .context("Failed to run git commit")?; @@ -721,14 +754,14 @@ fn run_commit(message: &str, verbose: u8) -> Result<()> { Ok(()) } -fn run_push(args: &[String], verbose: u8) -> Result<()> { +fn run_push(args: &[String], verbose: u8, global_args: &[String]) -> Result<()> { let timer = tracking::TimedExecution::start(); if verbose > 0 { eprintln!("git push"); } - let mut cmd = Command::new("git"); + let mut cmd = git_cmd(global_args); cmd.arg("push"); for arg in args { cmd.arg(arg); @@ -782,14 +815,14 @@ fn run_push(args: &[String], verbose: u8) -> Result<()> { Ok(()) } -fn run_pull(args: &[String], verbose: u8) -> Result<()> { +fn run_pull(args: &[String], verbose: u8, global_args: &[String]) -> Result<()> { let timer = tracking::TimedExecution::start(); if verbose > 0 { eprintln!("git pull"); } - let mut cmd = Command::new("git"); + let mut cmd = git_cmd(global_args); cmd.arg("pull"); for arg in args { cmd.arg(arg); @@ -867,14 +900,14 @@ fn run_pull(args: &[String], verbose: u8) -> Result<()> { Ok(()) } -fn run_branch(args: &[String], verbose: u8) -> Result<()> { +fn run_branch(args: &[String], verbose: u8, global_args: &[String]) -> Result<()> { let timer = tracking::TimedExecution::start(); if verbose > 0 { eprintln!("git branch"); } - let mut cmd = Command::new("git"); + let mut cmd = git_cmd(global_args); cmd.arg("branch"); // If user passes flags like -d, -D, -m, pass through directly @@ -995,14 +1028,14 @@ fn filter_branch_output(output: &str) -> String { result.join("\n") } -fn run_fetch(args: &[String], verbose: u8) -> Result<()> { +fn run_fetch(args: &[String], verbose: u8, global_args: &[String]) -> Result<()> { let timer = tracking::TimedExecution::start(); if verbose > 0 { eprintln!("git fetch"); } - let mut cmd = Command::new("git"); + let mut cmd = git_cmd(global_args); cmd.arg("fetch"); for arg in args { cmd.arg(arg); @@ -1039,7 +1072,12 @@ fn run_fetch(args: &[String], verbose: u8) -> Result<()> { Ok(()) } -fn run_stash(subcommand: Option<&str>, args: &[String], verbose: u8) -> Result<()> { +fn run_stash( + subcommand: Option<&str>, + args: &[String], + verbose: u8, + global_args: &[String], +) -> Result<()> { let timer = tracking::TimedExecution::start(); if verbose > 0 { @@ -1048,7 +1086,7 @@ fn run_stash(subcommand: Option<&str>, args: &[String], verbose: u8) -> Result<( match subcommand { Some("list") => { - let output = Command::new("git") + let output = git_cmd(global_args) .args(["stash", "list"]) .output() .context("Failed to run git stash list")?; @@ -1067,7 +1105,7 @@ fn run_stash(subcommand: Option<&str>, args: &[String], verbose: u8) -> Result<( timer.track("git stash list", "rtk git stash list", &raw, &filtered); } Some("show") => { - let mut cmd = Command::new("git"); + let mut cmd = git_cmd(global_args); cmd.args(["stash", "show", "-p"]); for arg in args { cmd.arg(arg); @@ -1090,7 +1128,7 @@ fn run_stash(subcommand: Option<&str>, args: &[String], verbose: u8) -> Result<( } Some("pop") | Some("apply") | Some("drop") | Some("push") => { let sub = subcommand.unwrap(); - let mut cmd = Command::new("git"); + let mut cmd = git_cmd(global_args); cmd.args(["stash", sub]); for arg in args { cmd.arg(arg); @@ -1121,7 +1159,7 @@ fn run_stash(subcommand: Option<&str>, args: &[String], verbose: u8) -> Result<( } _ => { // Default: git stash (push) - let mut cmd = Command::new("git"); + let mut cmd = git_cmd(global_args); cmd.arg("stash"); for arg in args { cmd.arg(arg); @@ -1177,7 +1215,7 @@ fn filter_stash_list(output: &str) -> String { result.join("\n") } -fn run_worktree(args: &[String], verbose: u8) -> Result<()> { +fn run_worktree(args: &[String], verbose: u8, global_args: &[String]) -> Result<()> { let timer = tracking::TimedExecution::start(); if verbose > 0 { @@ -1190,7 +1228,7 @@ fn run_worktree(args: &[String], verbose: u8) -> Result<()> { }); if has_action { - let mut cmd = Command::new("git"); + let mut cmd = git_cmd(global_args); cmd.arg("worktree"); for arg in args { cmd.arg(arg); @@ -1225,7 +1263,7 @@ fn run_worktree(args: &[String], verbose: u8) -> Result<()> { } // Default: list mode - let output = Command::new("git") + let output = git_cmd(global_args) .args(["worktree", "list"]) .output() .context("Failed to run git worktree list")?; @@ -1268,13 +1306,13 @@ fn filter_worktree_list(output: &str) -> String { } /// Runs an unsupported git subcommand by passing it through directly -pub fn run_passthrough(args: &[OsString], verbose: u8) -> Result<()> { +pub fn run_passthrough(args: &[OsString], global_args: &[String], verbose: u8) -> Result<()> { let timer = tracking::TimedExecution::start(); if verbose > 0 { eprintln!("git passthrough: {:?}", args); } - let status = Command::new("git") + let status = git_cmd(global_args) .args(args) .status() .context("Failed to run git")?; @@ -1295,6 +1333,48 @@ pub fn run_passthrough(args: &[OsString], verbose: u8) -> Result<()> { mod tests { use super::*; + #[test] + fn test_git_cmd_no_global_args() { + let cmd = git_cmd(&[]); + let program = cmd.get_program(); + assert_eq!(program, "git"); + let args: Vec<_> = cmd.get_args().collect(); + assert!(args.is_empty()); + } + + #[test] + fn test_git_cmd_with_directory() { + let global_args = vec!["-C".to_string(), "/tmp".to_string()]; + let cmd = git_cmd(&global_args); + let args: Vec<_> = cmd.get_args().collect(); + assert_eq!(args, vec!["-C", "/tmp"]); + } + + #[test] + fn test_git_cmd_with_multiple_global_args() { + let global_args = vec![ + "-C".to_string(), + "/tmp".to_string(), + "-c".to_string(), + "user.name=test".to_string(), + "--git-dir".to_string(), + "/foo/.git".to_string(), + ]; + let cmd = git_cmd(&global_args); + let args: Vec<_> = cmd.get_args().collect(); + assert_eq!( + args, + vec![ + "-C", + "/tmp", + "-c", + "user.name=test", + "--git-dir", + "/foo/.git" + ] + ); + } + #[test] fn test_compact_diff() { let diff = r#"diff --git a/foo.rs b/foo.rs diff --git a/src/main.rs b/src/main.rs index 0e58173..dc40614 100644 --- a/src/main.rs +++ b/src/main.rs @@ -121,6 +121,22 @@ enum Commands { /// Git commands with compact output Git { + /// Change to directory before executing (like git -C , can be repeated) + #[arg(short = 'C', action = clap::ArgAction::Append)] + directory: Vec, + + /// Git configuration override (like git -c key=value, can be repeated) + #[arg(short = 'c', action = clap::ArgAction::Append)] + config_override: Vec, + + /// Set the path to the .git directory + #[arg(long = "git-dir")] + git_dir: Option, + + /// Set the path to the working tree + #[arg(long = "work-tree")] + work_tree: Option, + #[command(subcommand)] command: GitCommands, }, @@ -874,52 +890,134 @@ fn main() -> Result<()> { local_llm::run(&file, &model, force_download, cli.verbose)?; } - Commands::Git { command } => match command { - GitCommands::Diff { args } => { - git::run(git::GitCommand::Diff, &args, None, cli.verbose)?; - } - GitCommands::Log { args } => { - git::run(git::GitCommand::Log, &args, None, cli.verbose)?; - } - GitCommands::Status { args } => { - git::run(git::GitCommand::Status, &args, None, cli.verbose)?; - } - GitCommands::Show { args } => { - git::run(git::GitCommand::Show, &args, None, cli.verbose)?; - } - GitCommands::Add { args } => { - git::run(git::GitCommand::Add, &args, None, cli.verbose)?; - } - GitCommands::Commit { message } => { - git::run(git::GitCommand::Commit { message }, &[], None, cli.verbose)?; - } - GitCommands::Push { args } => { - git::run(git::GitCommand::Push, &args, None, cli.verbose)?; - } - GitCommands::Pull { args } => { - git::run(git::GitCommand::Pull, &args, None, cli.verbose)?; - } - GitCommands::Branch { args } => { - git::run(git::GitCommand::Branch, &args, None, cli.verbose)?; + Commands::Git { + directory, + config_override, + git_dir, + work_tree, + command, + } => { + // Build global git args (inserted between "git" and subcommand) + let mut global_args: Vec = Vec::new(); + for dir in &directory { + global_args.push("-C".to_string()); + global_args.push(dir.clone()); } - GitCommands::Fetch { args } => { - git::run(git::GitCommand::Fetch, &args, None, cli.verbose)?; + for cfg in &config_override { + global_args.push("-c".to_string()); + global_args.push(cfg.clone()); } - GitCommands::Stash { subcommand, args } => { - git::run( - git::GitCommand::Stash { subcommand }, - &args, - None, - cli.verbose, - )?; + if let Some(ref dir) = git_dir { + global_args.push("--git-dir".to_string()); + global_args.push(dir.clone()); } - GitCommands::Worktree { args } => { - git::run(git::GitCommand::Worktree, &args, None, cli.verbose)?; + if let Some(ref tree) = work_tree { + global_args.push("--work-tree".to_string()); + global_args.push(tree.clone()); } - GitCommands::Other(args) => { - git::run_passthrough(&args, cli.verbose)?; + + match command { + GitCommands::Diff { args } => { + git::run( + git::GitCommand::Diff, + &args, + None, + cli.verbose, + &global_args, + )?; + } + GitCommands::Log { args } => { + git::run(git::GitCommand::Log, &args, None, cli.verbose, &global_args)?; + } + GitCommands::Status { args } => { + git::run( + git::GitCommand::Status, + &args, + None, + cli.verbose, + &global_args, + )?; + } + GitCommands::Show { args } => { + git::run( + git::GitCommand::Show, + &args, + None, + cli.verbose, + &global_args, + )?; + } + GitCommands::Add { args } => { + git::run(git::GitCommand::Add, &args, None, cli.verbose, &global_args)?; + } + GitCommands::Commit { message } => { + git::run( + git::GitCommand::Commit { message }, + &[], + None, + cli.verbose, + &global_args, + )?; + } + GitCommands::Push { args } => { + git::run( + git::GitCommand::Push, + &args, + None, + cli.verbose, + &global_args, + )?; + } + GitCommands::Pull { args } => { + git::run( + git::GitCommand::Pull, + &args, + None, + cli.verbose, + &global_args, + )?; + } + GitCommands::Branch { args } => { + git::run( + git::GitCommand::Branch, + &args, + None, + cli.verbose, + &global_args, + )?; + } + GitCommands::Fetch { args } => { + git::run( + git::GitCommand::Fetch, + &args, + None, + cli.verbose, + &global_args, + )?; + } + GitCommands::Stash { subcommand, args } => { + git::run( + git::GitCommand::Stash { subcommand }, + &args, + None, + cli.verbose, + &global_args, + )?; + } + GitCommands::Worktree { args } => { + git::run( + git::GitCommand::Worktree, + &args, + None, + cli.verbose, + &global_args, + )?; + } + GitCommands::Other(args) => { + git::run_passthrough(&args, &global_args, cli.verbose)?; + } } - }, + } Commands::Gh { subcommand, args } => { gh_cmd::run(&subcommand, &args, cli.verbose, cli.ultra_compact)?;