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