3.7 KiB
Co-located jj + git Repositories
A co-located repo has both .jj/ and .git/ at the same root. jj manages local operations; git stays in sync underneath so remote-side tooling (CI, GitHub, hooks, IDE plugins, gh CLI) continues to work normally.
This is the strongly recommended setup for working with an existing git project.
Why co-located
- Local: jj's superior model. Mutable commits, no staging area, the op log as a safety net.
- Remote: still git. GitHub sees ordinary commits and refs. Collaborators don't need to know you use jj.
- Tooling parity. IDE git integrations, pre-commit hooks,
gh/gitCLI for read-only inspection all continue to work.
Creating a co-located repo
# From scratch
jj git init --colocate
# From a remote
jj git clone <url> --colocate
# Adopt an existing git repo (run from inside it)
cd existing-git-repo
jj git init --colocate
After this, both .jj/ and .git/ exist and stay synchronized automatically. Never run jj git init without --colocate on a project you already have git history for — non-colocated init creates a separate git store inside .jj/repo/store/git which is harder to work with.
The mutation rule
Never use
gitfor mutations in a co-located jj repo. Read-onlygitis fine.
| git command | Allowed in jj repo? |
|---|---|
git log, git show, git diff, git blame, git grep |
✅ Allowed (jj equivalents preferred) |
git status |
✅ Allowed (shows git's view; may differ from jj st) |
git commit, git add, git stash, git reset |
❌ Forbidden — corrupts jj state |
git checkout <branch>, git switch |
❌ Forbidden — use jj edit instead |
git rebase, git merge, git cherry-pick |
❌ Forbidden — use jj rebase, jj new <a> <b>, jj duplicate |
git fetch |
⚠️ Use jj git fetch — it keeps jj in sync |
git pull |
❌ Forbidden — git pull does fetch+merge; use jj git fetch + jj rebase |
git push |
❌ Forbidden — use jj git push -b <bookmark> |
The reason: jj snapshots the working copy on every jj command. If git mutates state behind jj's back, jj's next snapshot will be confused about what changed when, and the op log can get out of sync.
Switching to git mode (rare; usually a smell)
If you genuinely need to use a git workflow for one operation (e.g., an external tool insists), do it carefully:
# 1. Ensure jj working copy is committed and clean
jj st
# (resolve anything pending — describe, refine, etc.)
# 2. Now run the git operation
git <something>
# 3. Return to jj — jj will re-sync on the next command
jj st # Triggers re-sync
jj edit <change-id> # Resume work
Almost every git operation has a jj equivalent that's safer. Reach for git mode only as a last resort.
Common gotchas
- Git complains about uncommitted changes: jj's working copy is always a commit; git sees that as "uncommitted." Use
jj st(notgit status) to assess state. - HEAD detachment: jj manages
HEADper change.git checkoutwill detach HEAD in confusing ways — usejj editinstead. .gitignoreis respected by jj automatically..jjignoreis only needed if you want jj-only ignore patterns the outer git doesn't see.- Branches vs bookmarks: git branches become jj bookmarks on fetch. Bookmarks don't auto-advance like git branches — you must
jj bookmark movebefore pushing.
Pushing from a co-located repo
Use jj git push, not git push:
jj bookmark move main --to @ # Move bookmark to your latest change
jj git push -b main # Push to remote
See references/PUSH.md for the full push workflow.