feat(git): replace vim-fugitive with custom git module
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
-- The built-in `git` filetype is reused for our `:G show` / `:G cat-file -p`
|
||||
-- output buffers and for commits opened from the log window. We set
|
||||
-- `vim.b.git_worktree` before assigning the filetype on those buffers; the
|
||||
-- guard below keeps the <CR> dispatcher off any unrelated git buffer (a
|
||||
-- real `.git/HEAD` file, etc.) so the default normal-mode <CR> still works.
|
||||
if not vim.b.git_worktree then
|
||||
return
|
||||
end
|
||||
|
||||
local cr = vim.api.nvim_replace_termcodes("<CR>", true, false, true)
|
||||
|
||||
vim.keymap.set("n", "<CR>", function()
|
||||
if not require("git.show").open_at_cursor() then
|
||||
-- "n" mode = no remap, so this doesn't recurse into our mapping.
|
||||
vim.api.nvim_feedkeys(cr, "n", false)
|
||||
end
|
||||
end, { buffer = 0, silent = true, desc = "Open file at commit" })
|
||||
@@ -0,0 +1,13 @@
|
||||
local cr = vim.api.nvim_replace_termcodes("<CR>", true, false, true)
|
||||
|
||||
vim.keymap.set("n", "<CR>", function()
|
||||
local worktree = vim.b.git_worktree
|
||||
local sha = worktree
|
||||
and vim.api.nvim_get_current_line():match("(%x%x%x%x%x%x%x+)")
|
||||
if sha then
|
||||
require("git.show").open_commit(worktree, sha)
|
||||
else
|
||||
-- "n" mode = no remap, so this doesn't recurse into our mapping.
|
||||
vim.api.nvim_feedkeys(cr, "n", false)
|
||||
end
|
||||
end, { buffer = 0, silent = true, desc = "Open commit" })
|
||||
@@ -30,7 +30,6 @@ require("pack").setup({
|
||||
"https://github.com/owallb/mason-auto-install.nvim",
|
||||
"https://github.com/mfussenegger/nvim-dap",
|
||||
"https://github.com/numToStr/Comment.nvim",
|
||||
"https://github.com/tpope/vim-fugitive",
|
||||
"https://github.com/lewis6991/gitsigns.nvim",
|
||||
"https://github.com/MagicDuck/grug-far.nvim",
|
||||
"https://github.com/nvim-tree/nvim-tree.lua",
|
||||
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
local log = require("log")
|
||||
local repo = require("git.repo")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class ow.Git.SplitHandler
|
||||
---@field ft string
|
||||
---@field needs_ref boolean?
|
||||
|
||||
---@type table<string, ow.Git.SplitHandler>
|
||||
local SPLIT_HANDLERS = {
|
||||
log = { ft = "gitlog" },
|
||||
show = { ft = "git", needs_ref = true },
|
||||
["cat-file"] = { ft = "git", needs_ref = true },
|
||||
diff = { ft = "diff" },
|
||||
}
|
||||
|
||||
---@type string[]?
|
||||
local cached_cmds
|
||||
|
||||
---@return string[]
|
||||
local function git_cmds()
|
||||
if cached_cmds then
|
||||
return cached_cmds
|
||||
end
|
||||
local result = vim.system(
|
||||
{ "git", "--list-cmds=main,others,alias" },
|
||||
{ text = true }
|
||||
)
|
||||
:wait()
|
||||
cached_cmds = {}
|
||||
if result.code == 0 then
|
||||
for line in (result.stdout or ""):gmatch("[^\r\n]+") do
|
||||
if line ~= "" then
|
||||
table.insert(cached_cmds, line)
|
||||
end
|
||||
end
|
||||
table.sort(cached_cmds)
|
||||
end
|
||||
return cached_cmds
|
||||
end
|
||||
|
||||
---@param content string
|
||||
---@return string[]
|
||||
local function split_lines(content)
|
||||
local lines = vim.split(content, "\n", { plain = true, trimempty = false })
|
||||
if #lines > 0 and lines[#lines] == "" then
|
||||
table.remove(lines)
|
||||
end
|
||||
return lines
|
||||
end
|
||||
|
||||
---@param args string[]
|
||||
---@param start integer
|
||||
---@return string?
|
||||
local function first_positional(args, start)
|
||||
for i = start, #args do
|
||||
local a = args[i]
|
||||
if a:sub(1, 1) ~= "-" then
|
||||
return a
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param worktree string
|
||||
---@param args string[]
|
||||
---@param conf ow.Git.SplitHandler
|
||||
local function run_in_split(worktree, args, conf)
|
||||
vim.cmd("new")
|
||||
local buf = vim.api.nvim_get_current_buf()
|
||||
vim.bo[buf].buftype = "nofile"
|
||||
vim.bo[buf].bufhidden = "hide"
|
||||
vim.bo[buf].swapfile = false
|
||||
vim.bo[buf].modifiable = false
|
||||
vim.b[buf].git_worktree = worktree
|
||||
if conf.needs_ref then
|
||||
local user_ref = first_positional(args, 2) or "HEAD"
|
||||
local sha = repo.rev_parse(worktree, user_ref, true) or user_ref
|
||||
vim.b[buf].git_ref = sha
|
||||
vim.b[buf].git_parent_ref =
|
||||
repo.rev_parse(worktree, user_ref .. "^", true)
|
||||
pcall(vim.api.nvim_buf_set_name, buf, "git://" .. sha .. "/")
|
||||
end
|
||||
vim.bo[buf].filetype = conf.ft
|
||||
|
||||
local cmd = { "git" }
|
||||
vim.list_extend(cmd, args)
|
||||
vim.system(cmd, { cwd = worktree, text = true }, function(obj)
|
||||
vim.schedule(function()
|
||||
if not vim.api.nvim_buf_is_valid(buf) then
|
||||
return
|
||||
end
|
||||
local content = (obj.stdout or "") .. (obj.stderr or "")
|
||||
vim.bo[buf].modifiable = true
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, split_lines(content))
|
||||
vim.bo[buf].modifiable = false
|
||||
vim.bo[buf].modified = false
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
---@param worktree string
|
||||
---@param args string[]
|
||||
local function run_to_messages(worktree, args)
|
||||
local cmd = { "git" }
|
||||
vim.list_extend(cmd, args)
|
||||
vim.system(cmd, { cwd = worktree, text = true }, function(obj)
|
||||
vim.schedule(function()
|
||||
local out = vim.trim(obj.stdout or "")
|
||||
local err = vim.trim(obj.stderr or "")
|
||||
local chunks = {}
|
||||
if out ~= "" then
|
||||
table.insert(chunks, { out })
|
||||
end
|
||||
if err ~= "" then
|
||||
if #chunks > 0 then
|
||||
table.insert(chunks, { "\n" })
|
||||
end
|
||||
table.insert(chunks, { err, "ErrorMsg" })
|
||||
end
|
||||
if #chunks == 0 and obj.code ~= 0 then
|
||||
table.insert(
|
||||
chunks,
|
||||
{ "git exited " .. tostring(obj.code), "ErrorMsg" }
|
||||
)
|
||||
end
|
||||
if #chunks > 0 then
|
||||
vim.api.nvim_echo(chunks, true, {})
|
||||
end
|
||||
end)
|
||||
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[]
|
||||
---@return boolean
|
||||
local function has_message(args)
|
||||
for _, a in ipairs(args) do
|
||||
if
|
||||
a == "-m"
|
||||
or a == "--message"
|
||||
or a:match("^%-%-message=")
|
||||
or a:match("^%-m")
|
||||
then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
---@param args string[]
|
||||
function M.run(args)
|
||||
local path = vim.api.nvim_buf_get_name(0)
|
||||
if path == "" then
|
||||
path = vim.fn.getcwd()
|
||||
end
|
||||
local _, worktree = repo.resolve(path)
|
||||
if not worktree then
|
||||
log.warning("not in a git repository")
|
||||
return
|
||||
end
|
||||
|
||||
local sub = args[1]
|
||||
if sub == "commit" and not has_message(args) then
|
||||
require("git.commit").commit({ amend = has_flag(args, "--amend") })
|
||||
return
|
||||
end
|
||||
|
||||
local conf = sub and SPLIT_HANDLERS[sub]
|
||||
if conf then
|
||||
run_in_split(worktree, args, conf)
|
||||
else
|
||||
run_to_messages(worktree, args)
|
||||
end
|
||||
end
|
||||
|
||||
---@param arg_lead string
|
||||
---@param cmd_line string
|
||||
---@return string[]
|
||||
function M.complete(arg_lead, cmd_line, _)
|
||||
local rest = cmd_line:gsub("^%s*%S+%s*", "", 1)
|
||||
local words = vim.split(rest, "%s+", { trimempty = false })
|
||||
if #words > 1 then
|
||||
return {}
|
||||
end
|
||||
local matches = {}
|
||||
for _, c in ipairs(git_cmds()) do
|
||||
if c:sub(1, #arg_lead) == arg_lead then
|
||||
table.insert(matches, c)
|
||||
end
|
||||
end
|
||||
return matches
|
||||
end
|
||||
|
||||
function M.setup()
|
||||
vim.api.nvim_create_user_command("G", function(opts)
|
||||
M.run(opts.fargs)
|
||||
end, {
|
||||
nargs = "*",
|
||||
complete = M.complete,
|
||||
desc = "Run git",
|
||||
})
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -0,0 +1,78 @@
|
||||
local log = require("log")
|
||||
local repo = require("git.repo")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@param opts { amend: boolean? }?
|
||||
function M.commit(opts)
|
||||
local amend = opts and opts.amend or false
|
||||
local path = vim.api.nvim_buf_get_name(0)
|
||||
if path == "" then
|
||||
path = vim.fn.getcwd()
|
||||
end
|
||||
local gitdir, worktree = repo.resolve(path)
|
||||
if not gitdir or not worktree then
|
||||
log.warning("not in a git repository")
|
||||
return
|
||||
end
|
||||
local msg_path = vim.fs.joinpath(gitdir, "COMMIT_EDITMSG")
|
||||
|
||||
local initial = ""
|
||||
if amend then
|
||||
local result = vim.system(
|
||||
{ "git", "log", "-1", "--pretty=%B" },
|
||||
{ cwd = worktree, text = true }
|
||||
):wait()
|
||||
if result.code == 0 then
|
||||
initial = (result.stdout or ""):gsub("\n+$", "")
|
||||
end
|
||||
end
|
||||
|
||||
local f, err = io.open(msg_path, "w")
|
||||
if not f then
|
||||
log.error("failed to open %s: %s", msg_path, err or "")
|
||||
return
|
||||
end
|
||||
f:write(initial)
|
||||
f:close()
|
||||
|
||||
vim.cmd("edit " .. vim.fn.fnameescape(msg_path))
|
||||
local buf = vim.api.nvim_get_current_buf()
|
||||
vim.bo[buf].filetype = "gitcommit"
|
||||
|
||||
vim.api.nvim_create_autocmd("BufWriteCmd", {
|
||||
buffer = buf,
|
||||
callback = function()
|
||||
local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
|
||||
local fw, werr = io.open(msg_path, "w")
|
||||
if not fw then
|
||||
log.error("failed to write %s: %s", msg_path, werr or "")
|
||||
return
|
||||
end
|
||||
fw:write(table.concat(lines, "\n"))
|
||||
fw:close()
|
||||
vim.bo[buf].modified = false
|
||||
|
||||
local cmd = { "git", "commit", "-F", msg_path }
|
||||
if amend then
|
||||
table.insert(cmd, "--amend")
|
||||
end
|
||||
local result = vim.system(cmd, { cwd = worktree, text = true })
|
||||
:wait()
|
||||
if result.code ~= 0 then
|
||||
log.error(
|
||||
"git commit failed: %s",
|
||||
vim.trim(result.stderr or "")
|
||||
)
|
||||
return
|
||||
end
|
||||
local out = vim.trim(result.stdout or "")
|
||||
if out ~= "" then
|
||||
log.info("%s", out)
|
||||
end
|
||||
vim.api.nvim_buf_delete(buf, { force = true })
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -0,0 +1,179 @@
|
||||
local log = require("log")
|
||||
local repo = require("git.repo")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@param buf integer
|
||||
---@param worktree string
|
||||
---@param path string
|
||||
local function attach_index_writer(buf, worktree, path)
|
||||
vim.api.nvim_create_autocmd("BufWriteCmd", {
|
||||
buffer = buf,
|
||||
callback = function()
|
||||
local body = table.concat(
|
||||
vim.api.nvim_buf_get_lines(buf, 0, -1, false),
|
||||
"\n"
|
||||
) .. "\n"
|
||||
local hash = vim.system(
|
||||
{ "git", "hash-object", "-w", "--stdin" },
|
||||
{ cwd = worktree, stdin = body, text = true }
|
||||
):wait()
|
||||
if hash.code ~= 0 then
|
||||
log.error("git hash-object failed: %s", hash.stderr or "")
|
||||
return
|
||||
end
|
||||
local sha = vim.trim(hash.stdout or "")
|
||||
local mode = "100644"
|
||||
local ls = vim.system(
|
||||
{ "git", "ls-files", "-s", "--", path },
|
||||
{ cwd = worktree, text = true }
|
||||
):wait()
|
||||
if ls.code == 0 and ls.stdout then
|
||||
local m = ls.stdout:match("^(%d+)")
|
||||
if m then
|
||||
mode = m
|
||||
end
|
||||
end
|
||||
local upd = vim.system({
|
||||
"git",
|
||||
"update-index",
|
||||
"--cacheinfo",
|
||||
mode .. "," .. sha .. "," .. path,
|
||||
}, { cwd = worktree, text = true }):wait()
|
||||
if upd.code ~= 0 then
|
||||
log.error("git update-index failed: %s", upd.stderr or "")
|
||||
return
|
||||
end
|
||||
vim.bo[buf].modified = false
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
---@param worktree string
|
||||
---@param revspec string anything `git show` accepts (e.g. `HEAD:foo`, `:foo`, blob SHA)
|
||||
---@return string[]
|
||||
local function read_show(worktree, revspec)
|
||||
local result = vim.system(
|
||||
{ "git", "show", revspec },
|
||||
{ cwd = worktree, text = true }
|
||||
)
|
||||
:wait()
|
||||
local content = result.code == 0 and (result.stdout or "") or ""
|
||||
local lines = vim.split(content, "\n", { plain = true, trimempty = false })
|
||||
if #lines > 0 and lines[#lines] == "" then
|
||||
table.remove(lines)
|
||||
end
|
||||
return lines
|
||||
end
|
||||
|
||||
---@param worktree string
|
||||
---@param ref string '' for index, 'HEAD' or a sha for committed refs
|
||||
---@param path string
|
||||
---@param is_index boolean? true to hook :w to update the git index
|
||||
---@return integer
|
||||
function M.git_show_buf(worktree, ref, path, is_index)
|
||||
local lines = read_show(worktree, ref .. ":" .. path)
|
||||
local buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
|
||||
vim.bo[buf].buftype = is_index and "acwrite" or "nofile"
|
||||
vim.bo[buf].bufhidden = "wipe"
|
||||
vim.bo[buf].swapfile = false
|
||||
if not is_index then
|
||||
vim.bo[buf].modifiable = false
|
||||
end
|
||||
if is_index then
|
||||
attach_index_writer(buf, worktree, path)
|
||||
end
|
||||
vim.bo[buf].modified = false
|
||||
return buf
|
||||
end
|
||||
|
||||
---@param worktree string
|
||||
---@param blob string the blob SHA (full or abbreviated)
|
||||
---@return integer
|
||||
function M.git_show_blob(worktree, blob)
|
||||
local lines = read_show(worktree, blob)
|
||||
local buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
|
||||
vim.bo[buf].buftype = "nofile"
|
||||
vim.bo[buf].bufhidden = "wipe"
|
||||
vim.bo[buf].swapfile = false
|
||||
vim.bo[buf].modifiable = false
|
||||
vim.bo[buf].modified = false
|
||||
return buf
|
||||
end
|
||||
|
||||
---@return integer
|
||||
function M.empty_buf()
|
||||
local buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.bo[buf].buftype = "nofile"
|
||||
vim.bo[buf].bufhidden = "wipe"
|
||||
vim.bo[buf].swapfile = false
|
||||
vim.bo[buf].modifiable = false
|
||||
vim.bo[buf].modified = false
|
||||
return buf
|
||||
end
|
||||
|
||||
---@param abs_path string
|
||||
---@return integer
|
||||
function M.load_file_buf(abs_path)
|
||||
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
|
||||
if
|
||||
vim.api.nvim_buf_is_loaded(buf)
|
||||
and vim.api.nvim_buf_get_name(buf) == abs_path
|
||||
then
|
||||
return buf
|
||||
end
|
||||
end
|
||||
local buf = vim.fn.bufadd(abs_path)
|
||||
vim.fn.bufload(buf)
|
||||
return buf
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
---@param name string
|
||||
local function set_buf_name_and_filetype(buf, name)
|
||||
pcall(vim.api.nvim_buf_set_name, buf, name)
|
||||
local ft = vim.filetype.match({ buf = buf })
|
||||
if ft then
|
||||
vim.bo[buf].filetype = ft
|
||||
end
|
||||
end
|
||||
|
||||
---@class ow.Git.SplitOpts
|
||||
---@field ref string '' for index, 'HEAD' for HEAD
|
||||
---@field vertical boolean
|
||||
|
||||
---@param opts ow.Git.SplitOpts
|
||||
function M.split(opts)
|
||||
local cur_buf = vim.api.nvim_get_current_buf()
|
||||
local cur_path = vim.api.nvim_buf_get_name(cur_buf)
|
||||
if cur_path == "" then
|
||||
log.warning("no file in current buffer")
|
||||
return
|
||||
end
|
||||
local _, worktree = repo.resolve(cur_path)
|
||||
if not worktree then
|
||||
log.warning("not in a git repository")
|
||||
return
|
||||
end
|
||||
local rel = vim.fs.relpath(worktree, cur_path)
|
||||
if not rel then
|
||||
log.warning("file is outside the worktree")
|
||||
return
|
||||
end
|
||||
|
||||
local is_index = opts.ref == ""
|
||||
local other = M.git_show_buf(worktree, opts.ref, rel, is_index)
|
||||
local label = is_index and "index" or opts.ref
|
||||
set_buf_name_and_filetype(other, "git://" .. label .. "/" .. rel)
|
||||
|
||||
local split_cmd = opts.vertical and "leftabove vertical sbuffer "
|
||||
or "leftabove sbuffer "
|
||||
vim.cmd(split_cmd .. other)
|
||||
vim.cmd("diffthis")
|
||||
vim.cmd("wincmd p")
|
||||
vim.cmd("diffthis")
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -68,6 +68,31 @@ function M.setup()
|
||||
vim.keymap.set("n", "<leader>gg", function()
|
||||
require("git.status_win").toggle()
|
||||
end, { desc = "Toggle git status sidebar" })
|
||||
vim.keymap.set("n", "<leader>gl", function()
|
||||
require("git.log_win").show()
|
||||
end, { desc = "Show git log" })
|
||||
vim.keymap.set("n", "<leader>gd", function()
|
||||
require("git.diff").split({ ref = "", vertical = true })
|
||||
end, { desc = "Diff index vs worktree (vsplit)" })
|
||||
vim.keymap.set("n", "<leader>gD", function()
|
||||
require("git.diff").split({ ref = "HEAD", vertical = true })
|
||||
end, { desc = "Diff HEAD vs worktree (vsplit)" })
|
||||
vim.keymap.set("n", "<leader>gh", function()
|
||||
require("git.diff").split({ ref = "", vertical = false })
|
||||
end, { desc = "Diff index vs worktree (split)" })
|
||||
vim.keymap.set("n", "<leader>gH", function()
|
||||
require("git.diff").split({ ref = "HEAD", vertical = false })
|
||||
end, { desc = "Diff HEAD vs worktree (split)" })
|
||||
vim.keymap.set("n", "<leader>gc", function()
|
||||
require("git.commit").commit()
|
||||
end, { desc = "Git commit" })
|
||||
vim.keymap.set("n", "<leader>ga", function()
|
||||
require("git.commit").commit({ amend = true })
|
||||
end, { desc = "Git commit --amend" })
|
||||
vim.keymap.set("n", "<leader>gp", function()
|
||||
require("git.cmd").run({ "push" })
|
||||
end, { desc = "Git push" })
|
||||
require("git.cmd").setup()
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
local log = require("log")
|
||||
local repo = require("git.repo")
|
||||
|
||||
local M = {}
|
||||
|
||||
local LOG_FORMAT = "%h %ad {%an}%d %s"
|
||||
|
||||
function M.show()
|
||||
local path = vim.api.nvim_buf_get_name(0)
|
||||
if path == "" then
|
||||
path = vim.fn.getcwd()
|
||||
end
|
||||
local _, worktree = repo.resolve(path)
|
||||
if not worktree then
|
||||
log.warning("not in a git repository")
|
||||
return
|
||||
end
|
||||
|
||||
local result = vim.system({
|
||||
"git",
|
||||
"log",
|
||||
"--graph",
|
||||
"--all",
|
||||
"--decorate",
|
||||
"--date=short",
|
||||
"--format=format:" .. LOG_FORMAT,
|
||||
}, { cwd = worktree, text = true }):wait()
|
||||
if result.code ~= 0 then
|
||||
log.error("git log failed: %s", result.stderr or "")
|
||||
return
|
||||
end
|
||||
|
||||
local lines = vim.split(
|
||||
result.stdout or "",
|
||||
"\n",
|
||||
{ plain = true, trimempty = false }
|
||||
)
|
||||
if #lines > 0 and lines[#lines] == "" then
|
||||
table.remove(lines)
|
||||
end
|
||||
|
||||
vim.cmd("new")
|
||||
local buf = vim.api.nvim_get_current_buf()
|
||||
vim.bo[buf].buftype = "nofile"
|
||||
vim.bo[buf].bufhidden = "hide"
|
||||
vim.bo[buf].swapfile = false
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
|
||||
vim.bo[buf].modifiable = false
|
||||
vim.bo[buf].modified = false
|
||||
vim.b[buf].git_worktree = worktree
|
||||
vim.bo[buf].filetype = "gitlog"
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -274,12 +274,38 @@ local function head(path)
|
||||
return nil
|
||||
end
|
||||
|
||||
---Resolve a git revision to its object SHA. Returns nil if the ref can't be
|
||||
---resolved (root-commit's `^`, blob's `^`, malformed ref, etc.). When `short`
|
||||
---is true, the result is abbreviated via `core.abbrev` (auto-extended by git
|
||||
---to keep the prefix unique in the current repo).
|
||||
---@param worktree string
|
||||
---@param ref string
|
||||
---@param short? boolean
|
||||
---@return string?
|
||||
local function rev_parse(worktree, ref, short)
|
||||
local cmd = { "git", "rev-parse", "--verify", "--quiet" }
|
||||
if short then
|
||||
table.insert(cmd, "--short")
|
||||
end
|
||||
table.insert(cmd, ref)
|
||||
local result = vim.system(cmd, { cwd = worktree, text = true }):wait()
|
||||
if result.code ~= 0 then
|
||||
return nil
|
||||
end
|
||||
local sha = vim.trim(result.stdout or "")
|
||||
if sha == "" then
|
||||
return nil
|
||||
end
|
||||
return sha
|
||||
end
|
||||
|
||||
return {
|
||||
UNMERGED = UNMERGED,
|
||||
head = head,
|
||||
indicator = indicator,
|
||||
refresh_buf = refresh_buf,
|
||||
resolve = resolve,
|
||||
rev_parse = rev_parse,
|
||||
stop_all = stop_all,
|
||||
unregister = unregister,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
local diff = require("git.diff")
|
||||
local log = require("log")
|
||||
local repo = require("git.repo")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class ow.Git.DiffSection
|
||||
---@field pre_path string path on the parent side (`a/...`)
|
||||
---@field post_path string path on the current side (`b/...`)
|
||||
---@field pre_blob string?
|
||||
---@field post_blob string?
|
||||
|
||||
---@class ow.Git.ShowContext
|
||||
---@field worktree string
|
||||
---@field ref string resolved commit SHA of the gitobject buffer
|
||||
---@field parent_ref string? resolved parent commit SHA, nil for root commits
|
||||
|
||||
---@return ow.Git.ShowContext?
|
||||
local function context()
|
||||
local worktree = vim.b.git_worktree
|
||||
local ref = vim.b.git_ref
|
||||
if not worktree or not ref then
|
||||
return nil
|
||||
end
|
||||
return { worktree = worktree, ref = ref, parent_ref = vim.b.git_parent_ref }
|
||||
end
|
||||
|
||||
---Walk upward from the cursor to the enclosing `diff --git` line and parse
|
||||
---the section's pre/post paths plus the pre/post blob SHAs from the `index`
|
||||
---line.
|
||||
---@param cursor_lnum integer 1-indexed
|
||||
---@return ow.Git.DiffSection?
|
||||
local function diff_section(cursor_lnum)
|
||||
local lines = vim.api.nvim_buf_get_lines(0, 0, cursor_lnum, false)
|
||||
local diff_lnum, diff_line
|
||||
for i = #lines, 1, -1 do
|
||||
if lines[i]:match("^diff %-%-git ") then
|
||||
diff_lnum = i
|
||||
diff_line = lines[i]
|
||||
break
|
||||
end
|
||||
end
|
||||
if not diff_lnum or not diff_line then
|
||||
return nil
|
||||
end
|
||||
local pre_path, post_path = diff_line:match("^diff %-%-git a/(.-) b/(.+)$")
|
||||
if not pre_path or not post_path then
|
||||
return nil
|
||||
end
|
||||
|
||||
local header =
|
||||
vim.api.nvim_buf_get_lines(0, diff_lnum, diff_lnum + 20, false)
|
||||
local pre_blob, post_blob
|
||||
for _, l in ipairs(header) do
|
||||
if l:sub(1, 3) == "@@ " or l:match("^diff %-%-git ") then
|
||||
break
|
||||
end
|
||||
local pre, post = l:match("^index (%x+)%.%.(%x+)")
|
||||
if pre then
|
||||
pre_blob = pre
|
||||
post_blob = post
|
||||
break
|
||||
end
|
||||
end
|
||||
return {
|
||||
pre_path = pre_path,
|
||||
post_path = post_path,
|
||||
pre_blob = pre_blob,
|
||||
post_blob = post_blob,
|
||||
}
|
||||
end
|
||||
|
||||
---@param sha string?
|
||||
---@return boolean
|
||||
local function is_zero(sha)
|
||||
return sha == nil or sha:match("^0+$") ~= nil
|
||||
end
|
||||
|
||||
---@param ref string buffer-name ref segment
|
||||
---@param path string
|
||||
---@return integer
|
||||
local function empty_buf(ref, path)
|
||||
local buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.bo[buf].buftype = "nofile"
|
||||
vim.bo[buf].bufhidden = "hide"
|
||||
vim.bo[buf].swapfile = false
|
||||
vim.bo[buf].modifiable = false
|
||||
pcall(vim.api.nvim_buf_set_name, buf, "git://" .. ref .. "/" .. path)
|
||||
return buf
|
||||
end
|
||||
|
||||
---Build a buffer holding the file's content at a given blob, named after the
|
||||
---commit ref it corresponds to (so the name lines up with `git log` output
|
||||
---instead of an opaque blob hash).
|
||||
---@param worktree string
|
||||
---@param blob string?
|
||||
---@param path string
|
||||
---@param ref string the commit ref the blob represents (e.g. `<sha>` or `<sha>^`)
|
||||
---@return integer
|
||||
local function blob_buf(worktree, blob, path, ref)
|
||||
if is_zero(blob) then
|
||||
return empty_buf(ref, path)
|
||||
end
|
||||
---@cast blob string
|
||||
local buf = diff.git_show_blob(worktree, blob)
|
||||
pcall(vim.api.nvim_buf_set_name, buf, "git://" .. ref .. "/" .. path)
|
||||
local ft = vim.filetype.match({ buf = buf })
|
||||
if ft then
|
||||
vim.bo[buf].filetype = ft
|
||||
end
|
||||
return buf
|
||||
end
|
||||
|
||||
---@param worktree string
|
||||
---@param blob string?
|
||||
---@param path string
|
||||
---@param ref string
|
||||
local function show_blob(worktree, blob, path, ref)
|
||||
local buf = blob_buf(worktree, blob, path, ref)
|
||||
vim.cmd("normal! m'")
|
||||
vim.cmd("buffer " .. buf)
|
||||
end
|
||||
|
||||
---@param ctx ow.Git.ShowContext
|
||||
---@param section ow.Git.DiffSection
|
||||
local function show_diff(ctx, section)
|
||||
if not section.pre_blob or not section.post_blob then
|
||||
log.warning("no index line; cannot determine blob SHAs")
|
||||
return
|
||||
end
|
||||
local parent = ctx.parent_ref or "0"
|
||||
local left =
|
||||
blob_buf(ctx.worktree, section.pre_blob, section.pre_path, parent)
|
||||
local right =
|
||||
blob_buf(ctx.worktree, section.post_blob, section.post_path, ctx.ref)
|
||||
vim.cmd("normal! m'")
|
||||
vim.cmd("buffer " .. left)
|
||||
vim.cmd("diffthis")
|
||||
vim.cmd("rightbelow vertical sbuffer " .. right)
|
||||
vim.cmd("diffthis")
|
||||
vim.cmd("wincmd p")
|
||||
end
|
||||
|
||||
---@param worktree string
|
||||
---@param ref string
|
||||
function M.open_commit(worktree, ref)
|
||||
local result = vim.system(
|
||||
{ "git", "show", ref },
|
||||
{ cwd = worktree, text = true }
|
||||
)
|
||||
:wait()
|
||||
if result.code ~= 0 then
|
||||
log.error("git show %s failed: %s", ref, result.stderr or "")
|
||||
return
|
||||
end
|
||||
local content = result.stdout or ""
|
||||
local lines = vim.split(content, "\n", { plain = true, trimempty = false })
|
||||
if #lines > 0 and lines[#lines] == "" then
|
||||
table.remove(lines)
|
||||
end
|
||||
local sha = repo.rev_parse(worktree, ref, true) or ref
|
||||
local parent = repo.rev_parse(worktree, ref .. "^", true)
|
||||
local buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.bo[buf].buftype = "nofile"
|
||||
vim.bo[buf].bufhidden = "hide"
|
||||
vim.bo[buf].swapfile = false
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
|
||||
vim.bo[buf].modifiable = false
|
||||
vim.bo[buf].modified = false
|
||||
pcall(vim.api.nvim_buf_set_name, buf, "git://" .. sha .. "/")
|
||||
vim.b[buf].git_worktree = worktree
|
||||
vim.b[buf].git_ref = sha
|
||||
vim.b[buf].git_parent_ref = parent
|
||||
vim.bo[buf].filetype = "git"
|
||||
vim.cmd("normal! m'")
|
||||
vim.cmd("buffer " .. buf)
|
||||
end
|
||||
|
||||
---@return boolean dispatched true if the cursor was on an actionable line
|
||||
function M.open_at_cursor()
|
||||
local ctx = context()
|
||||
if not ctx then
|
||||
return false
|
||||
end
|
||||
local cursor_lnum = vim.api.nvim_win_get_cursor(0)[1]
|
||||
local section = diff_section(cursor_lnum)
|
||||
if not section then
|
||||
return false
|
||||
end
|
||||
local parent = ctx.parent_ref or "0"
|
||||
|
||||
local line = vim.api.nvim_get_current_line()
|
||||
if line:match("^diff %-%-git ") then
|
||||
show_diff(ctx, section)
|
||||
return true
|
||||
end
|
||||
if line:match("^%-%-%- ") then
|
||||
show_blob(ctx.worktree, section.pre_blob, section.pre_path, parent)
|
||||
return true
|
||||
end
|
||||
if line:match("^%+%+%+ ") then
|
||||
show_blob(ctx.worktree, section.post_blob, section.post_path, ctx.ref)
|
||||
return true
|
||||
end
|
||||
local prefix = line:sub(1, 1)
|
||||
if prefix == "+" then
|
||||
show_blob(ctx.worktree, section.post_blob, section.post_path, ctx.ref)
|
||||
return true
|
||||
elseif prefix == "-" then
|
||||
show_blob(ctx.worktree, section.pre_blob, section.pre_path, parent)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
return M
|
||||
+7
-108
@@ -1,3 +1,4 @@
|
||||
local diff = require("git.diff")
|
||||
local log = require("log")
|
||||
local repo = require("git.repo")
|
||||
|
||||
@@ -361,109 +362,6 @@ local function current_entry(bufnr)
|
||||
return s, s.lines[lnum]
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
---@param worktree string
|
||||
---@param path string
|
||||
local function attach_index_writer(buf, worktree, path)
|
||||
vim.api.nvim_create_autocmd("BufWriteCmd", {
|
||||
buffer = buf,
|
||||
callback = function()
|
||||
local body = table.concat(
|
||||
vim.api.nvim_buf_get_lines(buf, 0, -1, false),
|
||||
"\n"
|
||||
) .. "\n"
|
||||
local hash = vim.system(
|
||||
{ "git", "hash-object", "-w", "--stdin" },
|
||||
{ cwd = worktree, stdin = body, text = true }
|
||||
):wait()
|
||||
if hash.code ~= 0 then
|
||||
log.error("git hash-object failed: %s", hash.stderr or "")
|
||||
return
|
||||
end
|
||||
local sha = vim.trim(hash.stdout or "")
|
||||
local mode = "100644"
|
||||
local ls = vim.system(
|
||||
{ "git", "ls-files", "-s", "--", path },
|
||||
{ cwd = worktree, text = true }
|
||||
):wait()
|
||||
if ls.code == 0 and ls.stdout then
|
||||
local m = ls.stdout:match("^(%d+)")
|
||||
if m then
|
||||
mode = m
|
||||
end
|
||||
end
|
||||
local upd = vim.system({
|
||||
"git",
|
||||
"update-index",
|
||||
"--cacheinfo",
|
||||
mode .. "," .. sha .. "," .. path,
|
||||
}, { cwd = worktree, text = true }):wait()
|
||||
if upd.code ~= 0 then
|
||||
log.error("git update-index failed: %s", upd.stderr or "")
|
||||
return
|
||||
end
|
||||
vim.bo[buf].modified = false
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
---@param worktree string
|
||||
---@param ref string '' for index, 'HEAD' for HEAD
|
||||
---@param path string
|
||||
---@param is_index boolean? true to hook :w to update the git index
|
||||
---@return integer
|
||||
local function git_show_buf(worktree, ref, path, is_index)
|
||||
local result = vim.system(
|
||||
{ "git", "show", ref .. ":" .. path },
|
||||
{ cwd = worktree, text = true }
|
||||
):wait()
|
||||
local content = result.code == 0 and (result.stdout or "") or ""
|
||||
local lines = vim.split(content, "\n", { plain = true, trimempty = false })
|
||||
if #lines > 0 and lines[#lines] == "" then
|
||||
table.remove(lines)
|
||||
end
|
||||
local buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
|
||||
vim.bo[buf].buftype = is_index and "acwrite" or "nofile"
|
||||
vim.bo[buf].bufhidden = "wipe"
|
||||
vim.bo[buf].swapfile = false
|
||||
if not is_index then
|
||||
vim.bo[buf].modifiable = false
|
||||
end
|
||||
if is_index then
|
||||
attach_index_writer(buf, worktree, path)
|
||||
end
|
||||
vim.bo[buf].modified = false
|
||||
return buf
|
||||
end
|
||||
|
||||
---@return integer
|
||||
local function empty_buf()
|
||||
local buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.bo[buf].buftype = "nofile"
|
||||
vim.bo[buf].bufhidden = "wipe"
|
||||
vim.bo[buf].swapfile = false
|
||||
vim.bo[buf].modifiable = false
|
||||
vim.bo[buf].modified = false
|
||||
return buf
|
||||
end
|
||||
|
||||
---@param abs_path string
|
||||
---@return integer
|
||||
local function load_file_buf(abs_path)
|
||||
for _, buf in ipairs(vim.api.nvim_list_bufs()) do
|
||||
if
|
||||
vim.api.nvim_buf_is_loaded(buf)
|
||||
and vim.api.nvim_buf_get_name(buf) == abs_path
|
||||
then
|
||||
return buf
|
||||
end
|
||||
end
|
||||
local buf = vim.fn.bufadd(abs_path)
|
||||
vim.fn.bufload(buf)
|
||||
return buf
|
||||
end
|
||||
|
||||
---@class ow.Git.DiffSide
|
||||
---@field buf integer
|
||||
---@field name string?
|
||||
@@ -478,7 +376,8 @@ end
|
||||
---@return ow.Git.DiffSide
|
||||
local function head_pane(worktree, path, content)
|
||||
return {
|
||||
buf = content and git_show_buf(worktree, "HEAD", path) or empty_buf(),
|
||||
buf = content and diff.git_show_buf(worktree, "HEAD", path)
|
||||
or diff.empty_buf(),
|
||||
name = "git://HEAD/" .. path,
|
||||
}
|
||||
end
|
||||
@@ -490,11 +389,11 @@ end
|
||||
local function worktree_pane(worktree, path, exists)
|
||||
if exists then
|
||||
return {
|
||||
buf = load_file_buf(vim.fs.joinpath(worktree, path)),
|
||||
buf = diff.load_file_buf(vim.fs.joinpath(worktree, path)),
|
||||
name = nil,
|
||||
}
|
||||
end
|
||||
return { buf = empty_buf(), name = "git://worktree/" .. path }
|
||||
return { buf = diff.empty_buf(), name = "git://worktree/" .. path }
|
||||
end
|
||||
|
||||
---@param s ow.Git.StatusState
|
||||
@@ -506,8 +405,8 @@ local function index_pane(s, entry)
|
||||
or (entry.section == "Staged" and entry.x == "D")
|
||||
)
|
||||
return {
|
||||
buf = in_index and git_show_buf(s.worktree, "", entry.path, true)
|
||||
or empty_buf(),
|
||||
buf = in_index and diff.git_show_buf(s.worktree, "", entry.path, true)
|
||||
or diff.empty_buf(),
|
||||
name = "git://index/" .. entry.path,
|
||||
}
|
||||
end
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
vim.api.nvim_create_user_command("Glog", function(opts)
|
||||
local mods = opts.mods ~= "" and (opts.mods .. " ") or ""
|
||||
vim.cmd(
|
||||
mods
|
||||
.. "Git log --graph --all --decorate --date=short "
|
||||
.. "--format=format:'%h %ad {%an}%d %s' "
|
||||
.. opts.args
|
||||
)
|
||||
end, { nargs = "*", desc = "Pretty git log via fugitive" })
|
||||
|
||||
vim.keymap.set("n", "<leader>gl", vim.cmd.Glog)
|
||||
vim.keymap.set("n", "<leader>gd", vim.cmd.Gvdiffsplit)
|
||||
vim.keymap.set("n", "<leader>gD", function()
|
||||
vim.cmd.Gvdiffsplit("HEAD")
|
||||
end)
|
||||
vim.keymap.set("n", "<leader>gh", vim.cmd.Ghdiffsplit)
|
||||
vim.keymap.set("n", "<leader>gH", function()
|
||||
vim.cmd.Ghdiffsplit("HEAD")
|
||||
end)
|
||||
vim.keymap.set("n", "<leader>gc", function()
|
||||
vim.cmd.G("commit")
|
||||
end)
|
||||
vim.keymap.set("n", "<leader>ga", function()
|
||||
vim.cmd.G("commit --amend")
|
||||
end)
|
||||
vim.keymap.set("n", "<leader>gp", function()
|
||||
vim.cmd.G("push")
|
||||
end)
|
||||
|
||||
vim.api.nvim_create_autocmd("User", {
|
||||
pattern = "GitRefresh",
|
||||
group = vim.api.nvim_create_augroup("ow.fugitive", { clear = true }),
|
||||
callback = function()
|
||||
vim.fn["fugitive#ReloadStatus"]()
|
||||
end,
|
||||
})
|
||||
@@ -1,3 +1,7 @@
|
||||
if exists("b:current_syntax")
|
||||
finish
|
||||
endif
|
||||
|
||||
syntax match gitlogGraph contained /^[*|\\\/_ ]*/
|
||||
\ nextgroup=gitlogHash
|
||||
syntax match gitlogHash contained /\<\x\{7,40\}\>/
|
||||
@@ -18,3 +22,5 @@ highlight default link gitlogHash GitSha
|
||||
highlight default link gitlogDate Number
|
||||
highlight default link gitlogAuthor String
|
||||
highlight default link gitlogRef Constant
|
||||
|
||||
let b:current_syntax = "gitlog"
|
||||
Reference in New Issue
Block a user