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 addressed by a git revspec. The ---URI is `git://` and BufReadCmd loads via `git cat-file -p`. ---@param worktree string ---@param revspec string any revspec git understands (e.g. `HEAD:foo`, `:foo`, `:1:foo`, ``, `:foo`) ---@return integer function M.git_show_buf(worktree, revspec) local name = "git://" .. revspec local buf = vim.fn.bufadd(name) vim.b[buf].git_worktree = worktree vim.fn.bufload(buf) return buf end ---BufReadCmd handler for `git://` URIs. Loads content via ---`git cat-file -p `. Worktree comes from `vim.b[buf] ---.git_worktree` if set, else from cwd. Index entries (revspec form ---`:` for stage 0) are made writable via `attach_index_writer`, ---so `:w` updates the index. Other revspecs are read-only. ---@param buf integer function M.read_uri(buf) local name = vim.api.nvim_buf_get_name(buf) local revspec = name:match("^git://(.+)$") if not revspec then return end 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" local stdout = util.exec( { "git", "cat-file", "-p", revspec }, { cwd = worktree } ) if stdout then vim.api.nvim_buf_set_lines(buf, 0, -1, false, util.split_lines(stdout)) end -- Stage-0 index entries (`:` with no further `:`) are -- editable; `:w` rewrites the index entry via `attach_index_writer`. -- Anything else (HEAD:, :, :1:, :2:, :3:, bare object refs) -- is read-only. local index_path = revspec:match("^:([^:]+)$") if index_path 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, index_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 -- Stage 0 (index) is `:`; named refs are `:`. local revspec = opts.ref == "" and (":" .. rel) or (opts.ref .. ":" .. rel) local uri = "git://" .. revspec -- 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