diff --git a/after/ftplugin/git.lua b/after/ftplugin/git.lua new file mode 100644 index 0000000..b3ca741 --- /dev/null +++ b/after/ftplugin/git.lua @@ -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 dispatcher off any unrelated git buffer (a +-- real `.git/HEAD` file, etc.) so the default normal-mode still works. +if not vim.b.git_worktree then + return +end + +local cr = vim.api.nvim_replace_termcodes("", true, false, true) + +vim.keymap.set("n", "", 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" }) diff --git a/ftplugin/gitlog.lua b/ftplugin/gitlog.lua new file mode 100644 index 0000000..1ff2d52 --- /dev/null +++ b/ftplugin/gitlog.lua @@ -0,0 +1,13 @@ +local cr = vim.api.nvim_replace_termcodes("", true, false, true) + +vim.keymap.set("n", "", 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" }) diff --git a/init.lua b/init.lua index 8b4bb27..5e971b5 100644 --- a/init.lua +++ b/init.lua @@ -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", diff --git a/lua/git/cmd.lua b/lua/git/cmd.lua new file mode 100644 index 0000000..d1a29c9 --- /dev/null +++ b/lua/git/cmd.lua @@ -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 +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 diff --git a/lua/git/commit.lua b/lua/git/commit.lua new file mode 100644 index 0000000..4c7882f --- /dev/null +++ b/lua/git/commit.lua @@ -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 diff --git a/lua/git/diff.lua b/lua/git/diff.lua new file mode 100644 index 0000000..d3070e9 --- /dev/null +++ b/lua/git/diff.lua @@ -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 diff --git a/lua/git/init.lua b/lua/git/init.lua index 7713094..4af5f42 100644 --- a/lua/git/init.lua +++ b/lua/git/init.lua @@ -68,6 +68,31 @@ function M.setup() vim.keymap.set("n", "gg", function() require("git.status_win").toggle() end, { desc = "Toggle git status sidebar" }) + vim.keymap.set("n", "gl", function() + require("git.log_win").show() + end, { desc = "Show git log" }) + vim.keymap.set("n", "gd", function() + require("git.diff").split({ ref = "", vertical = true }) + end, { desc = "Diff index vs worktree (vsplit)" }) + vim.keymap.set("n", "gD", function() + require("git.diff").split({ ref = "HEAD", vertical = true }) + end, { desc = "Diff HEAD vs worktree (vsplit)" }) + vim.keymap.set("n", "gh", function() + require("git.diff").split({ ref = "", vertical = false }) + end, { desc = "Diff index vs worktree (split)" }) + vim.keymap.set("n", "gH", function() + require("git.diff").split({ ref = "HEAD", vertical = false }) + end, { desc = "Diff HEAD vs worktree (split)" }) + vim.keymap.set("n", "gc", function() + require("git.commit").commit() + end, { desc = "Git commit" }) + vim.keymap.set("n", "ga", function() + require("git.commit").commit({ amend = true }) + end, { desc = "Git commit --amend" }) + vim.keymap.set("n", "gp", function() + require("git.cmd").run({ "push" }) + end, { desc = "Git push" }) + require("git.cmd").setup() end return M diff --git a/lua/git/log_win.lua b/lua/git/log_win.lua new file mode 100644 index 0000000..1feb0c8 --- /dev/null +++ b/lua/git/log_win.lua @@ -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 diff --git a/lua/git/repo.lua b/lua/git/repo.lua index a4b18e6..d60a14f 100644 --- a/lua/git/repo.lua +++ b/lua/git/repo.lua @@ -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, } diff --git a/lua/git/show.lua b/lua/git/show.lua new file mode 100644 index 0000000..f0e2cb5 --- /dev/null +++ b/lua/git/show.lua @@ -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. `` or `^`) +---@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 diff --git a/lua/git/status_win.lua b/lua/git/status_win.lua index d697181..d91a296 100644 --- a/lua/git/status_win.lua +++ b/lua/git/status_win.lua @@ -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 diff --git a/plugins/vim-fugitive.lua b/plugins/vim-fugitive.lua deleted file mode 100644 index 78f07ba..0000000 --- a/plugins/vim-fugitive.lua +++ /dev/null @@ -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", "gl", vim.cmd.Glog) -vim.keymap.set("n", "gd", vim.cmd.Gvdiffsplit) -vim.keymap.set("n", "gD", function() - vim.cmd.Gvdiffsplit("HEAD") -end) -vim.keymap.set("n", "gh", vim.cmd.Ghdiffsplit) -vim.keymap.set("n", "gH", function() - vim.cmd.Ghdiffsplit("HEAD") -end) -vim.keymap.set("n", "gc", function() - vim.cmd.G("commit") -end) -vim.keymap.set("n", "ga", function() - vim.cmd.G("commit --amend") -end) -vim.keymap.set("n", "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, -}) diff --git a/after/syntax/git.vim b/syntax/gitlog.vim similarity index 91% rename from after/syntax/git.vim rename to syntax/gitlog.vim index 889ef71..8fb6930 100644 --- a/after/syntax/git.vim +++ b/syntax/gitlog.vim @@ -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"