git_parsing_spec.lua

  1-- Load main.lua as a module (it exports functions when required)
  2package.path = package.path .. ";./?.lua"
  3local wt = dofile("src/main.lua")
  4
  5describe("escape_pattern", function()
  6	it("escapes percent sign", function()
  7		assert.are.equal("100%%", wt.escape_pattern("100%"))
  8	end)
  9
 10	it("escapes dot (wildcard)", function()
 11		assert.are.equal("file%.txt", wt.escape_pattern("file.txt"))
 12	end)
 13
 14	it("escapes hyphen", function()
 15		assert.are.equal("my%-branch", wt.escape_pattern("my-branch"))
 16	end)
 17
 18	it("escapes brackets", function()
 19		assert.are.equal("%[test%]", wt.escape_pattern("[test]"))
 20	end)
 21
 22	it("escapes parentheses", function()
 23		assert.are.equal("func%(%)", wt.escape_pattern("func()"))
 24	end)
 25
 26	it("escapes caret and dollar", function()
 27		assert.are.equal("%^start end%$", wt.escape_pattern("^start end$"))
 28	end)
 29
 30	it("escapes asterisk and question mark", function()
 31		assert.are.equal("glob%*%?", wt.escape_pattern("glob*?"))
 32	end)
 33
 34	it("escapes plus sign", function()
 35		assert.are.equal("a%+b", wt.escape_pattern("a+b"))
 36	end)
 37
 38	it("leaves alphanumeric and slashes unchanged", function()
 39		assert.are.equal("feature/foo/bar123", wt.escape_pattern("feature/foo/bar123"))
 40	end)
 41end)
 42
 43describe("parse_branch_remotes", function()
 44	describe("simple branches", function()
 45		it("parses single remote", function()
 46			local output = "  origin/main\n"
 47			local remotes = wt.parse_branch_remotes(output, "main")
 48			assert.are.same({ "origin" }, remotes)
 49		end)
 50
 51		it("parses multiple remotes", function()
 52			local output = "  origin/main\n  upstream/main\n"
 53			local remotes = wt.parse_branch_remotes(output, "main")
 54			assert.are.same({ "origin", "upstream" }, remotes)
 55		end)
 56
 57		it("returns empty for no matches", function()
 58			local output = "  origin/develop\n"
 59			local remotes = wt.parse_branch_remotes(output, "main")
 60			assert.are.same({}, remotes)
 61		end)
 62
 63		it("handles empty output", function()
 64			local remotes = wt.parse_branch_remotes("", "main")
 65			assert.are.same({}, remotes)
 66		end)
 67	end)
 68
 69	describe("slashy branches (the tricky case)", function()
 70		it("correctly parses feature/foo on origin", function()
 71			local output = "  origin/feature/foo\n"
 72			local remotes = wt.parse_branch_remotes(output, "feature/foo")
 73			assert.are.same({ "origin" }, remotes)
 74		end)
 75
 76		it("correctly parses deeply nested branch", function()
 77			local output = "  upstream/user/alice/feature/auth\n"
 78			local remotes = wt.parse_branch_remotes(output, "user/alice/feature/auth")
 79			assert.are.same({ "upstream" }, remotes)
 80		end)
 81
 82		it("handles multiple remotes with slashy branch", function()
 83			local output = "  origin/feature/auth\n  upstream/feature/auth\n"
 84			local remotes = wt.parse_branch_remotes(output, "feature/auth")
 85			assert.are.same({ "origin", "upstream" }, remotes)
 86		end)
 87
 88		it("does not confuse similar branch names", function()
 89			local output = "  origin/feature/foo\n  origin/feature/foobar\n"
 90			local remotes = wt.parse_branch_remotes(output, "feature/foo")
 91			assert.are.same({ "origin" }, remotes)
 92		end)
 93	end)
 94
 95	describe("special characters in branch names", function()
 96		it("handles dots in branch name", function()
 97			local output = "  origin/release/1.2.3\n"
 98			local remotes = wt.parse_branch_remotes(output, "release/1.2.3")
 99			assert.are.same({ "origin" }, remotes)
100		end)
101
102		it("handles hyphens in branch name", function()
103			local output = "  origin/fix/bug-123-thing\n"
104			local remotes = wt.parse_branch_remotes(output, "fix/bug-123-thing")
105			assert.are.same({ "origin" }, remotes)
106		end)
107
108		it("handles underscores", function()
109			local output = "  origin/my_feature_branch\n"
110			local remotes = wt.parse_branch_remotes(output, "my_feature_branch")
111			assert.are.same({ "origin" }, remotes)
112		end)
113	end)
114
115	describe("edge cases", function()
116		it("handles extra whitespace", function()
117			local output = "    origin/main   \n"
118			local remotes = wt.parse_branch_remotes(output, "main")
119			assert.are.same({ "origin" }, remotes)
120		end)
121
122		it("ignores HEAD pointer lines", function()
123			-- git branch -r sometimes shows: origin/HEAD -> origin/main
124			local output = "  origin/HEAD -> origin/main\n  origin/main\n"
125			local remotes = wt.parse_branch_remotes(output, "main")
126			assert.are.same({ "origin" }, remotes)
127		end)
128
129		it("handles branch name that looks like remote/branch", function()
130			local output = "  upstream/origin/main\n"
131			local remotes = wt.parse_branch_remotes(output, "origin/main")
132			assert.are.same({ "upstream" }, remotes)
133		end)
134	end)
135end)
136
137describe("parse_worktree_list", function()
138	describe("basic parsing", function()
139		it("parses single worktree with branch", function()
140			local output = [[worktree /home/user/project/main
141HEAD abc123def456
142branch refs/heads/main
143]]
144			local worktrees = wt.parse_worktree_list(output)
145			assert.are.equal(1, #worktrees)
146			assert.are.equal("/home/user/project/main", worktrees[1].path)
147			assert.are.equal("main", worktrees[1].branch)
148			assert.are.equal("abc123def456", worktrees[1].head)
149		end)
150
151		it("parses multiple worktrees", function()
152			local output = [[worktree /home/user/project/.bare
153bare
154
155worktree /home/user/project/main
156HEAD abc123
157branch refs/heads/main
158
159worktree /home/user/project/feature/auth
160HEAD def456
161branch refs/heads/feature/auth
162]]
163			local worktrees = wt.parse_worktree_list(output)
164			assert.are.equal(3, #worktrees)
165
166			assert.are.equal("/home/user/project/.bare", worktrees[1].path)
167			assert.is_true(worktrees[1].bare)
168
169			assert.are.equal("/home/user/project/main", worktrees[2].path)
170			assert.are.equal("main", worktrees[2].branch)
171
172			assert.are.equal("/home/user/project/feature/auth", worktrees[3].path)
173			assert.are.equal("feature/auth", worktrees[3].branch)
174		end)
175	end)
176
177	describe("special states", function()
178		it("parses detached HEAD worktree", function()
179			local output = [[worktree /home/user/project/detached
180HEAD abc123
181detached
182]]
183			local worktrees = wt.parse_worktree_list(output)
184			assert.are.equal(1, #worktrees)
185			assert.is_true(worktrees[1].detached)
186			assert.is_nil(worktrees[1].branch)
187		end)
188
189		it("parses bare repository entry", function()
190			local output = [[worktree /home/user/project/.bare
191bare
192]]
193			local worktrees = wt.parse_worktree_list(output)
194			assert.are.equal(1, #worktrees)
195			assert.is_true(worktrees[1].bare)
196		end)
197	end)
198
199	describe("branch name handling", function()
200		it("strips refs/heads/ prefix", function()
201			local output = [[worktree /path
202HEAD abc
203branch refs/heads/my-branch
204]]
205			local worktrees = wt.parse_worktree_list(output)
206			assert.are.equal("my-branch", worktrees[1].branch)
207		end)
208
209		it("handles slashy branch names", function()
210			local output = [[worktree /path
211HEAD abc
212branch refs/heads/feature/auth/oauth2
213]]
214			local worktrees = wt.parse_worktree_list(output)
215			assert.are.equal("feature/auth/oauth2", worktrees[1].branch)
216		end)
217	end)
218
219	describe("edge cases", function()
220		it("handles empty output", function()
221			local worktrees = wt.parse_worktree_list("")
222			assert.are.same({}, worktrees)
223		end)
224
225		it("handles paths with spaces", function()
226			local output = [[worktree /home/user/my project/main
227HEAD abc
228branch refs/heads/main
229]]
230			local worktrees = wt.parse_worktree_list(output)
231			assert.are.equal("/home/user/my project/main", worktrees[1].path)
232		end)
233	end)
234end)
235
236describe("path_inside", function()
237	describe("exact matches", function()
238		it("returns true for identical paths", function()
239			assert.is_true(wt.path_inside("/home/user", "/home/user"))
240		end)
241
242		it("returns true when both have trailing slashes", function()
243			assert.is_true(wt.path_inside("/home/user/", "/home/user/"))
244		end)
245
246		it("returns true with mixed trailing slashes", function()
247			assert.is_true(wt.path_inside("/home/user", "/home/user/"))
248			assert.is_true(wt.path_inside("/home/user/", "/home/user"))
249		end)
250	end)
251
252	describe("containment", function()
253		it("returns true for direct child", function()
254			assert.is_true(wt.path_inside("/home/user/project", "/home/user"))
255		end)
256
257		it("returns true for deep nesting", function()
258			assert.is_true(wt.path_inside("/home/user/project/src/lib", "/home/user"))
259		end)
260
261		it("returns false for sibling", function()
262			assert.is_false(wt.path_inside("/home/alice", "/home/bob"))
263		end)
264
265		it("returns false for parent", function()
266			assert.is_false(wt.path_inside("/home", "/home/user"))
267		end)
268
269		it("returns false for unrelated paths", function()
270			assert.is_false(wt.path_inside("/var/log", "/home/user"))
271		end)
272	end)
273
274	describe("prefix edge cases", function()
275		it("does not match partial directory names", function()
276			-- /home/user-backup is NOT inside /home/user
277			assert.is_false(wt.path_inside("/home/user-backup", "/home/user"))
278		end)
279
280		it("does not match substring prefixes", function()
281			assert.is_false(wt.path_inside("/home/username", "/home/user"))
282		end)
283
284		it("correctly handles similar names", function()
285			assert.is_false(wt.path_inside("/project-old/src", "/project"))
286			assert.is_true(wt.path_inside("/project/src", "/project"))
287		end)
288	end)
289
290	describe("root paths", function()
291		it("everything is inside root", function()
292			assert.is_true(wt.path_inside("/home/user", "/"))
293		end)
294
295		it("root is inside root", function()
296			assert.is_true(wt.path_inside("/", "/"))
297		end)
298	end)
299end)
300
301describe("summarize_hooks", function()
302	describe("single hook type", function()
303		it("summarizes copy hooks", function()
304			local hooks = { copy = { ".env", "Makefile" } }
305			local summary = wt.summarize_hooks(hooks)
306			assert.are.equal("copy: .env, Makefile", summary)
307		end)
308
309		it("summarizes symlink hooks", function()
310			local hooks = { symlink = { "node_modules" } }
311			local summary = wt.summarize_hooks(hooks)
312			assert.are.equal("symlink: node_modules", summary)
313		end)
314
315		it("summarizes run hooks", function()
316			local hooks = { run = { "npm install", "make setup" } }
317			local summary = wt.summarize_hooks(hooks)
318			assert.are.equal("run: npm install, make setup", summary)
319		end)
320	end)
321
322	describe("multiple hook types", function()
323		it("combines all types with semicolons", function()
324			local hooks = {
325				copy = { ".env" },
326				symlink = { "node_modules" },
327				run = { "npm install" },
328			}
329			local summary = wt.summarize_hooks(hooks)
330			assert.are.equal("copy: .env; symlink: node_modules; run: npm install", summary)
331		end)
332	end)
333
334	describe("truncation", function()
335		it("shows first 3 items with count for copy", function()
336			local hooks = { copy = { "a", "b", "c", "d", "e" } }
337			local summary = wt.summarize_hooks(hooks)
338			assert.are.equal("copy: a, b, c (+2 more)", summary)
339		end)
340
341		it("shows first 3 items with count for symlink", function()
342			local hooks = { symlink = { "1", "2", "3", "4" } }
343			local summary = wt.summarize_hooks(hooks)
344			assert.are.equal("symlink: 1, 2, 3 (+1 more)", summary)
345		end)
346
347		it("shows first 3 items with count for run", function()
348			local hooks = { run = { "cmd1", "cmd2", "cmd3", "cmd4", "cmd5", "cmd6" } }
349			local summary = wt.summarize_hooks(hooks)
350			assert.are.equal("run: cmd1, cmd2, cmd3 (+3 more)", summary)
351		end)
352
353		it("does not add suffix for exactly 3 items", function()
354			local hooks = { copy = { "a", "b", "c" } }
355			local summary = wt.summarize_hooks(hooks)
356			assert.are.equal("copy: a, b, c", summary)
357		end)
358	end)
359
360	describe("edge cases", function()
361		it("returns empty string for empty hooks", function()
362			local summary = wt.summarize_hooks({})
363			assert.are.equal("", summary)
364		end)
365
366		it("returns empty string for nil hooks table", function()
367			local summary = wt.summarize_hooks({})
368			assert.are.equal("", summary)
369		end)
370
371		it("skips empty arrays", function()
372			local hooks = { copy = {}, run = { "npm install" } }
373			local summary = wt.summarize_hooks(hooks)
374			assert.are.equal("run: npm install", summary)
375		end)
376	end)
377end)