fix(git/cmd): parse :G args with shell-style quoting
This commit is contained in:
+1
-1
@@ -248,7 +248,7 @@ vim.keymap.set("n", "<leader>gc", function()
|
|||||||
require("git.commit").commit()
|
require("git.commit").commit()
|
||||||
end)
|
end)
|
||||||
vim.keymap.set("n", "<leader>ga", function()
|
vim.keymap.set("n", "<leader>ga", function()
|
||||||
require("git.commit").commit({ amend = true })
|
require("git.commit").commit({ args = { "--amend" } })
|
||||||
end)
|
end)
|
||||||
vim.keymap.set("n", "<leader>gl", function()
|
vim.keymap.set("n", "<leader>gl", function()
|
||||||
require('git.log_view').open({ max_count = 1000 })
|
require('git.log_view').open({ max_count = 1000 })
|
||||||
|
|||||||
+94
-21
@@ -42,6 +42,95 @@ local function git_cmds()
|
|||||||
return cached_cmds
|
return cached_cmds
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@param tok string
|
||||||
|
---@return boolean
|
||||||
|
local function is_expansion_target(tok)
|
||||||
|
local first = tok:sub(1, 1)
|
||||||
|
if first == "%" or first == "#" then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
if tok:match("^<%w+>") then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
if tok == "~" or tok:sub(1, 2) == "~/" then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param line string
|
||||||
|
---@param i integer
|
||||||
|
---@param buf string[]
|
||||||
|
---@param escapes string?
|
||||||
|
---@return integer
|
||||||
|
local function parse_quoted(line, i, buf, escapes)
|
||||||
|
local quote = line:sub(i, i)
|
||||||
|
local n = #line
|
||||||
|
i = i + 1
|
||||||
|
while i <= n do
|
||||||
|
local c = line:sub(i, i)
|
||||||
|
if c == quote then
|
||||||
|
return i + 1
|
||||||
|
elseif escapes and c == "\\" and i < n then
|
||||||
|
local nxt = line:sub(i + 1, i + 1)
|
||||||
|
if escapes:find(nxt, 1, true) then
|
||||||
|
table.insert(buf, nxt)
|
||||||
|
i = i + 2
|
||||||
|
else
|
||||||
|
table.insert(buf, c)
|
||||||
|
i = i + 1
|
||||||
|
end
|
||||||
|
else
|
||||||
|
table.insert(buf, c)
|
||||||
|
i = i + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return i
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param line string
|
||||||
|
---@return string[]
|
||||||
|
function M.parse_args(line)
|
||||||
|
local args = {}
|
||||||
|
local i, n = 1, #line
|
||||||
|
while i <= n do
|
||||||
|
local c = line:sub(i, i)
|
||||||
|
if c == " " or c == "\t" then
|
||||||
|
i = i + 1
|
||||||
|
else
|
||||||
|
local buf = {}
|
||||||
|
local quoted = false
|
||||||
|
while i <= n do
|
||||||
|
c = line:sub(i, i)
|
||||||
|
if c == " " or c == "\t" then
|
||||||
|
break
|
||||||
|
elseif c == "\\" and i < n then
|
||||||
|
table.insert(buf, line:sub(i + 1, i + 1))
|
||||||
|
i = i + 2
|
||||||
|
elseif c == '"' then
|
||||||
|
quoted = true
|
||||||
|
i = parse_quoted(line, i, buf, '"\\$`')
|
||||||
|
elseif c == "'" then
|
||||||
|
quoted = true
|
||||||
|
i = parse_quoted(line, i, buf, nil)
|
||||||
|
else
|
||||||
|
table.insert(buf, c)
|
||||||
|
i = i + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local tok = table.concat(buf)
|
||||||
|
if not quoted and is_expansion_target(tok) then
|
||||||
|
local expanded = vim.fn.expand(tok) --[[@as string]]
|
||||||
|
if expanded ~= "" then
|
||||||
|
tok = expanded
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.insert(args, tok)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return args
|
||||||
|
end
|
||||||
|
|
||||||
---@param args string[]
|
---@param args string[]
|
||||||
---@param start integer
|
---@param start integer
|
||||||
---@return string?
|
---@return string?
|
||||||
@@ -139,18 +228,6 @@ local function run_to_messages(r, args)
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param args string[]
|
|
||||||
---@param flag string
|
|
||||||
---@return boolean
|
|
||||||
local function has_flag(args, flag)
|
|
||||||
for _, a in ipairs(args) do
|
|
||||||
if a == flag then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param args string[]
|
---@param args string[]
|
||||||
---@return boolean
|
---@return boolean
|
||||||
local function has_message(args)
|
local function has_message(args)
|
||||||
@@ -177,14 +254,13 @@ function M.run(args)
|
|||||||
|
|
||||||
local sub = args[1]
|
local sub = args[1]
|
||||||
if sub == "commit" and not has_message(args) then
|
if sub == "commit" and not has_message(args) then
|
||||||
commit.commit({ amend = has_flag(args, "--amend") })
|
commit.commit({ args = vim.list_slice(args, 2) })
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if sub == "show" then
|
if sub == "show" then
|
||||||
local arg = first_positional(args, 2)
|
if #args == 2 and args[2]:find(":", 1, true) then
|
||||||
if arg and arg:find(":", 1, true) then
|
object.open(r, args[2])
|
||||||
object.open(r, arg)
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
run_in_split(r, args, { ft = "git", needs_rev = true })
|
run_in_split(r, args, { ft = "git", needs_rev = true })
|
||||||
@@ -192,13 +268,10 @@ function M.run(args)
|
|||||||
end
|
end
|
||||||
|
|
||||||
if sub == "cat-file" then
|
if sub == "cat-file" then
|
||||||
if vim.list_contains(args, "-p") then
|
if #args == 3 and args[2] == "-p" then
|
||||||
local rev = first_positional(args, 2)
|
object.open(r, args[3])
|
||||||
if rev then
|
|
||||||
object.open(r, rev)
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
end
|
|
||||||
run_in_split(r, args, { ft = "git", needs_rev = true })
|
run_in_split(r, args, { ft = "git", needs_rev = true })
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|||||||
+3
-4
@@ -4,9 +4,8 @@ local util = require("git.util")
|
|||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
---@param opts { amend: boolean? }?
|
---@param opts { args: string[]? }?
|
||||||
function M.commit(opts)
|
function M.commit(opts)
|
||||||
local amend = opts and opts.amend or false
|
|
||||||
local r = repo.resolve()
|
local r = repo.resolve()
|
||||||
if not r then
|
if not r then
|
||||||
util.error("not in a git repository")
|
util.error("not in a git repository")
|
||||||
@@ -14,8 +13,8 @@ function M.commit(opts)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local cmd = { "git", "commit" }
|
local cmd = { "git", "commit" }
|
||||||
if amend then
|
if opts and opts.args then
|
||||||
table.insert(cmd, "--amend")
|
vim.list_extend(cmd, opts.args)
|
||||||
end
|
end
|
||||||
|
|
||||||
local proxy_buf, proxy_win
|
local proxy_buf, proxy_win
|
||||||
|
|||||||
+2
-1
@@ -137,7 +137,8 @@ function M.init()
|
|||||||
})
|
})
|
||||||
|
|
||||||
vim.api.nvim_create_user_command("G", function(opts)
|
vim.api.nvim_create_user_command("G", function(opts)
|
||||||
require("git.cmd").run(opts.fargs)
|
local cmd = require("git.cmd")
|
||||||
|
cmd.run(cmd.parse_args(opts.args))
|
||||||
end, {
|
end, {
|
||||||
nargs = "*",
|
nargs = "*",
|
||||||
complete = function(...)
|
complete = function(...)
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
local t = require("test")
|
||||||
|
local cmd = require("git.cmd")
|
||||||
|
|
||||||
|
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)
|
||||||
Reference in New Issue
Block a user