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