local repo = require("git.repo") local util = require("git.util") local M = {} ---@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 ---Name a buffer and re-run filetype detection from the (re-)set name. ---Wrapped in `pcall` because a buffer with that name may already exist ---(E95). ---@param buf integer ---@param name string local function 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 local function set_diff(win, enabled) vim.api.nvim_win_call(win, function() vim.cmd(enabled and "diffthis" or "diffoff") end) end ---Render two buffers as a diff pair. The right buffer takes the current ---window; the left opens in a leftabove split (vertical or horizontal ---per `vertical`). Both windows enter Vim's diff mode via `:diffsplit`'s ---built-in setup. Drops a `'` jumplist mark before reassigning the ---current window. ---@param left integer ---@param right integer ---@param vertical boolean function M.open(left, right, vertical) -- Read the name first: if `left` is the current window's buffer -- and has `bufhidden=wipe` (a freshly-loaded `git://` URI), the -- `nvim_set_current_buf(right)` below wipes it, and a later name -- lookup would fail. `:diffsplit` re-bufadds + reloads from the -- name, so the wipe-then-recreate sequence is fine. local left_name = vim.api.nvim_buf_get_name(left) vim.cmd.normal({ "m'", bang = true }) vim.api.nvim_set_current_buf(right) local prefix = vertical and "leftabove vert " or "leftabove " vim.cmd(prefix .. "diffsplit " .. vim.fn.fnameescape(left_name)) end ---Repoint two existing diff windows at a new pair of buffers. ---Toggles diff mode off around the swap so Vim tears down the old diff ---group and re-establishes a fresh one. `nvim_win_set_buf` swaps the ---buffer pointer without invalidating cached diff state, and ---`:diffupdate` alone doesn't reliably force a recompute when no buffer ---contents have actually changed. Sides with `name` set get renamed + ---filetype-refreshed (used to relabel a fresh `empty_buf` placeholder ---as `[absent] `, or to re-run ft detection on a `git://` buffer). ---@param left_win integer ---@param right_win integer ---@param pair ow.Git.DiffPair function M.update_pair(left_win, right_win, pair) set_diff(left_win, false) set_diff(right_win, false) vim.api.nvim_win_set_buf(left_win, pair.left.buf) vim.api.nvim_win_set_buf(right_win, pair.right.buf) for _, side in ipairs({ pair.left, pair.right }) do if side.name then set_buf_name_and_filetype(side.buf, side.name) end end set_diff(left_win, true) set_diff(right_win, true) end ---Open two buffers as a diff. `a_left` decides which one goes in the ---leftabove slot (where `M.open` parks the cursor). Caller picks per ---its own rule (writable-on-left for routine flows, anchor-on-left ---when neither side is writable, etc). ---@param buf_a integer ---@param buf_b integer ---@param a_left boolean ---@param vertical boolean local function place_pair(buf_a, buf_b, a_left, vertical) if a_left then M.open(buf_a, buf_b, vertical) else M.open(buf_b, buf_a, vertical) end end ---Dispatch for `M.split` when the current buffer is a `git://` ---URI. Placement is "writable on the left" via `place_pair`. --- ---gd/gh: pair cur with the worktree file at the URI's path. --- ---gD/gH: pair cur with the next layer toward HEAD — --- * stage 0 -> `HEAD:

` --- * stage 2 (ours) <-> stage 3 (theirs) --- * stage 1 (base) -> bail; ambiguous (suggest `:Gdiffsplit `) --- * any other ref -> `:0:

` --- ---A `` containing `:` (from `:Gdiffsplit`) short-circuits all the ---above and pairs cur with that revspec literally. This is the escape ---hatch the merge-base warning points at. ---@param opts ow.Git.SplitOpts ---@param cur_buf integer ---@param cur_revspec string local function uri_split(opts, cur_buf, cur_revspec) local worktree = vim.b[cur_buf].git_worktree or select(2, repo.resolve_cwd()) if not worktree then util.warning("git URI buffer has no worktree") return end local cur = util.parse_revspec(cur_revspec) if not cur.path then util.warning("git URI has no path; cannot diff against worktree") return end -- `git.object` is lazy-required to break the load-time cycle. -- It requires `git.diff` itself for `empty_buf`. local object = require("git.object") local cur_writable = cur.stage == 0 if opts.revspec ~= "" and opts.revspec:find(":", 1, true) then local content = util.exec( { "git", "cat-file", "-p", opts.revspec }, { cwd = worktree, silent = true } ) if not content then util.warning("invalid revspec: %s", opts.revspec) return end place_pair( cur_buf, object.buf_for(worktree, opts.revspec, content), cur_writable, opts.vertical ) return end if opts.revspec == "" then local worktree_buf = vim.fn.bufadd(vim.fs.joinpath(worktree, cur.path)) vim.fn.bufload(worktree_buf) place_pair(cur_buf, worktree_buf, cur_writable, opts.vertical) return end if cur.stage == 1 then util.warning("gD on merge base is ambiguous; use :Gdiffsplit ") return end local other_revspec if cur.stage == 2 then other_revspec = ":3:" .. cur.path elseif cur.stage == 3 then other_revspec = ":2:" .. cur.path elseif cur.stage == 0 then other_revspec = "HEAD:" .. cur.path else other_revspec = ":0:" .. cur.path end local content = util.exec( { "git", "cat-file", "-p", other_revspec }, { cwd = worktree, silent = true } ) if not content then util.warning("invalid revspec: %s", other_revspec) return end place_pair( cur_buf, object.buf_for(worktree, other_revspec, content), cur.stage ~= nil, opts.vertical ) end ---@class ow.Git.SplitOpts ---@field revspec string '' for the smart-default routing (index vs worktree); a plain ref like `'HEAD'` to compare `:` against the current path; or a full revspec containing `:` (e.g. `':2:foo'`, `'HEAD~1:other.lua'`) used as-is. ---@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) local cur_revspec = util.parse_uri(cur_path) if cur_revspec then return uri_split(opts, cur_buf, cur_revspec) end 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 -- A `` containing `:` is treated as a full revspec; -- otherwise the worktree-relative path is appended (the common -- keymap form). local revspec if opts.revspec == "" then revspec = ":0:" .. rel elseif opts.revspec:find(":", 1, true) then revspec = opts.revspec else revspec = opts.revspec .. ":" .. rel end local content = util.exec( { "git", "cat-file", "-p", revspec }, { cwd = worktree, silent = true } ) if not content then util.warning("invalid revspec: %s", revspec) return end local object = require("git.object") local buf = object.buf_for(worktree, revspec, content) local other_writable = util.parse_revspec(revspec).stage == 0 place_pair(buf, cur_buf, other_writable, opts.vertical) end return M