chore: initial commit
This commit is contained in:
@@ -0,0 +1,434 @@
|
||||
local Revision = require("git.core.revision")
|
||||
local repo = require("git.core.repo")
|
||||
local util = require("git.core.util")
|
||||
|
||||
local M = {}
|
||||
|
||||
M.URI_PREFIX = "git://"
|
||||
|
||||
---@param rev ow.Git.Revision
|
||||
---@return string
|
||||
function M.format_uri(rev)
|
||||
return M.URI_PREFIX .. rev:format()
|
||||
end
|
||||
|
||||
---@param str string
|
||||
---@return ow.Git.Revision?
|
||||
function M.parse_uri(str)
|
||||
local raw = str:match("^" .. M.URI_PREFIX .. "(.+)$")
|
||||
if raw then
|
||||
return Revision.parse(raw)
|
||||
end
|
||||
end
|
||||
|
||||
---@class ow.Git.DiffSection
|
||||
---@field path_a string
|
||||
---@field path_b string
|
||||
---@field blob_a string?
|
||||
---@field blob_b string?
|
||||
|
||||
---@return ow.Git.DiffSection?
|
||||
local function diff_section()
|
||||
local diff_lnum = vim.fn.search("^diff --git ", "bcnW")
|
||||
if diff_lnum == 0 then
|
||||
return nil
|
||||
end
|
||||
local diff_line =
|
||||
vim.api.nvim_buf_get_lines(0, diff_lnum - 1, diff_lnum, false)[1]
|
||||
if not diff_line then
|
||||
return nil
|
||||
end
|
||||
local path_a, path_b = diff_line:match("^diff %-%-git a/(.-) b/(.+)$")
|
||||
if not path_a or not path_b then
|
||||
return nil
|
||||
end
|
||||
|
||||
local header =
|
||||
vim.api.nvim_buf_get_lines(0, diff_lnum, diff_lnum + 20, false)
|
||||
local blob_a, blob_b
|
||||
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
|
||||
blob_a = pre
|
||||
blob_b = post
|
||||
break
|
||||
end
|
||||
end
|
||||
return {
|
||||
path_a = path_a,
|
||||
path_b = path_b,
|
||||
blob_a = blob_a,
|
||||
blob_b = blob_b,
|
||||
}
|
||||
end
|
||||
|
||||
---@param rev ow.Git.Revision
|
||||
---@return boolean
|
||||
local function is_immutable_rev(rev)
|
||||
if rev.stage ~= nil then
|
||||
return false
|
||||
end
|
||||
local base = rev.base
|
||||
if not base then
|
||||
return false
|
||||
end
|
||||
local stripped = base:gsub("%^%b{}", ""):gsub("[%^~]%d*", "")
|
||||
return stripped:match("^%x+$") ~= nil and #stripped >= 7
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
---@param r ow.Git.Repo
|
||||
---@param path string
|
||||
local function attach_index_writer(buf, r, 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_stdout = util.git(
|
||||
{ "hash-object", "-w", "--stdin" },
|
||||
{ cwd = r.worktree, stdin = body }
|
||||
)
|
||||
if not hash_stdout then
|
||||
return
|
||||
end
|
||||
local sha = vim.trim(hash_stdout)
|
||||
local state = r:state(buf)
|
||||
local mode = state and state.index_mode
|
||||
if not mode then
|
||||
mode = "100644"
|
||||
local ls = util.git(
|
||||
{ "ls-files", "-s", "--", path },
|
||||
{ cwd = r.worktree, silent = true }
|
||||
)
|
||||
if ls then
|
||||
local m = ls:match("^(%d+)")
|
||||
if m then
|
||||
mode = m
|
||||
end
|
||||
end
|
||||
if state then
|
||||
state.index_mode = mode
|
||||
end
|
||||
end
|
||||
if
|
||||
not util.git({
|
||||
"update-index",
|
||||
"--cacheinfo",
|
||||
mode,
|
||||
sha,
|
||||
path,
|
||||
}, { cwd = r.worktree })
|
||||
then
|
||||
return
|
||||
end
|
||||
if state then
|
||||
state.sha = r:rev_parse(":" .. path, true)
|
||||
end
|
||||
vim.bo[buf].modified = false
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
local cr = vim.api.nvim_replace_termcodes("<CR>", true, false, true)
|
||||
|
||||
---@param buf integer
|
||||
function M.attach_dispatch(buf)
|
||||
vim.keymap.set("n", "<CR>", function()
|
||||
if not M.open_under_cursor() then
|
||||
vim.api.nvim_feedkeys(cr, "n", false)
|
||||
end
|
||||
end, { buffer = buf, silent = true, desc = "Open file at commit" })
|
||||
vim.keymap.set("n", "gd", function()
|
||||
M.open_under_cursor()
|
||||
end, { buffer = buf, silent = true, desc = "Open file at commit" })
|
||||
end
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
---@param rev ow.Git.Revision
|
||||
---@return integer
|
||||
function M.buf_for(r, rev)
|
||||
local buf = vim.fn.bufadd(M.format_uri(rev))
|
||||
repo.bind(buf, r)
|
||||
vim.fn.bufload(buf)
|
||||
return buf
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
---@param r ow.Git.Repo
|
||||
---@param rev ow.Git.Revision
|
||||
---@param state ow.Git.Repo.BufState
|
||||
---@param rev_sha string
|
||||
---@return boolean ok
|
||||
local function populate(buf, r, rev, state, rev_sha)
|
||||
local rev_str = rev:format()
|
||||
local stdout = util.git({ "cat-file", "-p", rev_str }, { cwd = r.worktree })
|
||||
if not stdout then
|
||||
return false
|
||||
end
|
||||
|
||||
if rev.path == nil then
|
||||
local commit_sha = r:rev_parse(rev_str .. "^{commit}", true)
|
||||
if commit_sha then
|
||||
local patch = util.git({
|
||||
"diff-tree",
|
||||
"-p",
|
||||
"--diff-merges=first-parent",
|
||||
"--root",
|
||||
"--no-commit-id",
|
||||
commit_sha,
|
||||
}, { cwd = r.worktree })
|
||||
if patch then
|
||||
stdout = (stdout:gsub("\n*$", "\n\n")) .. patch
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
util.set_buf_lines(buf, 0, -1, util.split_lines(stdout))
|
||||
state.sha = rev_sha
|
||||
return true
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
function M.read_uri(buf)
|
||||
local name = vim.api.nvim_buf_get_name(buf)
|
||||
local rev = M.parse_uri(name)
|
||||
if not rev then
|
||||
return
|
||||
end
|
||||
|
||||
local r = repo.resolve(buf)
|
||||
if not r then
|
||||
util.error("git BufReadCmd %s: cannot resolve worktree", name)
|
||||
return
|
||||
end
|
||||
repo.bind(buf, r)
|
||||
local state = r:state(buf) --[[@as -nil]]
|
||||
|
||||
local writable = rev.stage == 0 and rev.path ~= nil
|
||||
util.setup_scratch(buf, {
|
||||
bufhidden = "delete",
|
||||
buftype = writable and "acwrite" or "nofile",
|
||||
modifiable = writable,
|
||||
})
|
||||
|
||||
local rev_sha = r:rev_parse(rev:format(), true)
|
||||
if not rev_sha then
|
||||
return
|
||||
end
|
||||
|
||||
if not populate(buf, r, rev, state, rev_sha) then
|
||||
return
|
||||
end
|
||||
|
||||
state.immutable = is_immutable_rev(rev)
|
||||
|
||||
if writable and not state.index_writer then
|
||||
attach_index_writer(buf, r, rev.path --[[@as string]])
|
||||
state.index_writer = true
|
||||
end
|
||||
|
||||
if rev.path then
|
||||
local ft = vim.filetype.match({ filename = rev.path, buf = buf })
|
||||
if ft then
|
||||
vim.bo[buf].filetype = ft
|
||||
end
|
||||
else
|
||||
vim.bo[buf].filetype = "git"
|
||||
end
|
||||
|
||||
M.attach_dispatch(buf)
|
||||
|
||||
vim.api.nvim_buf_call(buf, function()
|
||||
vim.api.nvim_exec_autocmds("BufReadPost", { buf = buf })
|
||||
end)
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
---@param r ow.Git.Repo
|
||||
local function refresh(buf, r)
|
||||
local state = r:state(buf)
|
||||
if not state or state.immutable or vim.bo[buf].modified then
|
||||
return
|
||||
end
|
||||
local rev = M.parse_uri(vim.api.nvim_buf_get_name(buf))
|
||||
if not rev then
|
||||
return
|
||||
end
|
||||
local rev_sha = r:rev_parse(rev:format(), true)
|
||||
if not rev_sha or rev_sha == state.sha then
|
||||
return
|
||||
end
|
||||
if state.sha == nil then
|
||||
M.read_uri(buf)
|
||||
else
|
||||
populate(buf, r, rev, state, rev_sha)
|
||||
end
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
---@param path string
|
||||
local function set_ft_from_path(buf, path)
|
||||
local ft = vim.filetype.match({ filename = path, buf = buf })
|
||||
if ft then
|
||||
vim.bo[buf].filetype = ft
|
||||
end
|
||||
end
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
---@param blob string?
|
||||
---@param path string
|
||||
---@return integer?
|
||||
local function side_buf(r, blob, path)
|
||||
if not blob or util.is_zero_sha(blob) then
|
||||
return nil
|
||||
end
|
||||
local full, status = r:resolve_sha(blob)
|
||||
if status == "ambiguous" then
|
||||
util.error("ambiguous blob abbreviation: %s", blob)
|
||||
return nil
|
||||
end
|
||||
if full then
|
||||
local buf = M.buf_for(r, Revision.new({ base = full }))
|
||||
set_ft_from_path(buf, path)
|
||||
return buf
|
||||
end
|
||||
local p = vim.fs.joinpath(r.worktree, path)
|
||||
if vim.uv.fs_stat(p) then
|
||||
local buf = vim.fn.bufadd(p)
|
||||
vim.fn.bufload(buf)
|
||||
return buf
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
---@param blob string?
|
||||
---@param path string
|
||||
local function load_side(r, blob, path)
|
||||
local buf = side_buf(r, blob, path)
|
||||
if not buf then
|
||||
util.error("no content for %s", path)
|
||||
return
|
||||
end
|
||||
vim.cmd.normal({ "m'", bang = true })
|
||||
vim.api.nvim_set_current_buf(buf)
|
||||
end
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
---@param section ow.Git.DiffSection
|
||||
local function open_section(r, section)
|
||||
if not section.blob_a or not section.blob_b then
|
||||
util.error("no index line, cannot determine blob SHAs")
|
||||
return
|
||||
end
|
||||
local left = side_buf(r, section.blob_a, section.path_a)
|
||||
local right = side_buf(r, section.blob_b, section.path_b)
|
||||
if left and right then
|
||||
vim.cmd.normal({ "m'", bang = true })
|
||||
vim.api.nvim_set_current_buf(right)
|
||||
require("git.diffsplit").open({
|
||||
target = vim.api.nvim_buf_get_name(left),
|
||||
mods = { vertical = true },
|
||||
})
|
||||
return
|
||||
end
|
||||
if not left and not right then
|
||||
util.error("no content for %s", section.path_b)
|
||||
return
|
||||
end
|
||||
local buf = left or right
|
||||
---@cast buf -nil
|
||||
vim.cmd.normal({ "m'", bang = true })
|
||||
vim.api.nvim_set_current_buf(buf)
|
||||
end
|
||||
|
||||
---@class ow.Git.Object.OpenOpts
|
||||
---@field split (false|"above"|"below"|"left"|"right")?
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
---@param rev string
|
||||
---@param opts ow.Git.Object.OpenOpts?
|
||||
function M.open(r, rev, opts)
|
||||
local parsed = Revision.parse(rev)
|
||||
if parsed.base then
|
||||
local sha = r:rev_parse(parsed.base, false)
|
||||
if not sha then
|
||||
util.error("not a git object: %s", rev)
|
||||
return
|
||||
end
|
||||
parsed.base = sha
|
||||
end
|
||||
if parsed.path and not r:rev_parse(parsed:format(), false) then
|
||||
util.error("not a git object: %s", rev)
|
||||
return
|
||||
end
|
||||
local buf = M.buf_for(r, parsed)
|
||||
util.place_buf(buf, opts and opts.split)
|
||||
end
|
||||
|
||||
---@return boolean dispatched
|
||||
function M.open_under_cursor()
|
||||
local r = repo.resolve()
|
||||
if not r then
|
||||
return false
|
||||
end
|
||||
|
||||
local line = vim.api.nvim_get_current_line()
|
||||
|
||||
local sha = line:match("^commit (%x+)$")
|
||||
or line:match("^parent (%x+)$")
|
||||
or line:match("^tree (%x+)$")
|
||||
or line:match("^object (%x+)$")
|
||||
if sha then
|
||||
M.open(r, sha, { split = false })
|
||||
return true
|
||||
end
|
||||
|
||||
local entry_type, entry_sha, entry_name =
|
||||
line:match("^%d+ (%w+) (%x+)\t(.+)$")
|
||||
if entry_sha then
|
||||
if entry_type == "blob" then
|
||||
load_side(r, entry_sha, entry_name --[[@as string]])
|
||||
else
|
||||
M.open(r, entry_sha, { split = false })
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local section = diff_section()
|
||||
if not section then
|
||||
return false
|
||||
end
|
||||
|
||||
if line:match("^diff %-%-git ") then
|
||||
open_section(r, section)
|
||||
return true
|
||||
end
|
||||
if line:match("^%-%-%- ") then
|
||||
load_side(r, section.blob_a, section.path_a)
|
||||
return true
|
||||
end
|
||||
if line:match("^%+%+%+ ") then
|
||||
load_side(r, section.blob_b, section.path_b)
|
||||
return true
|
||||
end
|
||||
local prefix = line:sub(1, 1)
|
||||
if prefix == "+" then
|
||||
load_side(r, section.blob_b, section.path_b)
|
||||
return true
|
||||
elseif prefix == "-" then
|
||||
load_side(r, section.blob_a, section.path_a)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
repo.on_uri_change(M.URI_PREFIX, refresh)
|
||||
|
||||
return M
|
||||
Reference in New Issue
Block a user