Files
nvim/test/git/status_view_test.lua
T

488 lines
16 KiB
Lua

local h = require("test.git.helpers")
local t = require("test")
---Replicate the user's global cursor-restore autocmd. Scoped to a
---named augroup + cleanup so it doesn't leak between tests.
local function install_cursor_restore_autocmd()
local group =
vim.api.nvim_create_augroup("test.cursor_restore", { clear = true })
vim.api.nvim_create_autocmd("BufReadPost", {
group = group,
pattern = "*",
command = 'silent! normal! g`"zv',
})
t.defer(function()
pcall(vim.api.nvim_del_augroup_by_name, "test.cursor_restore")
end)
end
---@param sidebar_buf integer
---@param needle string
---@return integer?
local function find_line(sidebar_buf, needle)
for i, l in ipairs(vim.api.nvim_buf_get_lines(sidebar_buf, 0, -1, false)) do
if l:match(needle) then
return i
end
end
end
---Find the gitstatus sidebar window in the current tabpage.
---@return integer? sidebar_buf
---@return integer? sidebar_win
local function find_sidebar()
for _, w in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
local b = vim.api.nvim_win_get_buf(w)
if vim.bo[b].filetype == "gitstatus" then
return b, w
end
end
end
---Find a diff window in the given tabpage (or current). "left" / "right"
---is determined by column position: the layout is [sidebar | left | right],
---so the leftmost &diff window is the left pane and the rightmost is the
---right pane.
---@param role "left"|"right"
---@param tab integer?
---@return integer?
local function find_diff_win(role, tab)
local diffs = {}
for _, w in ipairs(vim.api.nvim_tabpage_list_wins(tab or 0)) do
if vim.wo[w].diff then
table.insert(diffs, w)
end
end
table.sort(diffs, function(a, b)
return vim.api.nvim_win_get_position(a)[2]
< vim.api.nvim_win_get_position(b)[2]
end)
if role == "left" then
return diffs[1]
end
return diffs[#diffs]
end
---@param file_path string
---@param committed_content string
---@param worktree_content string
---@return integer sidebar_win
---@return integer entry_line
local function setup_sidebar_with_unstaged_file(
file_path,
committed_content,
worktree_content
)
local repo = h.make_repo({ [file_path] = committed_content })
t.write(repo, file_path, worktree_content)
vim.cmd("cd " .. repo)
require("git.status_view").open({ placement = "sidebar" })
local sidebar_buf, sidebar_win = find_sidebar()
assert(sidebar_buf, "sidebar buffer should exist")
assert(sidebar_win, "sidebar window should exist")
local r = assert(
require("git.core.repo").find(vim.fn.getcwd()),
"repo should resolve for the test worktree"
)
r:refresh()
t.wait_for(function()
return r.status and #r.status:rows("unstaged") > 0
end, "git status to report unstaged changes")
local entry_line = assert(
find_line(sidebar_buf, vim.pesc(file_path) .. "$"),
file_path .. " should appear in sidebar"
)
return sidebar_win, entry_line
end
t.test("stage with diff open: sidebar cursor stays put", function()
install_cursor_restore_autocmd()
local sidebar_win, line = setup_sidebar_with_unstaged_file(
"zsh/rc",
"ZSH=true\n",
"ZSH=true\nmodified\n"
)
vim.api.nvim_set_current_win(sidebar_win)
vim.api.nvim_win_set_cursor(sidebar_win, { line, 0 })
t.press("<Tab>")
t.wait_for(function()
return find_diff_win("left") ~= nil
end, "diff windows to appear")
local r = assert(require("git.core.repo").find(vim.fn.getcwd()))
vim.api.nvim_set_current_win(sidebar_win)
t.press("s")
t.wait_for(function()
return #r.status:rows("staged") > 0
end, "stage to propagate to repo state")
t.eq(
vim.api.nvim_win_get_cursor(sidebar_win),
{ line, 0 },
"sidebar cursor should remain at the entry's original line"
)
end)
t.test(
"stage with diff open: diff foldmethod is preserved on refresh",
function()
local sidebar_win, line = setup_sidebar_with_unstaged_file(
"zsh/rc",
"# vim: set ft=zsh nowrap:\nZSH=true\n",
"# vim: set ft=zsh nowrap:\nZSH=true\nmodified\n"
)
vim.api.nvim_set_current_win(sidebar_win)
vim.api.nvim_win_set_cursor(sidebar_win, { line, 0 })
t.press("<Tab>")
t.wait_for(function()
return find_diff_win("left") ~= nil
end, "diff windows to appear")
local left_win = assert(find_diff_win("left"))
t.eq(
vim.wo[left_win].foldmethod,
"diff",
"left diff foldmethod should be 'diff' after Tab"
)
local r = assert(require("git.core.repo").find(vim.fn.getcwd()))
vim.api.nvim_set_current_win(sidebar_win)
t.press("s")
t.wait_for(function()
return #r.status:rows("staged") > 0
end, "stage to propagate to repo state")
t.eq(
vim.wo[left_win].foldmethod,
"diff",
"left diff foldmethod should still be 'diff' after stage refresh"
)
end
)
t.test(
"<Tab> in a second tabpage opens the diff inside that tabpage",
function()
local sidebar_win, line =
setup_sidebar_with_unstaged_file("foo.txt", "v1\n", "v2\n")
local tab1 = vim.api.nvim_get_current_tabpage()
vim.api.nvim_set_current_win(sidebar_win)
vim.api.nvim_win_set_cursor(sidebar_win, { line, 0 })
t.press("<Tab>")
t.wait_for(function()
return find_diff_win("left", tab1) ~= nil
end, "diff windows in tab1 to appear")
vim.cmd("tabnew")
require("git.status_view").open({ placement = "sidebar" })
local tab2 = vim.api.nvim_get_current_tabpage()
t.truthy(tab2 ~= tab1, "tabnew should produce a distinct tabpage")
local _, sidebar_win2 = find_sidebar()
assert(sidebar_win2, "sidebar window should exist in tab2")
vim.api.nvim_set_current_win(sidebar_win2)
vim.api.nvim_win_set_cursor(sidebar_win2, { line, 0 })
t.press("<Tab>")
t.wait_for(function()
return find_diff_win("left", tab2) ~= nil
end, "diff windows in tab2 to appear")
t.truthy(
find_diff_win("right", tab2),
"right diff window should be in tab2"
)
end
)
t.test("refresh on stage updates the index URI buffer's content", function()
local sidebar_win, line =
setup_sidebar_with_unstaged_file("foo.txt", "v1\n", "v2\n")
vim.api.nvim_set_current_win(sidebar_win)
vim.api.nvim_win_set_cursor(sidebar_win, { line, 0 })
t.press("<Tab>")
t.wait_for(function()
return find_diff_win("left") ~= nil
end, "diff windows to appear")
local left_win = assert(find_diff_win("left"))
local index_buf = vim.api.nvim_win_get_buf(left_win)
t.eq(
vim.api.nvim_buf_get_lines(index_buf, 0, -1, false),
{ "v1" },
"index pane should initially show committed content"
)
vim.api.nvim_set_current_win(sidebar_win)
t.press("s")
t.wait_for(function()
local first = vim.api.nvim_buf_get_lines(index_buf, 0, -1, false)[1]
return first == "v2"
end, "index pane to refresh to staged content")
t.eq(
vim.api.nvim_buf_get_lines(index_buf, 0, -1, false),
{ "v2" },
"index pane should reflect staged content after refresh"
)
end)
t.test(
"re-selecting same entry after close + diffsplit keeps fold state in sync",
function()
local committed, worktree = {}, {}
for i = 1, 30 do
committed[i] = "line " .. i
worktree[i] = i == 15 and "CHANGED" or ("line " .. i)
end
local sidebar_win, line = setup_sidebar_with_unstaged_file(
"foo.txt",
table.concat(committed, "\n") .. "\n",
table.concat(worktree, "\n") .. "\n"
)
local prev_foldlevel = vim.o.foldlevel
vim.o.foldlevel = 99
t.defer(function()
vim.o.foldlevel = prev_foldlevel
end)
vim.api.nvim_set_current_win(sidebar_win)
vim.api.nvim_win_set_cursor(sidebar_win, { line, 0 })
t.press("<Tab>")
t.wait_for(function()
return find_diff_win("left") ~= nil
and find_diff_win("right") ~= nil
end, "first diff pair to appear")
local first_left = assert(find_diff_win("left"))
vim.api.nvim_win_close(first_left, false)
local remaining
for _, w in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
if w ~= sidebar_win then
remaining = w
break
end
end
if not remaining then
error("a non-sidebar window should remain after close")
end
vim.api.nvim_set_current_win(remaining)
require("git.diffsplit").open({ mods = { vertical = true } })
t.wait_for(function()
local count = 0
for _, w in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
if vim.wo[w].diff then
count = count + 1
end
end
return count == 2
end, "diffsplit to produce a diff pair")
vim.api.nvim_set_current_win(sidebar_win)
vim.api.nvim_win_set_cursor(sidebar_win, { line, 0 })
t.press("<Tab>")
t.wait_for(function()
local count = 0
for _, w in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
if vim.wo[w].diff then
count = count + 1
end
end
return count == 2
end, "diff pair after re-selecting entry")
local left_win = assert(find_diff_win("left"))
local right_win = assert(find_diff_win("right"))
t.eq(
vim.wo[left_win].foldlevel,
0,
"left pane foldlevel should be 0 after re-select"
)
t.eq(
vim.wo[right_win].foldlevel,
0,
"right pane foldlevel should be 0 after re-select"
)
end
)
t.test("sidebar buffer is named <worktree>/GitStatus", function()
local repo = h.make_repo({ ["foo.txt"] = "x\n" })
vim.cmd("cd " .. repo)
require("git.status_view").open({ placement = "sidebar" })
local r = assert(require("git.core.repo").find(vim.fn.getcwd()))
local buf = find_sidebar()
assert(buf, "sidebar buffer should exist")
t.eq(
vim.api.nvim_buf_get_name(buf),
r.worktree .. "/GitStatus",
"buffer name should be <worktree>/GitStatus"
)
end)
t.test(
"calling open twice without closing focuses the existing sidebar",
function()
local repo = h.make_repo({ ["foo.txt"] = "x\n" })
vim.cmd("cd " .. repo)
require("git.status_view").open({ placement = "sidebar" })
local first = find_sidebar()
assert(first, "first sidebar buffer should exist")
require("git.status_view").open({ placement = "sidebar" })
local second = find_sidebar()
assert(second, "second sidebar buffer should exist")
t.eq(
first,
second,
"consecutive opens should reuse the visible sidebar"
)
local count = 0
for _, b in ipairs(vim.api.nvim_list_bufs()) do
if vim.bo[b].filetype == "gitstatus" then
count = count + 1
end
end
t.eq(count, 1, "only one gitstatus buffer should exist")
end
)
t.test("opening for different worktrees creates separate buffers", function()
local repo_a = h.make_repo({ ["a.txt"] = "x\n" })
local repo_b = h.make_repo({ ["b.txt"] = "y\n" })
vim.cmd("cd " .. repo_a)
require("git.status_view").open({ placement = "sidebar" })
local buf_a = find_sidebar()
require("git.status_view").toggle()
vim.cmd("cd " .. repo_b)
require("git.status_view").open({ placement = "sidebar" })
local buf_b = find_sidebar()
assert(buf_a and buf_b)
t.truthy(
buf_a ~= buf_b,
"different worktrees should produce different buffers"
)
end)
t.test("sidebar buffer is buftype=nofile and not buflisted", function()
local repo = h.make_repo({ ["foo.txt"] = "x\n" })
vim.cmd("cd " .. repo)
require("git.status_view").open({ placement = "sidebar" })
local buf = find_sidebar()
assert(buf, "sidebar buffer should exist")
t.eq(vim.bo[buf].buftype, "nofile", "buftype should be nofile")
t.eq(vim.bo[buf].buflisted, false, "buflisted should be false")
end)
t.test("sidebar buffer name does not get written to disk", function()
local repo = h.make_repo({ ["foo.txt"] = "x\n" })
vim.cmd("cd " .. repo)
require("git.status_view").open({ placement = "sidebar" })
local buf = find_sidebar()
assert(buf, "sidebar buffer should exist")
local name = vim.api.nvim_buf_get_name(buf)
vim.api.nvim_buf_call(buf, function()
pcall(function()
vim.cmd("silent! write")
end)
end)
t.eq(
vim.uv.fs_stat(name),
nil,
"no real file should be created at the sidebar buffer's path"
)
end)
t.test(
"diffsplit from sidebar resets cursor so panes stay in sync",
function()
local committed, worktree = {}, {}
for i = 1, 100 do
committed[i] = "line " .. i
worktree[i] = i == 10
and "CHANGED " .. i
or i == 40 and "CHANGED " .. i
or i == 70 and "CHANGED " .. i
or i == 90 and "CHANGED " .. i
or ("line " .. i)
end
local repo = h.make_repo({
["file.txt"] = table.concat(committed, "\n") .. "\n",
})
t.write(repo, "file.txt", table.concat(worktree, "\n") .. "\n")
vim.cmd("cd " .. repo)
-- Open the worktree file in a normal window and position cursor in
-- what becomes a folded section after diff is set up.
vim.cmd("edit file.txt")
vim.api.nvim_win_set_cursor(0, { 50, 0 })
require("git.status_view").open({ placement = "sidebar" })
local sidebar_buf, sidebar_win = find_sidebar()
assert(sidebar_buf and sidebar_win)
local r = assert(require("git.core.repo").find(vim.fn.getcwd()))
r:refresh()
t.wait_for(function()
return r.status and #r.status:rows("unstaged") > 0
end, "git status to report unstaged changes")
local entry_line
for i, l in
ipairs(vim.api.nvim_buf_get_lines(sidebar_buf, 0, -1, false))
do
if l:match("file.txt$") then
entry_line = i
break
end
end
if not entry_line then
error("entry line should exist")
end
vim.api.nvim_set_current_win(sidebar_win)
vim.api.nvim_win_set_cursor(sidebar_win, { entry_line, 0 })
t.press("<Tab>")
t.wait_for(function()
return find_diff_win("left") ~= nil
and find_diff_win("right") ~= nil
end, "diff pair to appear")
local left_win = assert(find_diff_win("left"))
local right_win = assert(find_diff_win("right"))
local left_top =
vim.api.nvim_win_call(left_win, function() return vim.fn.line("w0") end)
local right_top = vim.api.nvim_win_call(
right_win,
function() return vim.fn.line("w0") end
)
t.eq(
left_top,
right_top,
"left and right panes should have the same topline after diffsplit"
)
t.eq(
vim.api.nvim_win_get_cursor(left_win),
{ 1, 0 },
"left pane should start at line 1"
)
t.eq(
vim.api.nvim_win_get_cursor(right_win),
{ 1, 0 },
"right pane should start at line 1"
)
end
)