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.
This commit is contained in:
12 files changed
+781
No files matched your search
+163
@@ -0,0 +1,163 @@
|
||||
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"));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user