Files
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

97 lines
3.1 KiB
Markdown

# fuckgit
A binary literally named `git` that, when run inside a [jj][] (Jujutsu) repo,
refuses to actually run git and instead tells you to use `jj`.
[jj]: https://github.com/jj-vcs/jj
## Why
AI coding agents (Claude Code, Cursor, et al.) keep running `git` inside
jj-managed repos despite being told to use `jj`. In a colocated jj+git
layout, that corrupts state. `fuckgit` shadows the real `git` on `PATH`
and traps these invocations, redirecting them with the equivalent `jj`
subcommand and inline `jj help <subcmd>` output.
It works on humans too, when they ask for it.
## Install
```sh
cargo install fuckgit
```
This installs a binary named **`git`** (yes, really) to `~/.cargo/bin/git`.
Make sure `~/.cargo/bin` is ahead of `/usr/bin` on `PATH`.
Linux and macOS only.
## Behavior
By default, `fuckgit` is a silent passthrough — every `git` invocation is
forwarded to the real `git` on `PATH`. The redirect *only* fires when
**all** of these are true:
1. You're inside a jj worktree (a `.jj` directory exists in the cwd or an
ancestor — honoring `git -C <path>`).
2. The intercept gate is open — at least one of:
- `FUCKGIT_INTERCEPT=1` is set, OR
- an AI-agent marker env var is set (currently just `CLAUDECODE`).
3. The subcommand isn't on the plumbing allowlist (`rev-parse`, `cat-file`,
`for-each-ref`, …).
4. None of the safety env vars are set: `GIT_DIR`, `GIT_EXEC_PATH`, `CARGO`.
5. No top-level `--git-dir=` or `--work-tree=` flag is present.
If any check fails, `git` behaves exactly as if `fuckgit` weren't installed.
When the redirect *does* fire, you'll see something like:
```
Don't use git. Use jj.
git status → jj st
jj has no staging area; `jj st` shows the working-copy commit's diff.
--- jj help status ---
Show high-level repo status [default alias: st]
…
Override with FUCKGIT_PASSTHROUGH=1 or git --fucking-git <args...>
```
…and exit `2`.
## Escape hatches
When you need real `git` and the intercept is firing:
- `FUCKGIT_PASSTHROUGH=1 git <args>` — env var override
- `git --fucking-git <args>` — flag override (the flag is stripped before
exec'ing real git)
## Configuration
| Variable | Effect |
|-------------------------|------------------------------------------------------------------|
| `FUCKGIT_INTERCEPT=1` | Open the intercept gate (opt in for your own jj repos). |
| `FUCKGIT_PASSTHROUGH=1` | Force passthrough even with the gate open. |
| `CLAUDECODE` | Set by Claude Code; opens the gate automatically. |
To extend the AI-marker list, edit `AI_MARKERS` in `src/main.rs` and rebuild.
## Supported subcommands
`fuckgit` knows the `jj` equivalent for these `git` subcommands:
`status`, `log`, `commit`, `add`, `checkout`, `switch`, `restore`, `branch`,
`merge`, `rebase`, `pull`, `push`, `fetch`, `clone`, `init`, `diff`, `show`,
`reset`, `revert`, `cherry-pick`, `stash`, `tag`, `blame`, `remote`,
`bisect`, `config`, `reflog`.
Unknown subcommands fall through to a generic "use jj instead" + `jj help`
output.
## License
MIT — see [LICENSE](LICENSE).