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/owallb/mason-auto-install.nvim",
"https://github.com/mfussenegger/nvim-dap", "https://github.com/mfussenegger/nvim-dap",
"https://github.com/numToStr/Comment.nvim", "https://github.com/numToStr/Comment.nvim",
"https://github.com/tpope/vim-fugitive",
"https://github.com/lewis6991/gitsigns.nvim", "https://github.com/lewis6991/gitsigns.nvim",
"https://github.com/MagicDuck/grug-far.nvim", "https://github.com/MagicDuck/grug-far.nvim",
"https://github.com/nvim-tree/nvim-tree.lua", "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() vim.keymap.set("n", "<leader>gg", function()
require("git.status_win").toggle() require("git.status_win").toggle()
end, { desc = "Toggle git status sidebar" }) 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 end
return M 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 return nil
end 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 { return {
UNMERGED = UNMERGED, UNMERGED = UNMERGED,
head = head, head = head,
indicator = indicator, indicator = indicator,
refresh_buf = refresh_buf, refresh_buf = refresh_buf,
resolve = resolve, resolve = resolve,
rev_parse = rev_parse,
stop_all = stop_all, stop_all = stop_all,
unregister = unregister, 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 log = require("log")
local repo = require("git.repo") local repo = require("git.repo")
@@ -361,109 +362,6 @@ local function current_entry(bufnr)
return s, s.lines[lnum] return s, s.lines[lnum]
end 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 ---@class ow.Git.DiffSide
---@field buf integer ---@field buf integer
---@field name string? ---@field name string?
@@ -478,7 +376,8 @@ end
---@return ow.Git.DiffSide ---@return ow.Git.DiffSide
local function head_pane(worktree, path, content) local function head_pane(worktree, path, content)
return { 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, name = "git://HEAD/" .. path,
} }
end end
@@ -490,11 +389,11 @@ end
local function worktree_pane(worktree, path, exists) local function worktree_pane(worktree, path, exists)
if exists then if exists then
return { return {
buf = load_file_buf(vim.fs.joinpath(worktree, path)), buf = diff.load_file_buf(vim.fs.joinpath(worktree, path)),
name = nil, name = nil,
} }
end end
return { buf = empty_buf(), name = "git://worktree/" .. path } return { buf = diff.empty_buf(), name = "git://worktree/" .. path }
end end
---@param s ow.Git.StatusState ---@param s ow.Git.StatusState
@@ -506,8 +405,8 @@ local function index_pane(s, entry)
or (entry.section == "Staged" and entry.x == "D") or (entry.section == "Staged" and entry.x == "D")
) )
return { return {
buf = in_index and git_show_buf(s.worktree, "", entry.path, true) buf = in_index and diff.git_show_buf(s.worktree, "", entry.path, true)
or empty_buf(), or diff.empty_buf(),
name = "git://index/" .. entry.path, name = "git://index/" .. entry.path,
} }
end 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 /^[*|\\\/_ ]*/ syntax match gitlogGraph contained /^[*|\\\/_ ]*/
\ nextgroup=gitlogHash \ nextgroup=gitlogHash
syntax match gitlogHash contained /\<\x\{7,40\}\>/ syntax match gitlogHash contained /\<\x\{7,40\}\>/
@@ -18,3 +22,5 @@ highlight default link gitlogHash GitSha
highlight default link gitlogDate Number highlight default link gitlogDate Number
highlight default link gitlogAuthor String highlight default link gitlogAuthor String
highlight default link gitlogRef Constant highlight default link gitlogRef Constant
let b:current_syntax = "gitlog"