jj / How jj git colocation works

How jj git colocation works

jj git colocation converts an existing workspace between colocated (a .git at the workspace root, so plain git works there) and non-colocated (the Git repository hidden in .jj/repo/store/git). status reports which one a workspace is; enable and disable move the Git repository and fix up what points at it.

On this page
  1. 1. Command-line surface
  2. 2. Configuration
  3. 3. End-to-end flow
  4. status
  5. 4. Core architecture touchpoints
  6. 5. Transaction and operation
  7. 6. Algorithm and invariants
  8. 7. Errors and edge cases
  9. 8. Worked example

In short: for the main workspace, enable renames .jj/repo/store/git to .git, rewrites the git_target file to ../../../.git, sets core.bare = false, writes .jj/.gitignore, and points Git's HEAD at the working-copy commit's parent. disable reverses each step and clears the recorded Git HEAD. A secondary workspace is colocated with a Git worktree instead of a move. Only the HEAD change is recorded as an operation.

The group is GitColocationCommand. New repos choose colocation at creation, in jj git init or jj git clone. What changes day to day in a colocated workspace (automatic import and export) is described on the jj git import and jj git export pages.

1. Command-line surface

SubcommandArgumentsMeaning
statusnoneShow the current colocation status (source).
enablenoneConvert into a colocated workspace (source).
disablenoneConvert into a non-colocated workspace; plain git then no longer works there (source).

2. Configuration

git.colocate doesn't affect these commands; it only sets the default for new repos. The state they change lives on disk: the .jj/repo/store/git_target file that tells the Git backend where its repository is, core.bare in the Git repository's own config, and .jj/.gitignore. enable and disable for a secondary workspace run git as a subprocess, so the git.executable-path setting applies (source).

3. End-to-end flow

jj git colocation enable and disable: call flow enable: stop if already colocated; require a working-copy commit. A secondary workspace gets a Git worktree. The main workspace renames .jj/repo/store/git to .git, writes git_target as ../../../.git and sets core.bare false. Then the workspace is reloaded, .jj/.gitignore is written, and reset_head points Git HEAD at the working-copy parent, recorded as set git head to working copy parent. disable: stop if not colocated. A secondary workspace unlinks its worktree. The main workspace renames .git back into the store, sets core.bare true and writes git_target as git. Then .jj/.gitignore is removed, the workspace reloaded, and the recorded Git HEAD cleared, recorded as remove git head reference. enable disable already colocated → stop needs a working-copy commit not colocated → stop secondary: git::create_worktree secondary: unlink_git_worktree main: rename store/git → .git git_target = "../../../.git" core.bare = false main: rename .git → store/git core.bare = true git_target = "git" reload the workspace at the same op write .jj/.gitignore remove .jj/.gitignore reload the workspace at the same op git::reset_head(wc commit) op: set git head to working copy parent set_git_head_target(absent) op: remove git head reference "Workspace successfully converted into a colocated …" "… into a non-colocated Jujutsu/Git workspace."

The secondary-workspace box and the main-workspace box are alternatives: a workspace whose .jj/repo is a file pointing at another workspace's repo is a secondary ("child") workspace (is_child_workspace).

status

Prints whether the workspace is colocated, by trying to open a Git repo at the workspace root that matches the backend's (is_colocated_git_workspace), and the Git HEAD recorded in the view, or (none). A hint then says how to switch. For a main workspace with no .jj/repo/store/git, the backing repo is external and the hint says colocation can't be enabled (source).

4. Core architecture touchpoints

  • Git backend location: the backend finds its Git repository through git_target, relative to the store directory; "git" and "../../../.git" are the two values these commands write (enable, disable). See the commits page for the Git backend.
  • Git worktrees: git::create_worktree runs git worktree add --orphan with a random jj-worktree-… branch name and leaves HEAD unborn. If the directory already has files, which it does here, the worktree is made in a temporary directory, its .git link moved into place and git worktree repair run (source).
  • View: the per-workspace Git HEAD (git_head) is the only view change; see the view page.
  • Working copy: files aren't touched. reset_head also rewrites Git's index from the working-copy commit's (merged) parent tree, so git diff shows the same changes as jj diff (reset_index). It holds Git's index.lock from before it reads the old index until the new one is committed, waiting up to a second if another process has it, so a concurrent git add isn't silently overwritten (lock_index).

