feat(git): replace vim-fugitive with custom git module

This commit is contained in:
2026-04-27 12:41:38 +02:00
parent 5a3e39574d
commit f55d7ac11d
13 changed files with 837 additions and 145 deletions
+17
View File
@@ -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" })
+13
View File
@@ -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" })
-1
View File
@@ -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
View File
@@ -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
+78
View File
@@ -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
+179
View File
@@ -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
+25
View File
@@ -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
+54
View File
@@ -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
+26
View File
@@ -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,
}
+216
View File
@@ -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
View File
@@ -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
-36
View File
@@ -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"