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_remove", 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 ---Helper to create a wt-managed project with worktrees
37 ---@param name string project name
38 ---@param branches string[] branches to create worktrees for
39 ---@return string project_path
40 local function create_wt_project(name, branches)
41 local project = temp_dir .. "/" .. name
42 os.execute("mkdir -p " .. project .. "/.bare")
43 git("init --bare " .. project .. "/.bare")
44 local f = io.open(project .. "/.git", "w")
45 if f then
46 f:write("gitdir: ./.bare\n")
47 f:close()
48 end
49
50 local first_branch = branches[1] or "main"
51 git("--git-dir=" .. project .. "/.bare symbolic-ref HEAD refs/heads/" .. first_branch)
52 local first_wt = project .. "/" .. first_branch
53 git("--git-dir=" .. project .. "/.bare worktree add --orphan -b " .. first_branch .. " -- " .. first_wt)
54 git("-C " .. first_wt .. " commit --allow-empty -m 'initial'")
55
56 for i = 2, #branches do
57 local branch = branches[i]
58 local wt_path = project .. "/" .. branch:gsub("/", "_")
59 git("--git-dir=" .. project .. "/.bare worktree add -b " .. branch .. " -- " .. wt_path .. " " .. first_branch)
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 git("-C " .. dirty_wt .. " 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 git("-C " .. target_wt .. " 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 git("--git-dir=" .. project .. "/.bare worktree remove -- " .. project .. "/main")
149
150 git("--git-dir=" .. project .. "/.bare symbolic-ref HEAD refs/heads/main")
151 git("--git-dir=" .. project .. "/.bare 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 git("--git-dir=" .. project .. "/.bare 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 git("--git-dir=" .. project .. "/.bare worktree add -b " .. branch .. " -- " .. flat_path .. " main")
226
227 -- wt r should find it by branch, not by computed path
228 local cmd = "cd " .. project .. "/main && lua " .. original_cwd .. "/src/main.lua r feature/auth 2>&1"
229 local output, code = wt.run_cmd(cmd)
230 assert.are.equal(0, code)
231 assert.is_truthy(output:match("[Rr]emoved"))
232
233 -- Verify worktree is gone
234 local check = io.open(flat_path .. "/.git", "r")
235 assert.is_nil(check)
236 end)
237
238 it("finds worktree at arbitrary path", function()
239 if not temp_dir then
240 pending("temp_dir not available")
241 return
242 end
243 local project = create_wt_project("arbitrary-path", { "main" })
244
245 -- Create worktree at completely arbitrary path
246 local branch = "my-feature"
247 local arbitrary_path = project .. "/some/weird/location"
248 os.execute("mkdir -p " .. project .. "/some/weird")
249 git("--git-dir=" .. project .. "/.bare worktree add -b " .. branch .. " -- " .. arbitrary_path .. " main")
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)