feat(fish): add workspace-oriented jj wrappers

Amolith created

Change summary

dot_config/private_fish/functions/jj-clone.fish   |  18 
dot_config/private_fish/functions/jj-convert.fish | 318 +++++++++++++++++
dot_config/private_fish/functions/jj-init.fish    |   9 
3 files changed, 345 insertions(+)

Detailed changes

dot_config/private_fish/functions/jj-clone.fish 🔗

@@ -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

dot_config/private_fish/functions/jj-convert.fish 🔗

@@ -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

dot_config/private_fish/functions/jj-init.fish 🔗

@@ -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