local Revision = require("git.revision") local repo = require("git.repo") local util = require("git.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 sha string? ---@return boolean local function is_zero(sha) return sha == nil or sha:match("^0+$") ~= nil 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.exec( { "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.exec( { "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.exec({ "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("", true, false, true) ---@param buf integer function M.attach_dispatch(buf) vim.keymap.set("n", "", 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" }) 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.exec( { "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.exec({ "git", "diff-tree", "-p", "-m", "--first-parent", "--root", "--no-commit-id", commit_sha, }, { cwd = r.worktree }) if patch then stdout = (stdout:gsub("\n*$", "\n\n")) .. patch end state.parent_sha = r:rev_parse(commit_sha .. "^", true) end end util.buf_set_lines(buf, 0, -1, false, 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]] vim.bo[buf].swapfile = false vim.bo[buf].bufhidden = "hide" 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 rev.stage == 0 and rev.path then vim.bo[buf].buftype = "acwrite" if not state.index_writer then attach_index_writer(buf, r, rev.path) state.index_writer = true end else vim.bo[buf].buftype = "nofile" vim.bo[buf].modifiable = false 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 r ow.Git.Repo ---@param blob string? ---@param path string ---@param sha string ---@return integer? local function blob_buf(r, blob, path, sha) if is_zero(blob) then return nil end return M.buf_for(r, Revision.new({ base = sha, path = path })) end ---@param r ow.Git.Repo ---@param blob string? ---@param path string ---@param sha string local function load_blob(r, blob, path, sha) local buf = blob_buf(r, blob, path, sha) if not buf then util.error("no content for %s at %s", path, sha) return end vim.cmd.normal({ "m'", bang = true }) vim.api.nvim_set_current_buf(buf) end ---@param s ow.Git.Repo.BufState ---@param section ow.Git.DiffSection local function open_section(s, section) if not section.blob_a or not section.blob_b then util.error("no index line, cannot determine blob SHAs") return end local parent = s.parent_sha or "0" local left = blob_buf(s.repo, section.blob_a, section.path_a, parent) local right = blob_buf(s.repo, section.blob_b, section.path_b, s.sha --[[@as string]]) if left and right then require("git.diff").open(left, right, 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, true) if sha then parsed.base = sha end end if not r:rev_parse(parsed:format(), true) 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 s = repo.state() if not s or not s.sha then return false end local line = vim.api.nvim_get_current_line() local r = s.repo 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 local nav_rev = entry_type == "blob" and Revision.new({ base = s.sha, path = entry_name }):format() or entry_sha M.open(r, nav_rev, { split = false }) return true end local section = diff_section() if not section then return false end local parent = s.parent_sha or "0" if line:match("^diff %-%-git ") then open_section(s, section) return true end if line:match("^%-%-%- ") then load_blob(r, section.blob_a, section.path_a, parent) return true end if line:match("^%+%+%+ ") then load_blob(r, section.blob_b, section.path_b, s.sha --[[@as string]]) return true end local prefix = line:sub(1, 1) if prefix == "+" then load_blob(r, section.blob_b, section.path_b, s.sha --[[@as string]]) return true elseif prefix == "-" then load_blob(r, section.blob_a, section.path_a, parent) return true end return false end repo.on_uri_refresh(M.URI_PREFIX, refresh) return M