1-- SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2--
3-- SPDX-License-Identifier: GPL-3.0-or-later
4
5package.path = package.path .. ";./?.lua"
6local wt = dofile("src/main.lua")
7local helper = require("spec.test_helper")
8local git = helper.git
9
10describe("cmd_init", function()
11 local temp_dir
12 local original_cwd
13
14 setup(function()
15 local handle = io.popen("pwd")
16 if handle then
17 original_cwd = handle:read("*l")
18 handle:close()
19 end
20 handle = io.popen("mktemp -d")
21 if handle then
22 temp_dir = handle:read("*l")
23 handle:close()
24 end
25 end)
26
27 teardown(function()
28 if original_cwd then
29 os.execute("cd " .. original_cwd)
30 end
31 if temp_dir then
32 os.execute("rm -rf " .. temp_dir)
33 end
34 end)
35
36 describe("already wt-managed repository", function()
37 it("reports already using wt structure when .git file points to .bare", function()
38 if not temp_dir then
39 pending("temp_dir not available")
40 return
41 end
42 local project = temp_dir .. "/already-wt"
43 os.execute("mkdir -p " .. project .. "/.bare")
44 git("init --bare " .. project .. "/.bare")
45 local f = io.open(project .. "/.git", "w")
46 if f then
47 f:write("gitdir: ./.bare\n")
48 f:close()
49 end
50
51 local output, code = wt.run_cmd("cd " .. project .. " && lua " .. original_cwd .. "/src/main.lua init")
52 assert.are.equal(0, code)
53 assert.is_truthy(output:match("[Aa]lready"))
54 end)
55 end)
56
57 describe("inside a worktree", function()
58 it("errors when run from inside a worktree", function()
59 if not temp_dir then
60 pending("temp_dir not available")
61 return
62 end
63 local project = temp_dir .. "/wt-project"
64 local worktree = project .. "/main"
65 os.execute("mkdir -p " .. project .. "/.bare")
66 git("init --bare " .. project .. "/.bare")
67 os.execute("mkdir -p " .. worktree)
68 local f = io.open(worktree .. "/.git", "w")
69 if f then
70 f:write("gitdir: ../.bare/worktrees/main\n")
71 f:close()
72 end
73
74 local output, code = wt.run_cmd("cd " .. worktree .. " && lua " .. original_cwd .. "/src/main.lua init 2>&1")
75 assert.are_not.equal(0, code)
76 assert.is_truthy(output:match("worktree") or output:match("not a git"))
77 end)
78 end)
79
80 describe("not a git repository", function()
81 it("errors when run in non-git directory", function()
82 if not temp_dir then
83 pending("temp_dir not available")
84 return
85 end
86 local project = temp_dir .. "/not-git"
87 os.execute("mkdir -p " .. project)
88
89 local output, code = wt.run_cmd("cd " .. project .. " && lua " .. original_cwd .. "/src/main.lua init 2>&1")
90 assert.are_not.equal(0, code)
91 assert.is_truthy(output:match("not a git"))
92 end)
93 end)
94
95 describe(".bare exists but no .git", function()
96 it("errors and suggests creating .git file", function()
97 if not temp_dir then
98 pending("temp_dir not available")
99 return
100 end
101 local project = temp_dir .. "/bare-only"
102 os.execute("mkdir -p " .. project .. "/.bare")
103 git("init --bare " .. project .. "/.bare")
104
105 local output, code = wt.run_cmd("cd " .. project .. " && lua " .. original_cwd .. "/src/main.lua init 2>&1")
106 assert.are_not.equal(0, code)
107 assert.is_truthy(output:match("%.git") or output:match("gitdir"))
108 end)
109 end)
110
111 describe("existing git worktrees", function()
112 it("errors when .git/worktrees/ exists", function()
113 if not temp_dir then
114 pending("temp_dir not available")
115 return
116 end
117 local project = temp_dir .. "/has-worktrees"
118 os.execute("mkdir -p " .. project)
119 git("init " .. project)
120 git("-C " .. project .. " commit --allow-empty -m 'initial'")
121 os.execute("mkdir -p " .. project .. "/.git/worktrees/feature-branch")
122
123 local output, code = wt.run_cmd("cd " .. project .. " && lua " .. original_cwd .. "/src/main.lua init 2>&1")
124 assert.are_not.equal(0, code)
125 assert.is_truthy(output:match("worktree"))
126 end)
127 end)
128
129 describe("dirty repository", function()
130 it("errors when uncommitted changes exist", function()
131 if not temp_dir then
132 pending("temp_dir not available")
133 return
134 end
135 local project = temp_dir .. "/dirty-repo"
136 os.execute("mkdir -p " .. project)
137 git("init " .. project)
138 git("-C " .. project .. " commit --allow-empty -m 'initial'")
139 local f = io.open(project .. "/dirty.txt", "w")
140 if f then
141 f:write("uncommitted\n")
142 f:close()
143 end
144 git("-C " .. project .. " add dirty.txt")
145
146 local output, code = wt.run_cmd("cd " .. project .. " && lua " .. original_cwd .. "/src/main.lua init 2>&1")
147 assert.are_not.equal(0, code)
148 assert.is_truthy(output:match("uncommitted") or output:match("stash") or output:match("commit"))
149 end)
150 end)
151
152 describe("--dry-run", function()
153 it("prints plan without modifying filesystem", function()
154 if not temp_dir then
155 pending("temp_dir not available")
156 return
157 end
158 local project = temp_dir .. "/dry-run-test"
159 os.execute("mkdir -p " .. project)
160 git("init " .. project)
161 git("-C " .. project .. " commit --allow-empty -m 'initial'")
162
163 local cmd = "cd " .. project .. " && lua " .. original_cwd .. "/src/main.lua init --dry-run 2>&1"
164 local output, code = wt.run_cmd(cmd)
165 assert.are.equal(0, code)
166 assert.is_truthy(output:match("[Dd]ry run") or output:match("planned"))
167
168 local git_still_dir = io.open(project .. "/.git/HEAD", "r")
169 assert.is_not_nil(git_still_dir)
170 if git_still_dir then
171 git_still_dir:close()
172 end
173
174 local bare_exists = io.open(project .. "/.bare/HEAD", "r")
175 assert.is_nil(bare_exists)
176 end)
177
178 it("lists orphaned files that would be removed", function()
179 if not temp_dir then
180 pending("temp_dir not available")
181 return
182 end
183 local project = temp_dir .. "/dry-orphans"
184 os.execute("mkdir -p " .. project)
185 git("init " .. project)
186 git("-C " .. project .. " commit --allow-empty -m 'initial'")
187
188 local f = io.open(project .. "/README.md", "w")
189 if f then
190 f:write("# Test\n")
191 f:close()
192 end
193 git("-C " .. project .. " add README.md")
194 git("-C " .. project .. " commit -m 'add readme'")
195
196 local cmd = "cd " .. project .. " && lua " .. original_cwd .. "/src/main.lua init --dry-run 2>&1"
197 local output, code = wt.run_cmd(cmd)
198 assert.are.equal(0, code)
199 assert.is_truthy(output:match("README") or output:match("orphan"))
200 end)
201
202 it("shows target worktree path", function()
203 if not temp_dir then
204 pending("temp_dir not available")
205 return
206 end
207 local project = temp_dir .. "/dry-worktree"
208 os.execute("mkdir -p " .. project)
209 git("init " .. project)
210 git("-C " .. project .. " commit --allow-empty -m 'initial'")
211
212 local cmd = "cd " .. project .. " && lua " .. original_cwd .. "/src/main.lua init --dry-run 2>&1"
213 local output, code = wt.run_cmd(cmd)
214 assert.are.equal(0, code)
215 assert.is_truthy(output:match("worktree") or output:match("main") or output:match("master"))
216 end)
217 end)
218
219 describe("-y/--yes flag", function()
220 it("bypasses confirmation prompt", function()
221 if not temp_dir then
222 pending("temp_dir not available")
223 return
224 end
225 local project = temp_dir .. "/yes-test"
226 os.execute("mkdir -p " .. project)
227 git("init " .. project)
228 git("-C " .. project .. " commit --allow-empty -m 'initial'")
229
230 local output, code = wt.run_cmd("cd " .. project .. " && lua " .. original_cwd .. "/src/main.lua init -y 2>&1")
231 assert.are.equal(0, code)
232 assert.is_truthy(output:match("[Cc]onverted") or output:match("[Bb]are"))
233
234 local bare_exists = io.open(project .. "/.bare/HEAD", "r")
235 assert.is_not_nil(bare_exists)
236 if bare_exists then
237 bare_exists:close()
238 end
239
240 local git_is_file = io.open(project .. "/.git", "r")
241 if git_is_file then
242 local content = git_is_file:read("*a")
243 git_is_file:close()
244 assert.is_truthy(content:match("gitdir"))
245 end
246 end)
247 end)
248
249 describe("successful conversion", function()
250 it("moves .git to .bare", function()
251 if not temp_dir then
252 pending("temp_dir not available")
253 return
254 end
255 local project = temp_dir .. "/convert-test"
256 os.execute("mkdir -p " .. project)
257 git("init " .. project)
258 git("-C " .. project .. " commit --allow-empty -m 'initial'")
259
260 local _, code = wt.run_cmd("cd " .. project .. " && lua " .. original_cwd .. "/src/main.lua init -y 2>&1")
261 assert.are.equal(0, code)
262
263 local bare_head = io.open(project .. "/.bare/HEAD", "r")
264 assert.is_not_nil(bare_head)
265 if bare_head then
266 bare_head:close()
267 end
268 end)
269
270 it("creates .git file pointing to .bare", function()
271 if not temp_dir then
272 pending("temp_dir not available")
273 return
274 end
275 local project = temp_dir .. "/git-file-test"
276 os.execute("mkdir -p " .. project)
277 git("init " .. project)
278 git("-C " .. project .. " commit --allow-empty -m 'initial'")
279
280 local _, code = wt.run_cmd("cd " .. project .. " && lua " .. original_cwd .. "/src/main.lua init -y 2>&1")
281 assert.are.equal(0, code)
282
283 local git_file = io.open(project .. "/.git", "r")
284 assert.is_not_nil(git_file)
285 if git_file then
286 local content = git_file:read("*a")
287 git_file:close()
288 assert.is_truthy(content:match("gitdir:.*%.bare"))
289 end
290 end)
291
292 it("creates worktree for default branch", function()
293 if not temp_dir then
294 pending("temp_dir not available")
295 return
296 end
297 local project = temp_dir .. "/worktree-created"
298 os.execute("mkdir -p " .. project)
299 git("init " .. project)
300 git("-C " .. project .. " commit --allow-empty -m 'initial'")
301
302 local cmd = "cd " .. project .. " && lua " .. original_cwd .. "/src/main.lua init -y 2>&1"
303 local _, code = wt.run_cmd(cmd)
304 assert.are.equal(0, code)
305
306 local worktrees_output, _ = wt.run_cmd("GIT_DIR=" .. project .. "/.bare git worktree list")
307 assert.is_truthy(worktrees_output:match("main") or worktrees_output:match("master"))
308 end)
309
310 it("removes orphaned files from root", function()
311 if not temp_dir then
312 pending("temp_dir not available")
313 return
314 end
315 local project = temp_dir .. "/orphan-cleanup"
316 os.execute("mkdir -p " .. project)
317 git("init " .. project)
318 local f = io.open(project .. "/tracked.txt", "w")
319 if f then
320 f:write("tracked\n")
321 f:close()
322 end
323 git("-C " .. project .. " add tracked.txt")
324 git("-C " .. project .. " commit -m 'add file'")
325
326 local _, code = wt.run_cmd("cd " .. project .. " && lua " .. original_cwd .. "/src/main.lua init -y 2>&1")
327 assert.are.equal(0, code)
328
329 local orphan_check = io.open(project .. "/tracked.txt", "r")
330 assert.is_nil(orphan_check)
331 end)
332 end)
333
334 describe("warnings", function()
335 it("warns about submodules", function()
336 if not temp_dir then
337 pending("temp_dir not available")
338 return
339 end
340 local project = temp_dir .. "/submodule-warn"
341 os.execute("mkdir -p " .. project)
342 git("init " .. project)
343 git("-C " .. project .. " commit --allow-empty -m 'initial'")
344 local f = io.open(project .. "/.gitmodules", "w")
345 if f then
346 f:write("[submodule \"lib\"]\n\tpath = lib\n\turl = https://example.com/lib.git\n")
347 f:close()
348 end
349 git("-C " .. project .. " add .gitmodules")
350 git("-C " .. project .. " commit -m 'add submodules'")
351
352 local output, _ = wt.run_cmd("cd " .. project .. " && lua " .. original_cwd .. "/src/main.lua init --dry-run 2>&1")
353 assert.is_truthy(output:match("submodule") or output:match("[Ww]arning"))
354 end)
355 end)
356
357 describe("preserving untracked and ignored files", function()
358 it("preserves untracked files in the new worktree", function()
359 if not temp_dir then
360 pending("temp_dir not available")
361 return
362 end
363 local project = temp_dir .. "/preserve-untracked"
364 os.execute("mkdir -p " .. project)
365 git("init " .. project)
366 git("-C " .. project .. " commit --allow-empty -m 'initial'")
367
368 -- Create an untracked file
369 local f = io.open(project .. "/untracked.txt", "w")
370 if f then
371 f:write("untracked content\n")
372 f:close()
373 end
374
375 local _, code = wt.run_cmd("cd " .. project .. " && lua " .. original_cwd .. "/src/main.lua init -y 2>&1")
376 assert.are.equal(0, code)
377
378 -- Find the worktree path (main or master)
379 local worktree = project .. "/main"
380 local check = io.open(worktree .. "/.git", "r")
381 if not check then
382 worktree = project .. "/master"
383 check = io.open(worktree .. "/.git", "r")
384 end
385 if check then
386 check:close()
387 end
388
389 -- Verify untracked file was preserved in worktree
390 local preserved = io.open(worktree .. "/untracked.txt", "r")
391 assert.is_not_nil(preserved, "untracked file should be preserved in worktree")
392 if preserved then
393 local content = preserved:read("*a")
394 preserved:close()
395 assert.are.equal("untracked content\n", content)
396 end
397
398 -- Verify it was removed from root
399 local root_file = io.open(project .. "/untracked.txt", "r")
400 assert.is_nil(root_file, "untracked file should be removed from root")
401 end)
402
403 it("preserves ignored files in the new worktree", function()
404 if not temp_dir then
405 pending("temp_dir not available")
406 return
407 end
408 local project = temp_dir .. "/preserve-ignored"
409 os.execute("mkdir -p " .. project)
410 git("init " .. project)
411
412 -- Create .gitignore and commit it
413 local gitignore = io.open(project .. "/.gitignore", "w")
414 if gitignore then
415 gitignore:write("*.secret\n")
416 gitignore:write("cache/\n")
417 gitignore:close()
418 end
419 git("-C " .. project .. " add .gitignore")
420 git("-C " .. project .. " commit -m 'add gitignore'")
421
422 -- Create ignored files
423 local secret = io.open(project .. "/config.secret", "w")
424 if secret then
425 secret:write("secret data\n")
426 secret:close()
427 end
428 os.execute("mkdir -p " .. project .. "/cache")
429 local cache_file = io.open(project .. "/cache/data.bin", "w")
430 if cache_file then
431 cache_file:write("cached data\n")
432 cache_file:close()
433 end
434
435 local _, code = wt.run_cmd("cd " .. project .. " && lua " .. original_cwd .. "/src/main.lua init -y 2>&1")
436 assert.are.equal(0, code)
437
438 -- Find the worktree path
439 local worktree = project .. "/main"
440 local check = io.open(worktree .. "/.git", "r")
441 if not check then
442 worktree = project .. "/master"
443 check = io.open(worktree .. "/.git", "r")
444 end
445 if check then
446 check:close()
447 end
448
449 -- Verify ignored file was preserved
450 local preserved_secret = io.open(worktree .. "/config.secret", "r")
451 assert.is_not_nil(preserved_secret, "ignored file should be preserved in worktree")
452 if preserved_secret then
453 local content = preserved_secret:read("*a")
454 preserved_secret:close()
455 assert.are.equal("secret data\n", content)
456 end
457
458 -- Verify ignored directory was preserved
459 local preserved_cache = io.open(worktree .. "/cache/data.bin", "r")
460 assert.is_not_nil(preserved_cache, "ignored directory should be preserved in worktree")
461 if preserved_cache then
462 preserved_cache:close()
463 end
464
465 -- Verify removed from root
466 local root_secret = io.open(project .. "/config.secret", "r")
467 assert.is_nil(root_secret, "ignored file should be removed from root")
468 end)
469
470 it("shows untracked/ignored files in dry-run output", function()
471 if not temp_dir then
472 pending("temp_dir not available")
473 return
474 end
475 local project = temp_dir .. "/dry-run-preserve"
476 os.execute("mkdir -p " .. project)
477 git("init " .. project)
478 git("-C " .. project .. " commit --allow-empty -m 'initial'")
479
480 -- Create untracked file
481 local f = io.open(project .. "/notes.txt", "w")
482 if f then
483 f:write("notes\n")
484 f:close()
485 end
486
487 local cmd = "cd " .. project .. " && lua " .. original_cwd .. "/src/main.lua init --dry-run 2>&1"
488 local output, code = wt.run_cmd(cmd)
489 assert.are.equal(0, code)
490 assert.is_truthy(output:match("notes.txt") or output:match("untracked"))
491 end)
492 end)
493end)