5. Transaction and operation

  • enable: one transaction after the move, set git head to working copy parent, recorded only if the Git HEAD changed (source, test).
  • disable: one transaction, remove git head reference, recorded only if a Git HEAD was recorded (source, test).
  • status: none.

The move itself happens outside any transaction, before it, so jj undo only restores the recorded HEAD; it doesn't move the Git repository back. Both commands reload the workspace at the operation they started from, so the transaction is based on the same repo state (reload_workspace_helper).

6. Algorithm and invariants

Releasing the repo before moving it. The workspace helper holds the Git repository open, and gix memory-maps pack files; Windows refuses to rename a directory with open files. So the helper is dropped before the rename and a fresh one loaded afterwards (source, test with pack files).

Bare flag. An internal repo is bare and a colocated one isn't, so set_git_repo_bare sets core.bare directly in the moved repository's config file.

Git HEAD. On enable, reset_head sets Git's HEAD to the working-copy commit's first parent, detached, or leaves it unborn if that parent is the root commit. It is called with no previous working-copy commit, which forces a full reset: it also clears an in-progress Git merge, rebase, cherry-pick or bisect state, and rewrites the index from the parent tree (call, source). On disable, jj just forgets the recorded HEAD, since a non-colocated workspace doesn't track one.

Invariants: after enable, the workspace is colocated, .jj is ignored by Git, and Git's HEAD matches the working-copy parent. After disable, no .git is left at the root and no Git HEAD is recorded. Running either twice is harmless: the second run says the workspace is already in that state.

7. Errors and edge cases

MessageWhenSource
Workspace is already colocated with Git. / Workspace is already not colocated with Git.Nothing to do.enable, disable
This command requires a working copyenable in a workspace with no working-copy commit.colocation.rs
A .git directory already exists in the workspace root. Cannot colocate.Something is already at .git.colocation.rs, test
Cannot colocate a workspace backed by an external Git repository at …The repo was created with --git-repo, so there is no internal store to move.colocation.rs, test
Failed to move Git repository …Any other rename failure.enable, disable
Failed to open Git config file., Failed to set core.bare …, Failed to write to Git config file …Changing core.bare failed.colocation.rs
Removed Git worktree for "…". or a warning that it faileddisable in a secondary workspace; a failure is only a warning.git_util.rs, test

8. Worked example

From upstream's test_git_colocation_enable_success. A repo made with jj git init --no-colocate repo, followed by jj new, so the working-copy commit has a non-root parent e8849ae1. Then jj git colocation enable:

Workspace layout before and after jj git colocation enable Before: the Git repository is at .jj/repo/store/git, git_target contains git, there is no .git at the root, and no Git HEAD is recorded. After: the repository is at .git in the workspace root, git_target contains ../../../.git, .jj/.gitignore contains /*, and the recorded Git HEAD is e8849ae1, the working-copy commit's parent. before after repo/ .jj/ repo/store/git/ (bare Git repo) repo/store/git_target = "git" working_copy/ no .git Git HEAD recorded: (none) repo/ .git/ (core.bare = false) .jj/ .gitignore = "/*" repo/store/git_target = "../../../.git" working_copy/ Git HEAD recorded: e8849ae1

Upstream's output, then jj git colocation status and jj op show (source):

Workspace successfully converted into a colocated Jujutsu/Git workspace.

Workspace 'default' is currently colocated with Git.
Last imported/exported Git HEAD: e8849ae12c709f2321908879bc724fdb2ab8a781

set git head to working copy parent

test_git_colocation_disable_success is the reverse: .git moves back into the store, git_target is git again, .jj/.gitignore is gone, the recorded HEAD is (none), and the operation is remove git head reference.