341 lines
11 KiB
Lua
341 lines
11 KiB
Lua
local t = require("test")
|
|
local helpers = require("test.git.helpers")
|
|
local cmd = require("git.cmd")
|
|
|
|
require("git").init()
|
|
|
|
local git = helpers.git
|
|
|
|
---@param files table<string, string>?
|
|
---@return string dir
|
|
local function make_repo(files)
|
|
return helpers.make_repo(files, { cd = true })
|
|
end
|
|
|
|
---@param actual string[]
|
|
---@param expected string[]
|
|
local function eq_sorted(actual, expected, msg)
|
|
table.sort(actual)
|
|
table.sort(expected)
|
|
t.eq(actual, expected, msg)
|
|
end
|
|
|
|
t.test("parse_args splits on whitespace", function()
|
|
t.eq(
|
|
cmd.parse_args("config user.name value"),
|
|
{ "config", "user.name", "value" }
|
|
)
|
|
end)
|
|
|
|
t.test("parse_args preserves spaces inside double quotes", function()
|
|
t.eq(
|
|
cmd.parse_args([[config user.name "Oscar Wallberg"]]),
|
|
{ "config", "user.name", "Oscar Wallberg" }
|
|
)
|
|
end)
|
|
|
|
t.test("parse_args preserves spaces inside single quotes", function()
|
|
t.eq(
|
|
cmd.parse_args([[log --grep='bug fix' --author=Oscar]]),
|
|
{ "log", "--grep=bug fix", "--author=Oscar" }
|
|
)
|
|
end)
|
|
|
|
t.test("parse_args handles backslash-escaped space", function()
|
|
t.eq(cmd.parse_args([[a\ b c]]), { "a b", "c" })
|
|
end)
|
|
|
|
t.test("parse_args handles escaped quote inside double quotes", function()
|
|
t.eq(cmd.parse_args([["a\"b" c]]), { 'a"b', "c" })
|
|
end)
|
|
|
|
t.test(
|
|
"parse_args treats backslash literally inside single quotes",
|
|
function()
|
|
t.eq(cmd.parse_args([['a\b' c]]), { "a\\b", "c" })
|
|
end
|
|
)
|
|
|
|
t.test("parse_args concatenates adjacent quoted segments", function()
|
|
t.eq(cmd.parse_args([[foo"bar"baz]]), { "foobarbaz" })
|
|
end)
|
|
|
|
t.test("parse_args handles tabs as separators", function()
|
|
t.eq(cmd.parse_args("a\tb\tc"), { "a", "b", "c" })
|
|
end)
|
|
|
|
t.test("parse_args returns empty list for empty or whitespace input", function()
|
|
t.eq(cmd.parse_args(""), {})
|
|
t.eq(cmd.parse_args(" \t "), {})
|
|
end)
|
|
|
|
t.test("parse_args preserves empty quoted token", function()
|
|
t.eq(cmd.parse_args([[a "" b]]), { "a", "", "b" })
|
|
end)
|
|
|
|
t.test("parse_args expands %% on unquoted token", function()
|
|
local buf = vim.api.nvim_create_buf(false, false)
|
|
t.defer(function()
|
|
pcall(vim.api.nvim_buf_delete, buf, { force = true })
|
|
end)
|
|
vim.api.nvim_buf_set_name(buf, vim.fn.getcwd() .. "/some-file.lua")
|
|
vim.api.nvim_set_current_buf(buf)
|
|
|
|
t.eq(
|
|
cmd.parse_args("add %"),
|
|
{ "add", vim.fn.getcwd() .. "/some-file.lua" }
|
|
)
|
|
end)
|
|
|
|
t.test("parse_args does not expand %% inside double quotes", function()
|
|
local buf = vim.api.nvim_create_buf(false, false)
|
|
t.defer(function()
|
|
pcall(vim.api.nvim_buf_delete, buf, { force = true })
|
|
end)
|
|
vim.api.nvim_buf_set_name(buf, vim.fn.getcwd() .. "/some-file.lua")
|
|
vim.api.nvim_set_current_buf(buf)
|
|
|
|
t.eq(cmd.parse_args([[log -- "%"]]), { "log", "--", "%" })
|
|
end)
|
|
|
|
t.test("parse_args does not expand %% inside single quotes", function()
|
|
local buf = vim.api.nvim_create_buf(false, false)
|
|
t.defer(function()
|
|
pcall(vim.api.nvim_buf_delete, buf, { force = true })
|
|
end)
|
|
vim.api.nvim_buf_set_name(buf, vim.fn.getcwd() .. "/some-file.lua")
|
|
vim.api.nvim_set_current_buf(buf)
|
|
|
|
t.eq(cmd.parse_args([[log -- '%']]), { "log", "--", "%" })
|
|
end)
|
|
|
|
t.test("parse_args does not treat mid-token tilde as expansion", function()
|
|
t.eq(cmd.parse_args("checkout HEAD~3"), { "checkout", "HEAD~3" })
|
|
end)
|
|
|
|
t.test("parse_args expands leading ~/ to home", function()
|
|
t.eq(cmd.parse_args("add ~/foo"), { "add", vim.fn.expand("~/foo") })
|
|
end)
|
|
|
|
t.test("parse_complete_state with trailing space", function()
|
|
local s = cmd._parse_complete_state("G push origin ")
|
|
t.eq(s.prior, { "push", "origin" })
|
|
t.falsy(s.after_separator)
|
|
end)
|
|
|
|
t.test("parse_complete_state mid-token", function()
|
|
local s = cmd._parse_complete_state("G push or")
|
|
t.eq(s.prior, { "push" })
|
|
t.falsy(s.after_separator)
|
|
end)
|
|
|
|
t.test("parse_complete_state empty after command", function()
|
|
local s = cmd._parse_complete_state("G ")
|
|
t.eq(s.prior, {})
|
|
t.falsy(s.after_separator)
|
|
end)
|
|
|
|
t.test("parse_complete_state detects -- separator", function()
|
|
local s = cmd._parse_complete_state("G log -- foo")
|
|
t.eq(s.prior, { "log", "--" })
|
|
t.truthy(s.after_separator)
|
|
end)
|
|
|
|
t.test("positional_index ignores flags", function()
|
|
t.eq(cmd._positional_index({ "push" }), 1)
|
|
t.eq(cmd._positional_index({ "push", "origin" }), 2)
|
|
t.eq(cmd._positional_index({ "push", "--force" }), 1)
|
|
t.eq(cmd._positional_index({ "push", "--force", "origin" }), 2)
|
|
t.eq(cmd._positional_index({ "checkout", "-b", "feature" }), 2)
|
|
end)
|
|
|
|
t.test("complete returns subcommands at first position", function()
|
|
local matches = cmd.complete("ch", "G ch", 4)
|
|
t.truthy(vim.tbl_contains(matches, "checkout"))
|
|
t.truthy(vim.tbl_contains(matches, "cherry-pick"))
|
|
end)
|
|
|
|
t.test("complete returns flags when arg starts with -", function()
|
|
local matches = cmd.complete("--am", "G commit --am", 13)
|
|
t.eq(matches, { "--amend" })
|
|
end)
|
|
|
|
t.test("complete branch returns plain refs (no pseudo, no stash)", function()
|
|
local dir = make_repo({ a = "x" })
|
|
git(dir, "branch", "feature")
|
|
git(dir, "tag", "v1")
|
|
t.write(dir, "a", "modified")
|
|
git(dir, "stash")
|
|
local matches = cmd.complete("", "G branch ", 9)
|
|
eq_sorted(matches, { "feature", "main", "v1" })
|
|
end)
|
|
|
|
t.test("complete merge returns refs + pseudo + stash", function()
|
|
local dir = make_repo({ a = "x" })
|
|
git(dir, "branch", "feature")
|
|
t.write(dir, "a", "y")
|
|
git(dir, "stash")
|
|
local matches = cmd.complete("", "G merge ", 8)
|
|
eq_sorted(
|
|
matches,
|
|
{ "HEAD", "ORIG_HEAD", "feature", "main", "stash", "stash@{0}" }
|
|
)
|
|
end)
|
|
|
|
t.test("complete push first positional returns remotes", function()
|
|
local dir = make_repo({ a = "x" })
|
|
git(dir, "remote", "add", "origin", "/tmp/nope")
|
|
git(dir, "remote", "add", "upstream", "/tmp/nope")
|
|
local matches = cmd.complete("", "G push ", 7)
|
|
eq_sorted(matches, { "origin", "upstream" })
|
|
end)
|
|
|
|
t.test("complete push second positional returns refs", function()
|
|
local dir = make_repo({ a = "x" })
|
|
git(dir, "branch", "feature")
|
|
local matches = cmd.complete("", "G push origin ", 14)
|
|
eq_sorted(matches, { "HEAD", "feature", "main" })
|
|
end)
|
|
|
|
t.test("complete add returns only unstaged/untracked paths", function()
|
|
local dir = make_repo({ tracked = "x" })
|
|
t.write(dir, "tracked", "modified")
|
|
t.write(dir, "newfile", "new")
|
|
local r = assert(require("git.repo").resolve(dir))
|
|
r:refresh()
|
|
vim.wait(500, function()
|
|
return r.status and #vim.tbl_keys(r.status.entries) > 0
|
|
end)
|
|
local matches = cmd.complete("", "G add ", 6)
|
|
eq_sorted(matches, { "newfile", "tracked" })
|
|
end)
|
|
|
|
t.test("complete after `--` returns tracked paths only", function()
|
|
local dir = make_repo({ a = "x", b = "y" })
|
|
t.write(dir, "untracked", "z")
|
|
local matches = cmd.complete("", "G log -- ", 9)
|
|
eq_sorted(matches, { "a", "b" })
|
|
end)
|
|
|
|
t.test("complete stash returns subsubcommands", function()
|
|
local dir = make_repo({ a = "x" })
|
|
local matches = cmd.complete("p", "G stash p", 9)
|
|
eq_sorted(matches, { "pop", "push" })
|
|
end)
|
|
|
|
t.test("complete show with <rev>:<path> returns tree paths", function()
|
|
local dir = make_repo({ a = "x", ["sub/b"] = "y" })
|
|
local matches = cmd.complete("HEAD:", "G show HEAD:", 12)
|
|
eq_sorted(matches, { "HEAD:a", "HEAD:sub/" })
|
|
end)
|
|
|
|
t.test("complete unknown subcommand falls back to tracked paths", function()
|
|
local dir = make_repo({ a = "x", b = "y" })
|
|
local matches = cmd.complete("", "G nonexistent ", 14)
|
|
eq_sorted(matches, { "a", "b" })
|
|
end)
|
|
|
|
---@param name_pattern string
|
|
---@return integer count
|
|
local function count_bufs_named(name_pattern)
|
|
local n = 0
|
|
for _, b in ipairs(vim.api.nvim_list_bufs()) do
|
|
if vim.api.nvim_buf_get_name(b):match(name_pattern) then
|
|
n = n + 1
|
|
end
|
|
end
|
|
return n
|
|
end
|
|
|
|
---@param buf_name_pattern string
|
|
---@param timeout integer?
|
|
local function wait_buf_populated(buf_name_pattern, timeout)
|
|
vim.wait(timeout or 1000, function()
|
|
for _, b in ipairs(vim.api.nvim_list_bufs()) do
|
|
if vim.api.nvim_buf_get_name(b):match(buf_name_pattern) then
|
|
return #vim.api.nvim_buf_get_lines(b, 0, -1, false) > 1
|
|
end
|
|
end
|
|
return false
|
|
end)
|
|
end
|
|
|
|
t.test("run :G diff reuses the same buffer across invocations", function()
|
|
local dir = make_repo({ a = "v1\n" })
|
|
t.write(dir, "a", "v2\n")
|
|
|
|
cmd.run({ "diff" })
|
|
wait_buf_populated("%[Git diff%]")
|
|
local first_count = count_bufs_named("%[Git diff%]")
|
|
t.eq(first_count, 1)
|
|
|
|
t.write(dir, "a", "v3\n")
|
|
cmd.run({ "diff" })
|
|
vim.wait(300)
|
|
t.eq(count_bufs_named("%[Git diff%]"), 1, "second :G diff should reuse")
|
|
|
|
cmd.run({ "diff" })
|
|
vim.wait(300)
|
|
t.eq(count_bufs_named("%[Git diff%]"), 1, "third :G diff should reuse")
|
|
end)
|
|
|
|
---@param buf integer
|
|
---@param prefix string
|
|
---@return integer? lnum
|
|
local function find_line(buf, prefix)
|
|
for i, l in ipairs(vim.api.nvim_buf_get_lines(buf, 0, -1, false)) do
|
|
if l:sub(1, #prefix) == prefix then
|
|
return i
|
|
end
|
|
end
|
|
end
|
|
|
|
t.test(":G show <CR> on + line opens the blob URI", function()
|
|
local dir = make_repo({ a = "first\n" })
|
|
t.write(dir, "a", "second\n")
|
|
git(dir, "add", "a")
|
|
git(dir, "commit", "-q", "-m", "second")
|
|
local r = assert(require("git.repo").resolve(dir))
|
|
local blob = vim.trim(git(dir, "rev-parse", "HEAD:a").stdout)
|
|
|
|
cmd.run({ "show", "HEAD" })
|
|
wait_buf_populated("%[Git show HEAD%]")
|
|
local diff_buf
|
|
for _, b in ipairs(vim.api.nvim_list_bufs()) do
|
|
if vim.api.nvim_buf_get_name(b):match("%[Git show HEAD%]") then
|
|
diff_buf = b
|
|
end
|
|
end
|
|
assert(diff_buf, "expected [Git show HEAD] buffer")
|
|
local win = vim.fn.bufwinid(diff_buf)
|
|
vim.api.nvim_set_current_win(win)
|
|
local lnum = assert(find_line(diff_buf, "+second"))
|
|
vim.api.nvim_win_set_cursor(win, { lnum, 0 })
|
|
|
|
t.truthy(require("git.object").open_under_cursor())
|
|
t.eq(vim.api.nvim_buf_get_name(0), "git://" .. blob)
|
|
end)
|
|
|
|
t.test(":G diff <CR> on + line falls back to worktree file", function()
|
|
local dir = make_repo({ a = "v1\n" })
|
|
t.write(dir, "a", "v2\n")
|
|
local r = assert(require("git.repo").resolve(dir))
|
|
|
|
cmd.run({ "diff" })
|
|
wait_buf_populated("%[Git diff%]")
|
|
local diff_buf
|
|
for _, b in ipairs(vim.api.nvim_list_bufs()) do
|
|
if vim.api.nvim_buf_get_name(b):match("%[Git diff%]") then
|
|
diff_buf = b
|
|
end
|
|
end
|
|
assert(diff_buf, "expected [Git diff] buffer")
|
|
local win = vim.fn.bufwinid(diff_buf)
|
|
vim.api.nvim_set_current_win(win)
|
|
local lnum = assert(find_line(diff_buf, "+v2"))
|
|
vim.api.nvim_win_set_cursor(win, { lnum, 0 })
|
|
|
|
t.truthy(require("git.object").open_under_cursor())
|
|
t.eq(vim.api.nvim_buf_get_name(0), vim.fs.joinpath(dir, "a"))
|
|
end)
|