From 2f3432beac07d227f2f8db86082a72a2a1540a1e Mon Sep 17 00:00:00 2001 From: Amolith Date: Tue, 24 Feb 2026 09:15:08 -0700 Subject: [PATCH] feat(fish): add workspace-oriented jj wrappers --- .../private_fish/functions/jj-clone.fish | 18 + .../private_fish/functions/jj-convert.fish | 318 ++++++++++++++++++ .../private_fish/functions/jj-init.fish | 9 + 3 files changed, 345 insertions(+) create mode 100644 dot_config/private_fish/functions/jj-clone.fish create mode 100644 dot_config/private_fish/functions/jj-convert.fish create mode 100644 dot_config/private_fish/functions/jj-init.fish diff --git a/dot_config/private_fish/functions/jj-clone.fish b/dot_config/private_fish/functions/jj-clone.fish new file mode 100644 index 0000000000000000000000000000000000000000..3ccddc7181ee2b49b0e29c46999d5bbaea8f911d --- /dev/null +++ b/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 [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 diff --git a/dot_config/private_fish/functions/jj-convert.fish b/dot_config/private_fish/functions/jj-convert.fish new file mode 100644 index 0000000000000000000000000000000000000000..2d8c4b42d7d683cf597d3a8b1fc4c28246c3eb02 --- /dev/null +++ b/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 diff --git a/dot_config/private_fish/functions/jj-init.fish b/dot_config/private_fish/functions/jj-init.fish new file mode 100644 index 0000000000000000000000000000000000000000..e11f3a448ea2ecdcf21bb825be05714e61e44cee --- /dev/null +++ b/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