Files
dotfiles/.config/nvim/tests/worktree_spec.lua
T
unkin-agent 05a642b7a9 feat(nvim): add git worktree picker that follows open buffers
Adds a telescope picker over 'git worktree list --porcelain'. Selecting an
entry cds to that worktree and re-points every open buffer at the same
relative path there, keeping cursor position and refusing to clobber
unsaved work.

Signed-off-by: unkin-agent <unkin-agent@unkin.net>
2026-09-05 11:19:42 +10:00

176 lines
5.4 KiB
Lua

--[[ tests/worktree_spec.lua ]]
local worktree = require('func.worktree')
local function sh(cmd)
local out = vim.fn.system(cmd)
assert(vim.v.shell_error == 0, table.concat(cmd, ' ') .. ' failed: ' .. out)
return out
end
local function write(path, text)
vim.fn.mkdir(vim.fn.fnamemodify(path, ':h'), 'p')
vim.fn.writefile(vim.split(text, '\n'), path)
end
local function realpath(path)
return (vim.uv or vim.loop).fs_realpath(path)
end
local function open(path)
vim.cmd('edit ' .. vim.fn.fnameescape(path))
return vim.api.nvim_get_current_buf()
end
local function wipe_all()
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
pcall(vim.api.nvim_buf_delete, buf, { force = true })
end
end
describe('func.worktree', function()
local tmp, main, other
before_each(function()
tmp = vim.fn.tempname()
vim.fn.mkdir(tmp, 'p')
tmp = realpath(tmp)
main = tmp .. '/main'
other = tmp .. '/other'
vim.fn.mkdir(main, 'p')
sh({ 'git', '-C', main, 'init', '-q', '-b', 'main' })
sh({ 'git', '-C', main, 'config', 'user.email', 'test@example.com' })
sh({ 'git', '-C', main, 'config', 'user.name', 'test' })
write(main .. '/one.txt', 'alpha\nbravo\ncharlie\ndelta')
write(main .. '/sub/two.txt', 'two')
sh({ 'git', '-C', main, 'add', '-A' })
sh({ 'git', '-C', main, 'commit', '-qm', 'init' })
sh({ 'git', '-C', main, 'worktree', 'add', '-q', '-b', 'feature', other })
write(main .. '/main-only.txt', 'only in main')
sh({ 'git', '-C', main, 'add', '-A' })
sh({ 'git', '-C', main, 'commit', '-qm', 'main only' })
wipe_all()
vim.cmd('cd ' .. vim.fn.fnameescape(main))
end)
after_each(function()
wipe_all()
vim.cmd('cd /')
vim.fn.delete(tmp, 'rf')
end)
it('lists every worktree of the repo', function()
local trees = worktree.list(main)
assert.equals(2, #trees)
local branches = {}
for _, tree in ipairs(trees) do
branches[tree.branch] = tree.path
end
assert.equals(realpath(main), branches['main'])
assert.equals(realpath(other), branches['feature'])
end)
it('reports an error outside a repository', function()
local trees, err = worktree.list(tmp)
assert.is_nil(trees)
assert.is_true(#err > 0)
end)
it('labels detached and bare entries', function()
assert.equals('feature', worktree.label({ branch = 'feature' }))
assert.equals('(bare)', worktree.label({ bare = true }))
assert.equals('(detached abcdef1)', worktree.label({ head = 'abcdef1234567890' }))
end)
it('re-points buffers at the same relative path', function()
local one = open(main .. '/one.txt')
open(main .. '/sub/two.txt')
local summary = worktree.switch(other)
assert.is_not_nil(summary)
assert.equals(realpath(other), summary.root)
assert.equals(realpath(other), realpath(vim.fn.getcwd()))
table.sort(summary.moved)
assert.same({ 'one.txt', 'sub/two.txt' }, summary.moved)
assert.is_false(vim.api.nvim_buf_is_valid(one))
local open_names = {}
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
if vim.bo[buf].buflisted then
open_names[vim.api.nvim_buf_get_name(buf)] = true
end
end
assert.is_true(open_names[realpath(other) .. '/one.txt'])
assert.is_true(open_names[realpath(other) .. '/sub/two.txt'])
end)
it('keeps the cursor of a displayed buffer', function()
open(main .. '/one.txt')
vim.api.nvim_win_set_cursor(0, { 3, 2 })
local one_win = vim.api.nvim_get_current_win()
vim.cmd('split')
open(main .. '/sub/two.txt')
worktree.switch(other)
assert.equals(realpath(other) .. '/one.txt', vim.api.nvim_buf_get_name(vim.api.nvim_win_get_buf(one_win)))
assert.same({ 3, 2 }, vim.api.nvim_win_get_cursor(one_win))
end)
it('carries the last position of a hidden buffer', function()
open(main .. '/one.txt')
vim.api.nvim_win_set_cursor(0, { 3, 2 })
open(main .. '/sub/two.txt')
worktree.switch(other)
local mark = vim.api.nvim_buf_get_mark(vim.fn.bufnr(realpath(other) .. '/one.txt'), '"')
assert.equals(3, mark[1])
end)
it('leaves and flags buffers with no counterpart', function()
local only = open(main .. '/main-only.txt')
local summary = worktree.switch(other)
assert.same({ 'main-only.txt' }, summary.missing)
assert.is_true(vim.api.nvim_buf_is_valid(only))
assert.equals(realpath(main) .. '/main-only.txt', vim.api.nvim_buf_get_name(only))
assert.equals(realpath(main), vim.b[only].worktree_orphan)
end)
it('refuses to switch when an unsaved buffer would be replaced', function()
open(main .. '/one.txt')
vim.api.nvim_buf_set_lines(0, 0, 1, false, { 'dirty' })
local summary = worktree.switch(other)
assert.is_nil(summary)
assert.equals(realpath(main), realpath(vim.fn.getcwd()))
assert.is_true(vim.bo.modified)
end)
it('keeps unsaved buffers that have no counterpart', function()
local only = open(main .. '/main-only.txt')
vim.api.nvim_buf_set_lines(only, 0, 1, false, { 'dirty' })
local summary = worktree.switch(other)
assert.is_not_nil(summary)
assert.same({ 'main-only.txt' }, summary.kept)
assert.is_true(vim.bo[only].modified)
assert.equals(realpath(main) .. '/main-only.txt', vim.api.nvim_buf_get_name(only))
end)
it('is a no-op when the target is the current worktree', function()
local summary = worktree.switch(main)
assert.same({}, summary.moved)
assert.equals(realpath(main), summary.root)
end)
end)