local t = require("test") require("git").init() ---@param dir string ---@param ... string local function git(dir, ...) local r = vim.system({ "git", ... }, { cwd = dir, text = true }):wait() if r.code ~= 0 then error( string.format( "git %s failed: %s", table.concat({ ... }, " "), vim.trim(r.stderr or "") ), 2 ) end return r end ---Build a temporary git repo with the given committed contents and ---queue cleanup (stop fs watchers, drop test buffers, delete the dir). ---@param files table? ---@return string dir local function make_repo(files) local dir = vim.fn.tempname() vim.fn.mkdir(dir, "p") git(dir, "init", "-q", "-b", "main") git(dir, "config", "user.email", "t@t.com") git(dir, "config", "user.name", "t") if files and next(files) then for path, content in pairs(files) do t.write(dir, path, content) end git(dir, "add", ".") git(dir, "commit", "-q", "-m", "init") end t.defer(function() pcall(vim.cmd.cd, "/tmp") pcall(function() require("git.repo").stop_all() end) vim.wait(60) for _, b in ipairs(vim.api.nvim_list_bufs()) do local name = vim.api.nvim_buf_get_name(b) if name:find(dir, 1, true) or name:match("^git[a-z]*://") then pcall(vim.api.nvim_buf_delete, b, { force = true }) end end vim.fn.delete(dir, "rf") end) return dir end ---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 ---@return integer? sidebar_buf, integer? sidebar_win local function find_sidebar() local buf, win for _, b in ipairs(vim.api.nvim_list_bufs()) do if vim.bo[b].filetype == "gitstatus" then buf = b end end for _, w in ipairs(vim.api.nvim_list_wins()) do if vim.api.nvim_win_get_buf(w) == buf then win = w end end return buf, win end ---@param role string ---@return integer? local function find_diff_win(role) for _, w in ipairs(vim.api.nvim_list_wins()) do if vim.w[w].git_diff_role == role then return w end end 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 = 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.repo").find(vim.fn.getcwd()), "repo should resolve for the test worktree" ) r:refresh() vim.wait(1000, function() return r.status and #r.status:by_kind("unstaged") > 0 end) local entry_line = assert( find_line(sidebar_buf, vim.pesc(file_path) .. "$"), file_path .. " should appear in sidebar" ) return sidebar_win, entry_line end local function press(keys) local rhs = vim.api.nvim_replace_termcodes(keys, true, false, true) vim.api.nvim_feedkeys(rhs, "x", false) end ---@param cond fun(): boolean ---@param msg string local function wait_for(cond, msg) t.truthy(vim.wait(1000, cond), "timed out waiting for: " .. msg) 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 }) press("") wait_for(function() return find_diff_win("left") ~= nil end, "diff windows to appear") local r = assert(require("git.repo").find(vim.fn.getcwd())) vim.api.nvim_set_current_win(sidebar_win) press("s") wait_for(function() return #r.status:by_kind("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 }) press("") 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.repo").find(vim.fn.getcwd())) vim.api.nvim_set_current_win(sidebar_win) press("s") wait_for(function() return #r.status:by_kind("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("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 }) press("") 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) press("s") 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)