jj-convert.fish

  1function jj-convert --description "Convert a Git repo to a jj workspace layout with a colocated main"
  2    # --- Detection ---
  3    set -l layout ""
  4    set -l primary_dir ""
  5    set -l primary_branch ""
  6
  7    if test -d .bare
  8        set layout bare-worktrees
  9    else if not test -e .git; and begin
 10            # Find which subdirectory has .git as a directory (the primary)
 11            for d in */
 12                if test -d "$d/.git"
 13                    set primary_dir (string trim --right --chars='/' "$d")
 14                    break
 15                end
 16            end
 17            test -n "$primary_dir"
 18        end
 19        set layout main-worktrees
 20    else if test -d .git
 21        set layout standard
 22    end
 23
 24    if test -z "$layout"
 25        echo "error: unrecognized Git layout"
 26        echo "Expected one of:"
 27        echo "  - standard clone (.git/ directory at root)"
 28        echo "  - worktrees from a primary subdirectory (no .git at root, subdir/.git/ is a directory)"
 29        echo "  - bare repo with worktrees (.bare/ directory at root)"
 30        return 1
 31    end
 32
 33    echo "Detected layout: $layout"
 34
 35    # --- Dirty check ---
 36    # Directories that survive the conversion only need tracked-file checks.
 37    # Directories that get destroyed and recreated need full checks (including untracked).
 38    switch $layout
 39        case standard
 40            # Files are moved, not destroyed — only check tracked modifications
 41            set -l status_out (git diff --name-only 2>&1; git diff --cached --name-only 2>&1)
 42            if test -n "$status_out"
 43                echo "error: working tree has uncommitted tracked changes; commit or stash first"
 44                git status --short
 45                return 1
 46            end
 47
 48        case main-worktrees
 49            set -l dirty 0
 50            for d in */
 51                set -l name (string trim --right --chars='/' "$d")
 52                if not test -e "$name/.git"
 53                    continue
 54                end
 55                if test "$name" = "$primary_dir"
 56                    # Primary survives — only check tracked modifications
 57                    set -l status_out (git -C "$name" diff --name-only 2>&1; git -C "$name" diff --cached --name-only 2>&1)
 58                    if test -n "$status_out"
 59                        echo "error: primary worktree '$name' has uncommitted tracked changes; commit or stash first"
 60                        git -C "$name" status --short
 61                        set dirty 1
 62                    end
 63                else
 64                    # Non-primary gets destroyed — check everything
 65                    set -l status_out (git -C "$name" status --porcelain 2>&1)
 66                    if test -n "$status_out"
 67                        echo "error: worktree '$name' is dirty; commit, stash, or clean up first"
 68                        git -C "$name" status --short
 69                        set dirty 1
 70                    end
 71                end
 72            end
 73            if test $dirty -eq 1
 74                return 1
 75            end
 76
 77        case bare-worktrees
 78            # Detect primary worktree dir so we know which survives
 79            set -l bare_primary_branch (git -C .bare symbolic-ref --short HEAD)
 80            set -l bare_primary_dir ""
 81            set -l current_path ""
 82            set -l is_bare 0
 83            for line in (git worktree list --porcelain)
 84                if string match -q 'worktree *' "$line"
 85                    set current_path (string replace 'worktree ' '' "$line")
 86                    set is_bare 0
 87                else if test "$line" = bare
 88                    set is_bare 1
 89                else if string match -q 'branch refs/heads/*' "$line"
 90                    set -l branch (string replace 'branch refs/heads/' '' "$line")
 91                    if test "$branch" = "$bare_primary_branch"; and test $is_bare -eq 0
 92                        set bare_primary_dir (basename "$current_path")
 93                    end
 94                end
 95            end
 96
 97            set -l dirty 0
 98            for d in */
 99                set -l name (string trim --right --chars='/' "$d")
