local repo = require("git.repo") local util = require("git.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_stdout = util.exec( { "git", "hash-object", "-w", "--stdin" }, { cwd = worktree, stdin = body } ) if not hash_stdout then return end local sha = vim.trim(hash_stdout) local mode = vim.b[buf].git_index_mode if not mode then mode = "100644" local ls = util.exec( { "git", "ls-files", "-s", "--", path }, { cwd = worktree, silent = true } ) if ls then local m = ls: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. if not util.exec({ "git", "update-index", "--cacheinfo", mode, sha, path, }, { cwd = worktree }) then return end vim.bo[buf].modified = false end, }) end ---Return a buffer holding the content at `:`. `ref` is ---"index", "HEAD", or a commit revspec. ---@param worktree string ---@param ref string ---@param path string ---@return integer function M.git_show_buf(worktree, ref, path) local name = "git://" .. ref .. "//" .. path local buf = vim.fn.bufadd(name) vim.b[buf].git_worktree = worktree vim.fn.bufload(buf) return buf end ---BufReadCmd handler for `git:////` URIs. Worktree comes from ---`vim.b[buf].git_worktree` if set, else from cwd. Ref of "index" maps ---to `git show :`; "worktree" leaves the buffer empty (placeholder ---for missing files); anything else is a revspec. ---@param buf integer function M.read_uri(buf) local name = vim.api.nvim_buf_get_name(buf) local ref, path = name:match("^git://(.-)//(.*)$") if not ref or path == "" then return end ---@cast path -nil local worktree = vim.b[buf].git_worktree or select(2, repo.resolve_cwd()) if not worktree then util.error("git BufReadCmd %s: cannot resolve worktree", name) return end vim.b[buf].git_worktree = worktree vim.bo[buf].swapfile = false vim.bo[buf].bufhidden = "wipe" if ref == "worktree" then vim.bo[buf].buftype = "nofile" vim.bo[buf].modifiable = false vim.bo[buf].modified = false vim.api.nvim_exec_autocmds("BufReadPost", { buffer = buf }) return end local revspec = ref == "index" and (":" .. path) or (ref .. ":" .. path) local stdout = util.exec({ "git", "show", revspec }, { cwd = worktree }) if stdout then vim.api.nvim_buf_set_lines(buf, 0, -1, false, util.split_lines(stdout)) end if ref == "index" then vim.bo[buf].buftype = "acwrite" -- Re-running BufReadCmd (e.g. on `:edit`) would otherwise stack -- another BufWriteCmd on the same buffer, so each `:w` runs -- hash-object + update-index N times. if not vim.b[buf].git_index_writer then attach_index_writer(buf, worktree, path) vim.b[buf].git_index_writer = true end else vim.bo[buf].buftype = "nofile" vim.bo[buf].modifiable = false end vim.bo[buf].modified = false -- BufReadCmd suppresses the normal BufReadPost dispatch, so filetype -- detection and modeline parsing don't run unless we fire it ourselves. vim.api.nvim_exec_autocmds("BufReadPost", { buffer = buf }) end ---@class ow.Git.EmptyBufOpts ---@field name string? ---@field bufhidden ("hide"|"wipe")? defaults to "wipe" ---Build a read-only scratch buffer, optionally naming it. When `opts.name` ---is set and a loaded buffer with that name already exists, returns it ---instead of creating a duplicate. ---@param opts ow.Git.EmptyBufOpts? ---@return integer function M.empty_buf(opts) opts = opts or {} if opts.name then local existing = vim.fn.bufnr(opts.name) if existing ~= -1 and vim.api.nvim_buf_is_loaded(existing) then return existing end end 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 ---Toggle a window into or out of Vim's diff mode. Goes through the ---`:diffthis` / `:diffoff` commands rather than `vim.wo[win].diff = X`. ---The command path runs Vim's full `diff_win_options` setup, which sets ---an internal flag that prevents subsequently-created floats from ---inheriting `'diff' = 1` when opened from a focused diff pane. The raw ---option setter skips that setup, so floats (oil, fzf-lua, etc) end up ---joining the tabpage's diff group and corrupting its render. ---@param win integer ---@param enabled boolean function M.set_diff(win, enabled) vim.api.nvim_win_call(win, function() vim.cmd(enabled and "diffthis" or "diffoff") 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 util.warning("no file in current buffer") return end if vim.bo[cur_buf].buftype ~= "" then util.warning("cannot diff this buffer (not a worktree file)") return end local _, worktree = repo.resolve(cur_path) if not worktree then util.warning("not in a git repository") return end local rel = vim.fs.relpath(worktree, cur_path) if not rel then util.warning("file is outside the worktree") return end local label = opts.ref == "" and "index" or opts.ref local uri = "git://" .. label .. "//" .. rel -- Stash the worktree on the buffer so the BufReadCmd handler doesn't -- fall back to cwd resolution (wrong when cwd != worktree). local buf = vim.fn.bufadd(uri) vim.b[buf].git_worktree = worktree local prefix = opts.vertical and "leftabove vert " or "leftabove " vim.cmd(prefix .. "diffsplit " .. vim.fn.fnameescape(uri)) end return M