Files
fuckgit/src/argv.rs
T
swaits a059ee55a2 feat: redirect git invocations to jj inside jj repos
fuckgit installs as a Cargo bin literally named `git` (via
`[[bin]] name = "git"`). When invoked inside a jj worktree by an
AI agent, it prints the equivalent `jj` subcommand plus the live
`jj help <sub>` output to stderr and exits 2 — the AI sees a clear
failure and the right correction. Outside a jj repo, or with no
intercept marker, it is a transparent passthrough.

The intercept gate fires only when:
- effective cwd (after honoring `git -C`) is inside a `.jj` worktree
- FUCKGIT_INTERCEPT=1 or an AI marker (CLAUDECODE) is set
- subcommand is not on the plumbing allowlist
  (rev-parse, cat-file, for-each-ref, ...)
- GIT_DIR / GIT_EXEC_PATH / CARGO are not set
- no top-level --git-dir= / --work-tree= flag is present

Otherwise we walk PATH, skip self by canonicalized current_exe(),
and exec() the first other `git` we find — preserving signals,
exit code, and TTY ownership.

Escape hatches: FUCKGIT_PASSTHROUGH=1 env var, --fucking-git flag
(stripped before exec). FUCKGIT_DEPTH guard aborts at depth >= 2
to prevent self-recursion.

Linux and macOS, zero runtime dependencies.
2026-04-30 01:04:50 -06:00

164 lines
4.0 KiB
Rust

use std::env;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
pub struct ParsedArgv {
pub effective_cwd: PathBuf,
pub has_git_dir_or_work_tree: bool,
pub subcommand: Option<String>,
pub is_version_query: bool,
}
const BOOL_TOP_FLAGS: &[&str] = &[
"-p",
"-P",
"--paginate",
"--no-pager",
"--bare",
"--no-replace-objects",
"--html-path",
"--man-path",
"--info-path",
];
pub fn parse_top_level(argv: &[OsString]) -> ParsedArgv {
let mut effective_cwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let mut has_git_dir_or_work_tree = false;
let mut subcommand: Option<String> = None;
let mut is_version_query = false;
let mut i = 0;
while i < argv.len() {
let arg = &argv[i];
if arg == "-C" {
if let Some(p) = argv.get(i + 1) {
effective_cwd = effective_cwd.join(Path::new(p));
i += 2;
continue;
}
break;
}
if arg == "-c" {
i += 2;
continue;
}
if arg == "--git-dir" || arg == "--work-tree" {
has_git_dir_or_work_tree = true;
i += 2;
continue;
}
let Some(s) = arg.to_str() else { break };
if s.starts_with("--git-dir=") || s.starts_with("--work-tree=") {
has_git_dir_or_work_tree = true;
i += 1;
continue;
}
if s.starts_with("--namespace=")
|| s.starts_with("--config-env=")
|| s.starts_with("--exec-path=")
|| s == "--exec-path"
{
i += 1;
continue;
}
if BOOL_TOP_FLAGS.contains(&s) {
i += 1;
continue;
}
if s == "--version" || s == "-v" {
is_version_query = true;
i += 1;
continue;
}
if s == "--help" || s == "-h" {
subcommand = Some("help".to_string());
break;
}
if !s.starts_with('-') {
subcommand = Some(s.to_string());
break;
}
// Unknown leading flag — bail out; it presumably belongs to a subcommand
// we couldn't identify.
break;
}
ParsedArgv {
effective_cwd,
has_git_dir_or_work_tree,
subcommand,
is_version_query,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(args: &[&str]) -> ParsedArgv {
let owned: Vec<OsString> = args.iter().map(OsString::from).collect();
parse_top_level(&owned)
}
#[test]
fn bare_subcommand() {
let p = parse(&["status"]);
assert_eq!(p.subcommand.as_deref(), Some("status"));
assert!(!p.is_version_query);
assert!(!p.has_git_dir_or_work_tree);
}
#[test]
fn version_query() {
let p = parse(&["--version"]);
assert!(p.is_version_query);
let p = parse(&["-v"]);
assert!(p.is_version_query);
}
#[test]
fn dash_capital_c_changes_cwd() {
let p = parse(&["-C", "/tmp", "status"]);
assert_eq!(p.subcommand.as_deref(), Some("status"));
assert_eq!(p.effective_cwd, PathBuf::from("/tmp"));
}
#[test]
fn git_dir_forces_passthrough_flag() {
let p = parse(&["--git-dir=/foo", "status"]);
assert!(p.has_git_dir_or_work_tree);
let p = parse(&["--work-tree", "/foo", "status"]);
assert!(p.has_git_dir_or_work_tree);
}
#[test]
fn config_kv_consumes_two_args() {
let p = parse(&["-c", "user.name=Foo", "log"]);
assert_eq!(p.subcommand.as_deref(), Some("log"));
}
#[test]
fn paginate_skipped() {
let p = parse(&["--paginate", "log"]);
assert_eq!(p.subcommand.as_deref(), Some("log"));
}
#[test]
fn help_short_and_long() {
assert_eq!(parse(&["--help"]).subcommand.as_deref(), Some("help"));
assert_eq!(parse(&["-h"]).subcommand.as_deref(), Some("help"));
}
}