05a642b7a9
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>
176 lines
5.4 KiB
Lua
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)
|