local log = require("log") local repo = require("git.repo") local util = require("util") 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 = vim.b[buf].git_index_mode if not mode then 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 vim.b[buf].git_index_mode = mode end -- Use the 3-arg form (mode sha path) instead of the comma form -- (mode,sha,path), which doesn't survive paths containing a -- comma. 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 ---Run `git show ` asynchronously and call `callback(lines?)` on the ---main loop. `lines` is nil on failure (the error is logged). ---@param worktree string ---@param revspec string anything `git show` accepts (e.g. `HEAD:foo`, `:foo`, blob SHA) ---@param callback fun(lines: string[]?) local function read_show_async(worktree, revspec, callback) vim.system( { "git", "show", revspec }, { cwd = worktree, text = true }, vim.schedule_wrap(function(result) if result.code ~= 0 then log.error( "git show %s failed: %s", revspec, vim.trim(result.stderr or "") ) callback(nil) return end callback(util.split_lines(result.stdout or "")) end) ) end ---Internal builder: create a scratch buffer immediately and asynchronously ---fill it with the content of `git show `. Returning the buffer ---synchronously lets callers wire up windows / `:diffthis` right away; the ---diff updates when the content arrives. The buffer starts non-modifiable ---and the index `BufWriteCmd` is only attached after a successful load, so ---a premature `:w` can't blow away the index entry with empty content. ---@param worktree string ---@param revspec string ---@param is_index boolean ---@param index_path string? required when is_index is true ---@return integer local function build_show_buf(worktree, revspec, is_index, index_path) 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 read_show_async(worktree, revspec, function(lines) if not vim.api.nvim_buf_is_valid(buf) then return end vim.bo[buf].modifiable = true vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines or {}) if is_index then vim.bo[buf].buftype = "acwrite" attach_index_writer(buf, worktree, assert(index_path)) else vim.bo[buf].modifiable = false end vim.bo[buf].modified = false end) return buf 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) return build_show_buf(worktree, ref .. ":" .. path, is_index or false, path) end ---@param worktree string ---@param blob string the blob SHA (full or abbreviated) ---@return integer function M.git_show_blob(worktree, blob) return build_show_buf(worktree, blob, false, nil) end ---@class ow.Git.EmptyBufOpts ---@field name string? ---@field bufhidden ("hide"|"wipe")? defaults to "wipe" ---Build a read-only scratch buffer, optionally naming it via ---`nvim_buf_set_name` (silently no-op if a buffer with that name ---already exists). ---@param opts ow.Git.EmptyBufOpts? ---@return integer function M.empty_buf(opts) opts = opts or {} local buf = vim.api.nvim_create_buf(false, true) vim.bo[buf].buftype = "nofile" vim.bo[buf].bufhidden = opts.bufhidden or "wipe" vim.bo[buf].swapfile = false vim.bo[buf].modifiable = false vim.bo[buf].modified = false if opts.name then pcall(vim.api.nvim_buf_set_name, buf, opts.name) end return buf end ---@param abs_path string ---@return integer function M.load_file_buf(abs_path) local buf = vim.fn.bufadd(abs_path) vim.fn.bufload(buf) return buf end ---Name a scratch buffer with a `git://...` URI and apply the filetype ---inferred from the inner path segment. The `nvim_buf_set_name` call is ---wrapped in pcall because a buffer with that name may already exist ---(E95). ---@param buf integer ---@param name string function M.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 M.set_buf_name_and_filetype(other, "git://" .. label .. "/" .. rel) local cur_win = vim.api.nvim_get_current_win() local other_win = vim.api.nvim_open_win(other, true, { split = opts.vertical and "left" or "above", win = cur_win, }) vim.wo[other_win].diff = true vim.api.nvim_set_current_win(cur_win) vim.wo[cur_win].diff = true end return M