100                if not test -e "$name/.git"
101                    continue
102                end
103                if test "$name" = "$bare_primary_dir"
104                    # Primary survives — only check tracked modifications
105                    set -l status_out (git -C "$name" diff --name-only 2>&1; git -C "$name" diff --cached --name-only 2>&1)
106                    if test -n "$status_out"
107                        echo "error: primary worktree '$name' has uncommitted tracked changes; commit or stash first"
108                        git -C "$name" status --short
109                        set dirty 1
110                    end
111                else
112                    # Non-primary gets destroyed — check everything
113                    set -l status_out (git -C "$name" status --porcelain 2>&1)
114                    if test -n "$status_out"
115                        echo "error: worktree '$name' is dirty; commit, stash, or clean up first"
116                        git -C "$name" status --short
117                        set dirty 1
118                    end
119                end
120            end
121            if test $dirty -eq 1
122                return 1
123            end
124    end
125
126    # --- Conversion ---
127    switch $layout
128        case standard
129            _jj_convert_standard
130
131        case main-worktrees
132            _jj_convert_main_worktrees $primary_dir
133
134        case bare-worktrees
135            _jj_convert_bare_worktrees
136    end
137end
138
139function _jj_convert_standard
140    set -l primary_branch (git symbolic-ref --short HEAD)
141    echo "Primary branch: $primary_branch"
142
143    mkdir main
144
145    # Move everything except .git and main into main/
146    for item in (ls -A)
147        if test "$item" != ".git"; and test "$item" != "main"
148            mv "$item" main/
149        end
150    end
151    mv .git main/
152
153    cd main
154    jj git init --git-repo .
155    echo
156    echo "Converted standard clone → jj workspace layout"
157    echo "Primary workspace: main/ (colocated, branch: $primary_branch)"
158end
159
160function _jj_convert_main_worktrees --argument-names primary_dir
161    set -l primary_branch (git -C "$primary_dir" symbolic-ref --short HEAD)
162    echo "Primary directory: $primary_dir"
163    echo "Primary branch: $primary_branch"
164
165    # Collect non-primary worktree info
166    set -l wt_names
167    set -l wt_branches
168    set -l current_path ""
169    set -l current_branch ""
170    set -l in_primary 0
171
172    for line in (git -C "$primary_dir" worktree list --porcelain)
173        if string match -q 'worktree *' "$line"
174            # Save previous entry if it was non-primary
175            if test -n "$current_path"; and test $in_primary -eq 0; and test -n "$current_branch"
176                set -a wt_names (basename "$current_path")
177                set -a wt_branches "$current_branch"
178            end
179            set current_path (string replace 'worktree ' '' "$line")
180            set current_branch ""
181            # Check if this is the primary
182            if test (basename "$current_path") = "$primary_dir"
183                set in_primary 1
184            else
185                set in_primary 0
186            end
187        else if string match -q 'branch refs/heads/*' "$line"
188            set current_branch (string replace 'branch refs/heads/' '' "$line")
189        end
190    end
191    # Don't forget the last entry
192    if test -n "$current_path"; and test $in_primary -eq 0; and test -n "$current_branch"
193        set -a wt_names (basename "$current_path")
194        set -a wt_branches "$current_branch"
195    end
196
197    # Show what we found
198    echo "Non-primary worktrees:"
199    for i in (seq (count $wt_names))
200        echo "  $wt_names[$i] → $wt_branches[$i]"
201    end
202
203    # Remove non-primary git worktrees
204    for i in (seq (count $wt_names))
205        echo "Removing git worktree: $wt_names[$i]"
206        git -C "$primary_dir" worktree remove --force "../$wt_names[$i]"
207    end
208
209    # Initialize jj in the primary
210    cd "$primary_dir"
211    jj git init --git-repo .
212
213    # Add jj workspaces named by branch
214    for i in (seq (count $wt_branches))
215        set -l branch $wt_branches[$i]
216        echo "Adding jj workspace: $branch"
217        jj workspace add "../$branch"
218        jj -R "../$branch" new "$branch"
219    end
220
221    echo
222    echo "Converted main-primary worktrees → jj workspace layout"
223    echo "Primary workspace: $primary_dir/ (colocated, branch: $primary_branch)"
224    for i in (seq (count $wt_branches))
225        echo "Workspace: $wt_branches[$i]/ (jj-only)"
226    end
227end
228
229function _jj_convert_bare_worktrees
230    set -l primary_branch (git -C .bare symbolic-ref --short HEAD)
231    echo "Primary branch: $primary_branch"
232
233    # Collect worktree info and find which dir has the primary branch
234    set -l primary_wt_dir ""
235    set -l wt_names
236    set -l wt_branches
237    set -l current_path ""
238    set -l current_branch ""
239    set -l is_bare 0
240
241    for line in (git worktree list --porcelain)
242        if string match -q 'worktree *' "$line"
243            # Save previous entry
244            if test -n "$current_path"; and test $is_bare -eq 0; and test -n "$current_branch"
245                set -l name (basename "$current_path")
246                if test "$current_branch" = "$primary_branch"
247                    set primary_wt_dir "$name"
248                else
249                    set -a wt_names "$name"
250                    set -a wt_branches "$current_branch"
251                end
252            end
253            set current_path (string replace 'worktree ' '' "$line")
254            set current_branch ""
255            set is_bare 0
256        else if test "$line" = bare
257            set is_bare 1
258        else if string match -q 'branch refs/heads/*' "$line"
259            set current_branch (string replace 'branch refs/heads/' '' "$line")
260        end
261    end
262    # Last entry
263    if test -n "$current_path"; and test $is_bare -eq 0; and test -n "$current_branch"
264        set -l name (basename "$current_path")
265        if test "$current_branch" = "$primary_branch"
266            set primary_wt_dir "$name"
267        else
268            set -a wt_names "$name"
269            set -a wt_branches "$current_branch"
270        end
271    end
272
273    if test -z "$primary_wt_dir"
274        echo "error: could not find a worktree on the primary branch ($primary_branch)"
275        return 1
276    end
277
278    echo "Primary worktree dir: $primary_wt_dir"
279    echo "Non-primary worktrees:"
280    for i in (seq (count $wt_names))
281        echo "  $wt_names[$i] → $wt_branches[$i]"
282    end
283
284    # Remove non-primary git worktrees
285    for i in (seq (count $wt_names))
286        echo "Removing git worktree: $wt_names[$i]"
287        git worktree remove --force "$wt_names[$i]"
288    end
289
290    # Restructure: un-bare, move .bare into the primary worktree dir
291    git -C .bare config core.bare false
292    rm -rf .bare/worktrees
293    rm "$primary_wt_dir/.git"
294    mv .bare "$primary_wt_dir/.git"
295    rm .git
296
297    # Rebuild the git index (bare repos don't have one)
298    cd "$primary_wt_dir"
299    git reset
300
301    # Initialize jj
302    jj git init --git-repo .
303
304    # Add jj workspaces for former worktrees
305    for i in (seq (count $wt_branches))
306        set -l branch $wt_branches[$i]
307        echo "Adding jj workspace: $branch"
308        jj workspace add "../$branch"
309        jj -R "../$branch" new "$branch"
310    end
311
312    echo
313    echo "Converted bare repo with worktrees → jj workspace layout"
314    echo "Primary workspace: $primary_wt_dir/ (colocated, branch: $primary_branch)"
315    for i in (seq (count $wt_branches))
316        echo "Workspace: $wt_branches[$i]/ (jj-only)"
317    end
318end