@@ -0,0 +1,18 @@
+function jj-clone --description "Clone a repo into a jj workspace layout with a colocated main"
+ if test (count $argv) -eq 0
+ echo "Usage: jj-clone <url> [directory]"
+ return 1
+ end
+
+ set -l url $argv[1]
+ set -l dir
+ if test (count $argv) -ge 2
+ set dir $argv[2]
+ else
+ set dir (basename "$url" .git)
+ end
+
+ mkdir -p "$dir"
+ jj git clone "$url" "$dir/main"
+ cd "$dir/main"
+end
@@ -0,0 +1,318 @@
+function jj-convert --description "Convert a Git repo to a jj workspace layout with a colocated main"
+ # --- Detection ---
+ set -l layout ""
+ set -l primary_dir ""
+ set -l primary_branch ""
+
+ if test -d .bare
+ set layout bare-worktrees
+ else if not test -e .git; and begin
+ # Find which subdirectory has .git as a directory (the primary)
+ for d in */
+ if test -d "$d/.git"
+ set primary_dir (string trim --right --chars='/' "$d")
+ break
+ end
+ end
+ test -n "$primary_dir"
+ end
+ set layout main-worktrees
+ else if test -d .git
+ set layout standard
+ end
+
+ if test -z "$layout"
+ echo "error: unrecognized Git layout"
+ echo "Expected one of:"
+ echo " - standard clone (.git/ directory at root)"
+ echo " - worktrees from a primary subdirectory (no .git at root, subdir/.git/ is a directory)"
+ echo " - bare repo with worktrees (.bare/ directory at root)"
+ return 1
+ end
+
+ echo "Detected layout: $layout"
+
+ # --- Dirty check ---
+ # Directories that survive the conversion only need tracked-file checks.
+ # Directories that get destroyed and recreated need full checks (including untracked).
+ switch $layout
+ case standard
+ # Files are moved, not destroyed — only check tracked modifications
+ set -l status_out (git diff --name-only 2>&1; git diff --cached --name-only 2>&1)
+ if test -n "$status_out"
+ echo "error: working tree has uncommitted tracked changes; commit or stash first"
+ git status --short
+ return 1
+ end
+
+ case main-worktrees
+ set -l dirty 0
+ for d in */
+ set -l name (string trim --right --chars='/' "$d")
+ if not test -e "$name/.git"
+ continue
+ end
+ if test "$name" = "$primary_dir"
+ # Primary survives — only check tracked modifications
+ set -l status_out (git -C "$name" diff --name-only 2>&1; git -C "$name" diff --cached --name-only 2>&1)
+ if test -n "$status_out"
+ echo "error: primary worktree '$name' has uncommitted tracked changes; commit or stash first"
+ git -C "$name" status --short
+ set dirty 1
+ end
+ else
+ # Non-primary gets destroyed — check everything
+ set -l status_out (git -C "$name" status --porcelain 2>&1)
+ if test -n "$status_out"
+ echo "error: worktree '$name' is dirty; commit, stash, or clean up first"
+ git -C "$name" status --short
+ set dirty 1
+ end
+ end
+ end
+ if test $dirty -eq 1
+ return 1
+ end
+
+ case bare-worktrees
+ # Detect primary worktree dir so we know which survives
+ set -l bare_primary_branch (git -C .bare symbolic-ref --short HEAD)
+ set -l bare_primary_dir ""
+ set -l current_path ""
+ set -l is_bare 0
+ for line in (git worktree list --porcelain)
+ if string match -q 'worktree *' "$line"
+ set current_path (string replace 'worktree ' '' "$line")
+ set is_bare 0
+ else if test "$line" = bare
+ set is_bare 1
+ else if string match -q 'branch refs/heads/*' "$line"
+ set -l branch (string replace 'branch refs/heads/' '' "$line")
+ if test "$branch" = "$bare_primary_branch"; and test $is_bare -eq 0
+ set bare_primary_dir (basename "$current_path")
+ end
+ end
+ end
+
+ set -l dirty 0
+ for d in */
+ set -l name (string trim --right --chars='/' "$d")
+ if not test -e "$name/.git"
+ continue
+ end
+ if test "$name" = "$bare_primary_dir"
+ # Primary survives — only check tracked modifications
+ set -l status_out (git -C "$name" diff --name-only 2>&1; git -C "$name" diff --cached --name-only 2>&1)
+ if test -n "$status_out"
+ echo "error: primary worktree '$name' has uncommitted tracked changes; commit or stash first"
+ git -C "$name" status --short
+ set dirty 1
+ end
+ else
+ # Non-primary gets destroyed — check everything
+ set -l status_out (git -C "$name" status --porcelain 2>&1)
+ if test -n "$status_out"
+ echo "error: worktree '$name' is dirty; commit, stash, or clean up first"
+ git -C "$name" status --short
+ set dirty 1
+ end
+ end
+ end
+ if test $dirty -eq 1
+ return 1
+ end
+ end
+
+ # --- Conversion ---
+ switch $layout
+ case standard
+ _jj_convert_standard
+
+ case main-worktrees
+ _jj_convert_main_worktrees $primary_dir
+
+ case bare-worktrees
+ _jj_convert_bare_worktrees
+ end
+end
+
+function _jj_convert_standard
+ set -l primary_branch (git symbolic-ref --short HEAD)
+ echo "Primary branch: $primary_branch"
+
+ mkdir main
+
+ # Move everything except .git and main into main/
+ for item in (ls -A)
+ if test "$item" != ".git"; and test "$item" != "main"
+ mv "$item" main/
+ end
+ end
+ mv .git main/
+
+ cd main
+ jj git init --git-repo .
+ echo
+ echo "Converted standard clone → jj workspace layout"
+ echo "Primary workspace: main/ (colocated, branch: $primary_branch)"
+end
+
+function _jj_convert_main_worktrees --argument-names primary_dir
+ set -l primary_branch (git -C "$primary_dir" symbolic-ref --short HEAD)
+ echo "Primary directory: $primary_dir"
+ echo "Primary branch: $primary_branch"
+
+ # Collect non-primary worktree info
+ set -l wt_names
+ set -l wt_branches
+ set -l current_path ""
+ set -l current_branch ""
+ set -l in_primary 0
+
+ for line in (git -C "$primary_dir" worktree list --porcelain)
+ if string match -q 'worktree *' "$line"
+ # Save previous entry if it was non-primary
+ if test -n "$current_path"; and test $in_primary -eq 0; and test -n "$current_branch"
+ set -a wt_names (basename "$current_path")
+ set -a wt_branches "$current_branch"
+ end
+ set current_path (string replace 'worktree ' '' "$line")
+ set current_branch ""
+ # Check if this is the primary
+ if test (basename "$current_path") = "$primary_dir"
+ set in_primary 1
+ else
+ set in_primary 0
+ end
+ else if string match -q 'branch refs/heads/*' "$line"
+ set current_branch (string replace 'branch refs/heads/' '' "$line")
+ end
+ end
+ # Don't forget the last entry
+ if test -n "$current_path"; and test $in_primary -eq 0; and test -n "$current_branch"
+ set -a wt_names (basename "$current_path")
+ set -a wt_branches "$current_branch"
+ end
+
+ # Show what we found
+ echo "Non-primary worktrees:"
+ for i in (seq (count $wt_names))
+ echo " $wt_names[$i] → $wt_branches[$i]"
+ end
+
+ # Remove non-primary git worktrees
+ for i in (seq (count $wt_names))
+ echo "Removing git worktree: $wt_names[$i]"
+ git -C "$primary_dir" worktree remove --force "../$wt_names[$i]"
+ end
+
+ # Initialize jj in the primary
+ cd "$primary_dir"
+ jj git init --git-repo .
+
+ # Add jj workspaces named by branch
+ for i in (seq (count $wt_branches))
+ set -l branch $wt_branches[$i]
+ echo "Adding jj workspace: $branch"
+ jj workspace add "../$branch"
+ jj -R "../$branch" new "$branch"
+ end
+
+ echo
+ echo "Converted main-primary worktrees → jj workspace layout"
+ echo "Primary workspace: $primary_dir/ (colocated, branch: $primary_branch)"
+ for i in (seq (count $wt_branches))
+ echo "Workspace: $wt_branches[$i]/ (jj-only)"
+ end
+end
+
+function _jj_convert_bare_worktrees
+ set -l primary_branch (git -C .bare symbolic-ref --short HEAD)
+ echo "Primary branch: $primary_branch"
+
+ # Collect worktree info and find which dir has the primary branch
+ set -l primary_wt_dir ""
+ set -l wt_names
+ set -l wt_branches
+ set -l current_path ""
+ set -l current_branch ""
+ set -l is_bare 0
+
+ for line in (git worktree list --porcelain)
+ if string match -q 'worktree *' "$line"
+ # Save previous entry
+ if test -n "$current_path"; and test $is_bare -eq 0; and test -n "$current_branch"
+ set -l name (basename "$current_path")
+ if test "$current_branch" = "$primary_branch"
+ set primary_wt_dir "$name"
+ else
+ set -a wt_names "$name"
+ set -a wt_branches "$current_branch"
+ end
+ end
+ set current_path (string replace 'worktree ' '' "$line")
+ set current_branch ""
+ set is_bare 0
+ else if test "$line" = bare
+ set is_bare 1
+ else if string match -q 'branch refs/heads/*' "$line"
+ set current_branch (string replace 'branch refs/heads/' '' "$line")
+ end
+ end
+ # Last entry
+ if test -n "$current_path"; and test $is_bare -eq 0; and test -n "$current_branch"
+ set -l name (basename "$current_path")
+ if test "$current_branch" = "$primary_branch"
+ set primary_wt_dir "$name"
+ else
+ set -a wt_names "$name"
+ set -a wt_branches "$current_branch"
+ end
+ end
+
+ if test -z "$primary_wt_dir"
+ echo "error: could not find a worktree on the primary branch ($primary_branch)"
+ return 1
+ end
+
+ echo "Primary worktree dir: $primary_wt_dir"
+ echo "Non-primary worktrees:"
+ for i in (seq (count $wt_names))
+ echo " $wt_names[$i] → $wt_branches[$i]"
+ end
+
+ # Remove non-primary git worktrees
+ for i in (seq (count $wt_names))
+ echo "Removing git worktree: $wt_names[$i]"
+ git worktree remove --force "$wt_names[$i]"
+ end
+
+ # Restructure: un-bare, move .bare into the primary worktree dir
+ git -C .bare config core.bare false
+ rm -rf .bare/worktrees
+ rm "$primary_wt_dir/.git"
+ mv .bare "$primary_wt_dir/.git"
+ rm .git
+
+ # Rebuild the git index (bare repos don't have one)
+ cd "$primary_wt_dir"
+ git reset
+
+ # Initialize jj
+ jj git init --git-repo .
+
+ # Add jj workspaces for former worktrees
+ for i in (seq (count $wt_branches))
+ set -l branch $wt_branches[$i]
+ echo "Adding jj workspace: $branch"
+ jj workspace add "../$branch"
+ jj -R "../$branch" new "$branch"
+ end
+
+ echo
+ echo "Converted bare repo with worktrees → jj workspace layout"
+ echo "Primary workspace: $primary_wt_dir/ (colocated, branch: $primary_branch)"
+ for i in (seq (count $wt_branches))
+ echo "Workspace: $wt_branches[$i]/ (jj-only)"
+ end
+end
@@ -0,0 +1,9 @@
+function jj-init --description "Initialize a new jj repo with a colocated main workspace"
+ if test -d main
+ echo "error: main/ already exists in this directory"
+ return 1
+ end
+
+ jj git init main
+ cd main
+end