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 ---Internal builder: create a scratch buffer and fill it with the output of ---`git show `. Synchronous so the buffer is ready by the time the ---caller wires up windows / `:diffthis`. An empty buffer briefly visible ---to the diff engine produces a spurious whole-file diff. ---@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 local result = vim.system( { "git", "show", revspec }, { cwd = worktree, text = true } ) :wait() if result.code ~= 0 then log.error( "git show %s failed: %s", revspec, vim.trim(result.stderr or "") ) else vim.api.nvim_buf_set_lines( buf, 0, -1, false, util.split_lines(result.stdout or "") ) end 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 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 ---Apply (or remove) the window-local options that `:diffthis` would ---normally set. Used by both `M.split` here and the sidebar so the two ---diff entry points behave consistently. Vim is supposed to save and ---restore these around the `'diff'` flag flip, but that round-trip is ---fragile when buffers are swapped under an already-diff window. ---@param win integer ---@param enabled boolean function M.set_diff(win, enabled) vim.wo[win].diff = enabled if enabled then vim.wo[win].foldmethod = "diff" vim.wo[win].foldenable = true vim.wo[win].foldlevel = 0 vim.wo[win].scrollbind = true vim.wo[win].cursorbind = true vim.wo[win].wrap = false else vim.wo[win].scrollbind = false vim.wo[win].cursorbind = false 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 if vim.bo[cur_buf].buftype ~= "" then log.warning("cannot diff this buffer (not a worktree file)") 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, }) -- The synthetic index/HEAD buffer can't run BufRead, so its filetype -- detection in `set_buf_name_and_filetype` only catches -- filename-pattern matches. Mirror cur_buf's filetype, since this is -- the same logical file at a different version. Done after the -- window opens so any BufWinEnter / BufEnter autocmds that fire on -- nvim_open_win can't undo it. local cur_ft = vim.bo[cur_buf].filetype if cur_ft ~= "" then vim.bo[other].filetype = cur_ft end M.set_diff(cur_win, true) M.set_diff(other_win, true) end return M