refactor(git): load git:// URI buffers via BufReadCmd
This commit is contained in:
+30
-1
@@ -68,10 +68,39 @@ local function first_positional(args, start)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---Open `<ref>:<path>` in a split via the `git://` BufReadCmd loader.
|
||||||
|
---Resolves to a sha first so the URI stays stable if the ref moves.
|
||||||
|
---@param worktree string
|
||||||
|
---@param user_ref string
|
||||||
|
---@param path string
|
||||||
|
local function show_file_in_split(worktree, user_ref, path)
|
||||||
|
repo.rev_parse(worktree, user_ref, true, function(sha)
|
||||||
|
local label = sha or user_ref
|
||||||
|
local uri = "git://" .. label .. "//" .. path
|
||||||
|
local buf = vim.fn.bufadd(uri)
|
||||||
|
vim.b[buf].git_worktree = worktree
|
||||||
|
vim.cmd("split " .. vim.fn.fnameescape(uri))
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
---@param worktree string
|
---@param worktree string
|
||||||
---@param args string[]
|
---@param args string[]
|
||||||
---@param conf ow.Git.SplitHandler
|
---@param conf ow.Git.SplitHandler
|
||||||
local function run_in_split(worktree, args, conf)
|
local function run_in_split(worktree, args, conf)
|
||||||
|
-- `<ref>:<path>` is a file lookup; the URI must carry the path so
|
||||||
|
-- filetype detection has something to match against.
|
||||||
|
if args[1] == "show" then
|
||||||
|
local arg = first_positional(args, 2)
|
||||||
|
if arg then
|
||||||
|
local ref, path = arg:match("^(.-):(.+)$")
|
||||||
|
if ref then
|
||||||
|
---@cast path -nil
|
||||||
|
show_file_in_split(worktree, ref, path)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
local buf = git.new_scratch()
|
local buf = git.new_scratch()
|
||||||
vim.b[buf].git_worktree = worktree
|
vim.b[buf].git_worktree = worktree
|
||||||
if conf.needs_ref then
|
if conf.needs_ref then
|
||||||
@@ -80,7 +109,7 @@ local function run_in_split(worktree, args, conf)
|
|||||||
if not vim.api.nvim_buf_is_valid(buf) then
|
if not vim.api.nvim_buf_is_valid(buf) then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
pcall(vim.api.nvim_buf_set_name, buf, "git://" .. label .. "/")
|
pcall(vim.api.nvim_buf_set_name, buf, "git://" .. label .. "//")
|
||||||
vim.bo[buf].filetype = conf.ft
|
vim.bo[buf].filetype = conf.ft
|
||||||
end
|
end
|
||||||
repo.rev_parse(worktree, user_ref, true, function(sha)
|
repo.rev_parse(worktree, user_ref, true, function(sha)
|
||||||
|
|||||||
+79
-63
@@ -59,33 +59,58 @@ local function attach_index_writer(buf, worktree, path)
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
---Internal builder: create a scratch buffer and fill it with the output of
|
---Return a buffer holding the content at `<ref>:<path>`. `ref` is
|
||||||
---`git show <revspec>`. Synchronous so the buffer is ready by the time the
|
---"index", "HEAD", or a commit revspec.
|
||||||
---caller wires up windows / `:diffthis`. An empty buffer briefly visible
|
|
||||||
---to the diff engine produces a spurious whole-file diff.
|
|
||||||
---@param worktree string
|
---@param worktree string
|
||||||
---@param revspec string
|
---@param ref string
|
||||||
---@param is_index boolean
|
---@param path string
|
||||||
---@param index_path string? required when is_index is true
|
|
||||||
---@return integer
|
---@return integer
|
||||||
local function build_show_buf(worktree, revspec, is_index, index_path)
|
function M.git_show_buf(worktree, ref, path)
|
||||||
local buf = vim.api.nvim_create_buf(false, true)
|
local name = "git://" .. ref .. "//" .. path
|
||||||
vim.bo[buf].buftype = "nofile"
|
local buf = vim.fn.bufadd(name)
|
||||||
vim.bo[buf].bufhidden = "wipe"
|
vim.b[buf].git_worktree = worktree
|
||||||
vim.bo[buf].swapfile = false
|
vim.fn.bufload(buf)
|
||||||
|
return buf
|
||||||
|
end
|
||||||
|
|
||||||
|
---BufReadCmd handler for `git://<ref>//<path>` URIs. Worktree comes from
|
||||||
|
---`vim.b[buf].git_worktree` if set, else from cwd. Ref of "index" maps
|
||||||
|
---to `git show :<path>`; "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
|
||||||
|
log.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 result = vim.system(
|
local result = vim.system(
|
||||||
{ "git", "show", revspec },
|
{ "git", "show", revspec },
|
||||||
{ cwd = worktree, text = true }
|
{ cwd = worktree, text = true }
|
||||||
)
|
)
|
||||||
:wait()
|
:wait()
|
||||||
if result.code ~= 0 then
|
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(
|
vim.api.nvim_buf_set_lines(
|
||||||
buf,
|
buf,
|
||||||
0,
|
0,
|
||||||
@@ -93,45 +118,51 @@ local function build_show_buf(worktree, revspec, is_index, index_path)
|
|||||||
false,
|
false,
|
||||||
util.split_lines(result.stdout or "")
|
util.split_lines(result.stdout or "")
|
||||||
)
|
)
|
||||||
|
else
|
||||||
|
log.error(
|
||||||
|
"git show %s failed: %s",
|
||||||
|
revspec,
|
||||||
|
vim.trim(result.stderr or "")
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
if is_index then
|
if ref == "index" then
|
||||||
vim.bo[buf].buftype = "acwrite"
|
vim.bo[buf].buftype = "acwrite"
|
||||||
attach_index_writer(buf, worktree, assert(index_path))
|
-- 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
|
else
|
||||||
|
vim.bo[buf].buftype = "nofile"
|
||||||
vim.bo[buf].modifiable = false
|
vim.bo[buf].modifiable = false
|
||||||
end
|
end
|
||||||
vim.bo[buf].modified = false
|
vim.bo[buf].modified = false
|
||||||
return buf
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param worktree string
|
-- BufReadCmd suppresses the normal BufReadPost dispatch, so filetype
|
||||||
---@param ref string '' for index, 'HEAD' or a sha for committed refs
|
-- detection and modeline parsing don't run unless we fire it ourselves.
|
||||||
---@param path string
|
vim.api.nvim_exec_autocmds("BufReadPost", { buffer = buf })
|
||||||
---@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
|
end
|
||||||
|
|
||||||
---@class ow.Git.EmptyBufOpts
|
---@class ow.Git.EmptyBufOpts
|
||||||
---@field name string?
|
---@field name string?
|
||||||
---@field bufhidden ("hide"|"wipe")? defaults to "wipe"
|
---@field bufhidden ("hide"|"wipe")? defaults to "wipe"
|
||||||
|
|
||||||
---Build a read-only scratch buffer, optionally naming it via
|
---Build a read-only scratch buffer, optionally naming it. When `opts.name`
|
||||||
---`nvim_buf_set_name` (silently no-op if a buffer with that name
|
---is set and a loaded buffer with that name already exists, returns it
|
||||||
---already exists).
|
---instead of creating a duplicate.
|
||||||
---@param opts ow.Git.EmptyBufOpts?
|
---@param opts ow.Git.EmptyBufOpts?
|
||||||
---@return integer
|
---@return integer
|
||||||
function M.empty_buf(opts)
|
function M.empty_buf(opts)
|
||||||
opts = opts or {}
|
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)
|
local buf = vim.api.nvim_create_buf(false, true)
|
||||||
vim.bo[buf].buftype = "nofile"
|
vim.bo[buf].buftype = "nofile"
|
||||||
vim.bo[buf].bufhidden = opts.bufhidden or "wipe"
|
vim.bo[buf].bufhidden = opts.bufhidden or "wipe"
|
||||||
@@ -215,30 +246,15 @@ function M.split(opts)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local is_index = opts.ref == ""
|
local label = opts.ref == "" and "index" or opts.ref
|
||||||
local other = M.git_show_buf(worktree, opts.ref, rel, is_index)
|
local uri = "git://" .. label .. "//" .. rel
|
||||||
local label = is_index and "index" or opts.ref
|
-- Stash the worktree on the buffer so the BufReadCmd handler doesn't
|
||||||
M.set_buf_name_and_filetype(other, "git://" .. label .. "/" .. rel)
|
-- fall back to cwd resolution (wrong when cwd != worktree).
|
||||||
|
local buf = vim.fn.bufadd(uri)
|
||||||
|
vim.b[buf].git_worktree = worktree
|
||||||
|
|
||||||
local cur_win = vim.api.nvim_get_current_win()
|
local prefix = opts.vertical and "leftabove vert " or "leftabove "
|
||||||
local other_win = vim.api.nvim_open_win(other, true, {
|
vim.cmd(prefix .. "diffsplit " .. vim.fn.fnameescape(uri))
|
||||||
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
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
+8
-1
@@ -58,12 +58,19 @@ function M.setup()
|
|||||||
end
|
end
|
||||||
vim.filetype.add({
|
vim.filetype.add({
|
||||||
pattern = {
|
pattern = {
|
||||||
["git://[^/]+/(.+)"] = function(_, bufnr, inner)
|
["git://.-//(.+)"] = function(_, bufnr, inner)
|
||||||
return vim.filetype.match({ filename = inner, buf = bufnr })
|
return vim.filetype.match({ filename = inner, buf = bufnr })
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
local group = vim.api.nvim_create_augroup("ow.git", { clear = true })
|
local group = vim.api.nvim_create_augroup("ow.git", { clear = true })
|
||||||
|
vim.api.nvim_create_autocmd("BufReadCmd", {
|
||||||
|
pattern = "git://*",
|
||||||
|
group = group,
|
||||||
|
callback = function(args)
|
||||||
|
require("git.diff").read_uri(args.buf)
|
||||||
|
end,
|
||||||
|
})
|
||||||
vim.api.nvim_create_autocmd(
|
vim.api.nvim_create_autocmd(
|
||||||
{ "BufReadPost", "BufNewFile", "BufWritePost", "FileChangedShellPost" },
|
{ "BufReadPost", "BufNewFile", "BufWritePost", "FileChangedShellPost" },
|
||||||
{
|
{
|
||||||
|
|||||||
+5
-16
@@ -80,30 +80,19 @@ local function is_zero(sha)
|
|||||||
return sha == nil or sha:match("^0+$") ~= nil
|
return sha == nil or sha:match("^0+$") ~= nil
|
||||||
end
|
end
|
||||||
|
|
||||||
---Build a buffer holding the file's content at a given blob, named after the
|
---Buffer for the file at `<ref>:<path>`. A zero/nil blob (file absent on
|
||||||
---commit ref it corresponds to (so the name lines up with `git log` output
|
---this side of the diff) yields an empty placeholder.
|
||||||
---instead of an opaque blob hash).
|
|
||||||
---@param worktree string
|
---@param worktree string
|
||||||
---@param blob string?
|
---@param blob string?
|
||||||
---@param path string
|
---@param path string
|
||||||
---@param ref string the commit ref the blob represents (e.g. `<sha>` or `<sha>^`)
|
---@param ref string the commit ref the blob represents (e.g. `<sha>` or `<sha>^`)
|
||||||
---@return integer
|
---@return integer
|
||||||
local function blob_buf(worktree, blob, path, ref)
|
local function blob_buf(worktree, blob, path, ref)
|
||||||
local name = "git://" .. ref .. "/" .. path
|
local name = "git://" .. ref .. "//" .. path
|
||||||
-- Reuse an existing buffer with this name (commit refs and blobs are
|
|
||||||
-- immutable, so the content is stable); avoids E95 collisions and
|
|
||||||
-- prevents accumulating duplicate buffers as the user navigates.
|
|
||||||
local existing = vim.fn.bufnr(name)
|
|
||||||
if existing ~= -1 and vim.api.nvim_buf_is_loaded(existing) then
|
|
||||||
return existing
|
|
||||||
end
|
|
||||||
if is_zero(blob) then
|
if is_zero(blob) then
|
||||||
return diff.empty_buf({ name = name, bufhidden = "hide" })
|
return diff.empty_buf({ name = name, bufhidden = "hide" })
|
||||||
end
|
end
|
||||||
---@cast blob string
|
return diff.git_show_buf(worktree, ref, path)
|
||||||
local buf = diff.git_show_blob(worktree, blob)
|
|
||||||
diff.set_buf_name_and_filetype(buf, name)
|
|
||||||
return buf
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param worktree string
|
---@param worktree string
|
||||||
@@ -143,7 +132,7 @@ end
|
|||||||
function M.open_commit(worktree, ref)
|
function M.open_commit(worktree, ref)
|
||||||
repo.rev_parse(worktree, ref, true, function(resolved)
|
repo.rev_parse(worktree, ref, true, function(resolved)
|
||||||
local sha = resolved or ref
|
local sha = resolved or ref
|
||||||
local name = "git://" .. sha .. "/"
|
local name = "git://" .. sha .. "//"
|
||||||
-- Reuse a previously-opened buffer for the same commit; commit SHAs
|
-- Reuse a previously-opened buffer for the same commit; commit SHAs
|
||||||
-- are immutable so the content is stable.
|
-- are immutable so the content is stable.
|
||||||
local existing = vim.fn.bufnr(name)
|
local existing = vim.fn.bufnr(name)
|
||||||
|
|||||||
+5
-14
@@ -485,14 +485,14 @@ end
|
|||||||
local function head_pane(worktree, path)
|
local function head_pane(worktree, path)
|
||||||
return {
|
return {
|
||||||
buf = diff.git_show_buf(worktree, "HEAD", path),
|
buf = diff.git_show_buf(worktree, "HEAD", path),
|
||||||
name = "git://HEAD/" .. path,
|
name = "git://HEAD//" .. path,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param path string
|
---@param path string
|
||||||
---@return ow.Git.DiffSide
|
---@return ow.Git.DiffSide
|
||||||
local function head_empty_pane(path)
|
local function head_empty_pane(path)
|
||||||
return { buf = diff.empty_buf(), name = "git://HEAD/" .. path }
|
return { buf = diff.empty_buf(), name = "git://HEAD//" .. path }
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param worktree string
|
---@param worktree string
|
||||||
@@ -508,7 +508,7 @@ end
|
|||||||
---@param path string
|
---@param path string
|
||||||
---@return ow.Git.DiffSide
|
---@return ow.Git.DiffSide
|
||||||
local function worktree_empty_pane(path)
|
local function worktree_empty_pane(path)
|
||||||
return { buf = diff.empty_buf(), name = "git://worktree/" .. path }
|
return { buf = diff.empty_buf(), name = "git://worktree//" .. path }
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param s ow.Git.SidebarState
|
---@param s ow.Git.SidebarState
|
||||||
@@ -520,9 +520,9 @@ local function index_pane(s, entry)
|
|||||||
or (entry.section == "Staged" and entry.x == "D")
|
or (entry.section == "Staged" and entry.x == "D")
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
buf = in_index and diff.git_show_buf(s.worktree, "", entry.path, true)
|
buf = in_index and diff.git_show_buf(s.worktree, "index", entry.path)
|
||||||
or diff.empty_buf(),
|
or diff.empty_buf(),
|
||||||
name = "git://index/" .. entry.path,
|
name = "git://index//" .. entry.path,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -727,15 +727,6 @@ local function show_diff(s, entry, focus_left)
|
|||||||
diff.set_buf_name_and_filetype(side.buf, side.name)
|
diff.set_buf_name_and_filetype(side.buf, side.name)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
-- Synthetic index/HEAD buffers never run BufRead, so modeline-only
|
|
||||||
-- filetypes aren't detected. Copy from the side that did resolve.
|
|
||||||
local left_ft = vim.bo[pair.left.buf].filetype
|
|
||||||
local right_ft = vim.bo[pair.right.buf].filetype
|
|
||||||
if left_ft == "" and right_ft ~= "" then
|
|
||||||
vim.bo[pair.left.buf].filetype = right_ft
|
|
||||||
elseif right_ft == "" and left_ft ~= "" then
|
|
||||||
vim.bo[pair.right.buf].filetype = left_ft
|
|
||||||
end
|
|
||||||
diff.set_diff(left_win, true)
|
diff.set_diff(left_win, true)
|
||||||
diff.set_diff(right_win, true)
|
diff.set_diff(right_win, true)
|
||||||
s.last_shown_key = key
|
s.last_shown_key = key
|
||||||
|
|||||||
Reference in New Issue
Block a user