1package.path = package.path .. ";./?.lua"
2local wt = dofile("src/main.lua")
3
4describe("cmd_remove", function()
5 local temp_dir
6 local original_cwd
7
8 setup(function()
9 local handle = io.popen("pwd")
10 if handle then
11 original_cwd = handle:read("*l")
12 handle:close()
13 end
14 handle = io.popen("mktemp -d")
15 if handle then
16 temp_dir = handle:read("*l")
17 handle:close()
18 end
19 end)
20
21 teardown(function()
22 if original_cwd then
23 os.execute("cd " .. original_cwd)
24 end
25 if temp_dir then
26 os.execute("rm -rf " .. temp_dir)
27 end
28 end)
29
30 ---Helper to create a wt-managed project with worktrees
31 ---@param name string project name
32 ---@param branches string[] branches to create worktrees for
33 ---@return string project_path
34 local function create_wt_project(name, branches)
35 local project = temp_dir .. "/" .. name
36 os.execute("mkdir -p " .. project .. "/.bare")
37 os.execute("git init --bare " .. project .. "/.bare")
38 local f = io.open(project .. "/.git", "w")
39 if f then
40 f:write("gitdir: ./.bare\n")
41 f:close()
42 end
43
44 local first_branch = branches[1] or "main"
45 os.execute("GIT_DIR=" .. project .. "/.bare git symbolic-ref HEAD refs/heads/" .. first_branch)
46 local first_wt = project .. "/" .. first_branch
47 os.execute("GIT_DIR=" .. project .. "/.bare git worktree add --orphan -b " .. first_branch .. " -- " .. first_wt)
48 os.execute("cd " .. first_wt .. " && git commit --allow-empty -m 'initial'")
49
50 for i = 2, #branches do
51 local branch = branches[i]
52 local wt_path = project .. "/" .. branch:gsub("/", "_")
53 local cmd = "GIT_DIR=" .. project .. "/.bare git worktree add -b "
54 .. branch .. " -- " .. wt_path .. " " .. first_branch
55 os.execute(cmd)
56 end
57
58 return project
59 end
60
61 describe("worktree not found", function()
62 it("errors when target worktree does not exist", function()
63 if not temp_dir then
64 pending("temp_dir not available")
65 return
66 end
67 local project = create_wt_project("no-wt", { "main" })
68
69 local output, code = wt.run_cmd("cd " .. project .. " && lua " .. original_cwd .. "/src/main.lua r nonexistent 2>&1")
70 assert.are_not.equal(0, code)
71 assert.is_truthy(output:match("no worktree found") or output:match("not found"))
72 end)
73 end)
74
75 describe("cwd inside target worktree", function()
76 it("errors when trying to remove worktree while inside it", function()
77 if not temp_dir then
78 pending("temp_dir not available")
79 return
80 end
81 local project = create_wt_project("inside-wt", { "main", "feature" })
82 local feature_wt = project .. "/feature"
83
84 local output, code = wt.run_cmd("cd " .. feature_wt .. " && lua " .. original_cwd .. "/src/main.lua r feature 2>&1")
85 assert.are_not.equal(0, code)
86 assert.is_truthy(output:match("inside") or output:match("cannot remove"))
87 end)
88 end)
89
90 describe("uncommitted changes", function()
91 it("errors when worktree has uncommitted changes without -f", function()
92 if not temp_dir then
93 pending("temp_dir not available")
94 return
95 end
96 local project = create_wt_project("dirty-wt", { "main", "dirty" })
97 local dirty_wt = project .. "/dirty"
98
99 local f = io.open(dirty_wt .. "/uncommitted.txt", "w")
100 if f then
101 f:write("dirty file\n")
102 f:close()
103 end
104 os.execute("cd " .. dirty_wt .. " && git add uncommitted.txt")
105
106 local output, code = wt.run_cmd("cd " .. project .. "/main && lua " .. original_cwd .. "/src/main.lua r dirty 2>&1")
107 assert.are_not.equal(0, code)
108 assert.is_truthy(output:match("uncommitted") or output:match("%-f") or output:match("force"))
109 end)
110
111 it("succeeds with -f when worktree has uncommitted changes", function()
112 if not temp_dir then
113 pending("temp_dir not available")
114 return
115 end
116 local project = create_wt_project("force-dirty", { "main", "force-test" })
117 local target_wt = project .. "/force-test"
118
119 local f = io.open(target_wt .. "/uncommitted.txt", "w")
120 if f then
121 f:write("dirty file\n")
122 f:close()
123 end
124 os.execute("cd " .. target_wt .. " && git add uncommitted.txt")
125
126 local cmd = "cd " .. project .. "/main && lua " .. original_cwd .. "/src/main.lua r force-test -f 2>&1"
127 local output, code = wt.run_cmd(cmd)
128 assert.are.equal(0, code)
129 assert.is_truthy(output:match("[Rr]emoved") or output:match("[Ss]uccess"))
130
131 local check = io.open(target_wt .. "/.git", "r")
132 assert.is_nil(check)
133 end)
134 end)
135
136 describe("branch deletion with -b", function()
137 it("refuses to delete branch if it's bare repo HEAD", function()
138 if not temp_dir then
139 pending("temp_dir not available")
140 return
141 end
142 local project = create_wt_project("bare-head", { "main", "other" })
143
144 os.execute("GIT_DIR=" .. project .. "/.bare git worktree remove -- " .. project .. "/main")
145
146 os.execute("GIT_DIR=" .. project .. "/.bare git symbolic-ref HEAD refs/heads/main")
147 os.execute("GIT_DIR=" .. project .. "/.bare git worktree add -- " .. project .. "/main main")
148
149 local cmd = "cd " .. project .. "/other && lua " .. original_cwd .. "/src/main.lua r main -b 2>&1"
150 local output, _ = wt.run_cmd(cmd)
151 assert.is_truthy(output:match("HEAD") or output:match("retained") or output:match("cannot delete"))
152 end)
153
154 it("refuses to delete branch if checked out in another worktree", function()
155 if not temp_dir then
156 pending("temp_dir not available")
157 return
158 end
159 local project = create_wt_project("multi-checkout", { "main", "feature", "other" })
160
161 local cmd = "cd " .. project .. "/other && lua " .. original_cwd .. "/src/main.lua r feature -b 2>&1"
162 local output, code = wt.run_cmd(cmd)
163 assert.are.equal(0, code)
164 assert.is_truthy(output:match("[Rr]emoved"))
165 local ref_cmd = "GIT_DIR=" .. project .. "/.bare git show-ref --verify --quiet refs/heads/feature"
166 local branch_exists = wt.run_cmd_silent(ref_cmd)
167 assert.is_false(branch_exists)
168 end)
169
170 it("deletes branch when no conflicts exist", function()
171 if not temp_dir then
172 pending("temp_dir not available")
173 return
174 end
175 local project = create_wt_project("clean-delete", { "main", "deleteme" })
176
177 os.execute("GIT_DIR=" .. project .. "/.bare git symbolic-ref HEAD refs/heads/main")
178
179 local cmd = "cd " .. project .. "/main && lua " .. original_cwd .. "/src/main.lua r deleteme -b 2>&1"
180 local output, code = wt.run_cmd(cmd)
181 assert.are.equal(0, code)
182 assert.is_truthy(output:match("branch") and output:match("[Rr]emoved"))
183
184 local ref_cmd = "GIT_DIR=" .. project .. "/.bare git show-ref --verify --quiet refs/heads/deleteme"
185 local branch_exists = wt.run_cmd_silent(ref_cmd)
186 assert.is_false(branch_exists)
187 end)
188 end)
189
190 describe("successful removal", function()
191 it("removes worktree without -b", function()
192 if not temp_dir then
193 pending("temp_dir not available")
194 return
195 end
196 local project = create_wt_project("simple-remove", { "main", "removeme" })
197
198 local cmd = "cd " .. project .. "/main && lua " .. original_cwd .. "/src/main.lua r removeme 2>&1"
199 local output, code = wt.run_cmd(cmd)
200 assert.are.equal(0, code)
201 assert.is_truthy(output:match("[Rr]emoved"))
202
203 local check = io.open(project .. "/removeme/.git", "r")
204 assert.is_nil(check)
205
206 local ref_cmd = "GIT_DIR=" .. project .. "/.bare git show-ref --verify --quiet refs/heads/removeme"
207 local branch_exists = wt.run_cmd_silent(ref_cmd)
208 assert.is_true(branch_exists)
209 end)
210
211 it("finds worktree by branch regardless of path style", function()
212 if not temp_dir then
213 pending("temp_dir not available")
214 return
215 end
216 local project = create_wt_project("path-agnostic", { "main" })
217
218 -- Create worktree at flat-style path even though default config is nested
219 local branch = "feature/auth"
220 local flat_path = project .. "/feature_auth"
221 local wt_cmd = "GIT_DIR=" .. project .. "/.bare git worktree add -b "
222 .. branch .. " -- " .. flat_path .. " main 2>&1"
223 os.execute(wt_cmd)
224
225 -- wt r should find it by branch, not by computed path
226 local cmd = "cd " .. project .. "/main && lua " .. original_cwd .. "/src/main.lua r feature/auth 2>&1"
227 local output, code = wt.run_cmd(cmd)
228 assert.are.equal(0, code)
229 assert.is_truthy(output:match("[Rr]emoved"))
230
231 -- Verify worktree is gone
232 local check = io.open(flat_path .. "/.git", "r")
233 assert.is_nil(check)
234 end)
235
236 it("finds worktree at arbitrary path", function()
237 if not temp_dir then
238 pending("temp_dir not available")
239 return
240 end
241 local project = create_wt_project("arbitrary-path", { "main" })
242
243 -- Create worktree at completely arbitrary path
244 local branch = "my-feature"
245 local arbitrary_path = project .. "/some/weird/location"
246 os.execute("mkdir -p " .. project .. "/some/weird")
247 local wt_cmd = "GIT_DIR=" .. project .. "/.bare git worktree add -b "
248 .. branch .. " -- " .. arbitrary_path .. " main 2>&1"
249 os.execute(wt_cmd)
250
251 local cmd = "cd " .. project .. "/main && lua " .. original_cwd .. "/src/main.lua r my-feature 2>&1"
252 local output, code = wt.run_cmd(cmd)
253 assert.are.equal(0, code)
254 assert.is_truthy(output:match("[Rr]emoved"))
255 end)
256 end)
257
258 describe("usage errors", function()
259 it("errors when no branch argument provided", function()
260 if not temp_dir then
261 pending("temp_dir not available")
262 return
263 end
264 local project = create_wt_project("no-arg", { "main" })
265
266 local output, code = wt.run_cmd("cd " .. project .. " && lua " .. original_cwd .. "/src/main.lua r 2>&1")
267 assert.are_not.equal(0, code)
268 assert.is_truthy(output:match("usage") or output:match("Usage"))
269 end)
270 end)
271end)