refactor(git): unify diff/object dispatch, codify naming, add :Gdiffsplit
This commit is contained in:
+79
-88
@@ -1,4 +1,5 @@
|
|||||||
local git = require("git")
|
local commit = require("git.commit")
|
||||||
|
local object = require("git.object")
|
||||||
local repo = require("git.repo")
|
local repo = require("git.repo")
|
||||||
local util = require("git.util")
|
local util = require("git.util")
|
||||||
|
|
||||||
@@ -8,11 +9,12 @@ local M = {}
|
|||||||
---@field ft string
|
---@field ft string
|
||||||
---@field needs_ref boolean?
|
---@field needs_ref boolean?
|
||||||
|
|
||||||
|
---Subcommands whose output goes to a buffer. Subcommands with their
|
||||||
|
---own dispatch (`commit`, `show`, `cat-file`) call `run_in_split`
|
||||||
|
---directly with a one-off conf.
|
||||||
---@type table<string, ow.Git.SplitHandler>
|
---@type table<string, ow.Git.SplitHandler>
|
||||||
local SPLIT_HANDLERS = {
|
local SPLIT_HANDLERS = {
|
||||||
log = { ft = "git" },
|
log = { ft = "git" },
|
||||||
show = { ft = "git", needs_ref = true },
|
|
||||||
["cat-file"] = { ft = "git", needs_ref = true },
|
|
||||||
diff = { ft = "diff" },
|
diff = { ft = "diff" },
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,109 +69,70 @@ local function first_positional(args, start)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
---Open `<ref>:<path>` in a split via the `git://` BufReadCmd loader.
|
---Find or create the named scratch buffer and place it in a window.
|
||||||
---Resolves to a sha first so the URI stays stable if the ref moves.
|
---@param name string
|
||||||
---@param worktree string
|
---@return integer buf
|
||||||
---@param user_ref string
|
local function place_split(name)
|
||||||
---@param path string
|
local buf = vim.fn.bufnr("\\V" .. name)
|
||||||
local function show_file_in_split(worktree, user_ref, path)
|
if buf == -1 or not vim.api.nvim_buf_is_loaded(buf) then
|
||||||
local label = repo.rev_parse(worktree, user_ref, true) or user_ref
|
buf = util.new_scratch()
|
||||||
local uri = "git://" .. label .. ":" .. path
|
pcall(vim.api.nvim_buf_set_name, buf, name)
|
||||||
local buf = vim.fn.bufadd(uri)
|
return buf
|
||||||
vim.b[buf].git_worktree = worktree
|
end
|
||||||
vim.cmd("split " .. vim.fn.fnameescape(uri))
|
local win_id = vim.fn.bufwinid(buf)
|
||||||
|
if win_id ~= -1 then
|
||||||
|
vim.api.nvim_set_current_win(win_id)
|
||||||
|
else
|
||||||
|
vim.api.nvim_open_win(buf, true, {
|
||||||
|
split = vim.o.splitbelow and "below" or "above",
|
||||||
|
})
|
||||||
|
end
|
||||||
|
return buf
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---Run `git <args>` async. On success, drop the output into a named
|
||||||
|
---scratch split (creating or reusing as needed). On failure, `util.exec`
|
||||||
|
---notifies and the split is never opened, so a bad ref doesn't leave a
|
||||||
|
---stray buffer behind.
|
||||||
---@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
|
|
||||||
|
|
||||||
-- `cat-file -p <sha>` routes to the gitobject viewer so commits get
|
|
||||||
-- the full message + diff view and other types render via cat-file.
|
|
||||||
-- Other modes (-t, -s, -e) fall through to the generic dump.
|
|
||||||
if args[1] == "cat-file" and vim.list_contains(args, "-p") then
|
|
||||||
local ref = first_positional(args, 2)
|
|
||||||
if ref then
|
|
||||||
require("git.object").open_object(worktree, ref)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local name = "[git " .. table.concat(args, " ") .. "]"
|
|
||||||
local buf = vim.fn.bufnr("\\V" .. name)
|
|
||||||
if buf == -1 or not vim.api.nvim_buf_is_loaded(buf) then
|
|
||||||
buf = git.new_scratch()
|
|
||||||
pcall(vim.api.nvim_buf_set_name, buf, name)
|
|
||||||
else
|
|
||||||
local win_id = vim.fn.bufwinid(buf)
|
|
||||||
if win_id ~= -1 then
|
|
||||||
vim.api.nvim_set_current_win(win_id)
|
|
||||||
else
|
|
||||||
vim.api.nvim_open_win(buf, true, {
|
|
||||||
split = vim.o.splitbelow and "below" or "above",
|
|
||||||
})
|
|
||||||
end
|
|
||||||
vim.bo[buf].modifiable = true
|
|
||||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, {})
|
|
||||||
end
|
|
||||||
|
|
||||||
vim.b[buf].git_worktree = worktree
|
|
||||||
vim.b[buf].git_ref = nil
|
|
||||||
vim.b[buf].git_parent_ref = nil
|
|
||||||
if conf.needs_ref then
|
|
||||||
local user_ref = first_positional(args, 2) or "HEAD"
|
|
||||||
local sha = repo.rev_parse(worktree, user_ref, true)
|
|
||||||
if sha then
|
|
||||||
vim.b[buf].git_ref = sha
|
|
||||||
vim.b[buf].git_parent_ref =
|
|
||||||
repo.rev_parse(worktree, user_ref .. "^", true)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
vim.bo[buf].filetype = conf.ft
|
|
||||||
|
|
||||||
local cmd = { "git" }
|
local cmd = { "git" }
|
||||||
vim.list_extend(cmd, args)
|
vim.list_extend(cmd, args)
|
||||||
vim.system(
|
util.exec(cmd, {
|
||||||
cmd,
|
cwd = worktree,
|
||||||
{ cwd = worktree, text = true },
|
on_done = function(stdout)
|
||||||
vim.schedule_wrap(function(obj)
|
if not stdout then
|
||||||
if not vim.api.nvim_buf_is_valid(buf) then
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local content = (obj.stdout or "") .. (obj.stderr or "")
|
local name = "[git " .. table.concat(args, " ") .. "]"
|
||||||
|
local buf = place_split(name)
|
||||||
|
vim.b[buf].git_worktree = worktree
|
||||||
|
vim.b[buf].git_ref = nil
|
||||||
|
vim.b[buf].git_parent_ref = nil
|
||||||
|
if conf.needs_ref then
|
||||||
|
local user_ref = first_positional(args, 2) or "HEAD"
|
||||||
|
local sha = repo.rev_parse(worktree, user_ref, true)
|
||||||
|
if sha then
|
||||||
|
vim.b[buf].git_ref = sha
|
||||||
|
vim.b[buf].git_parent_ref =
|
||||||
|
repo.rev_parse(worktree, user_ref .. "^", true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
vim.bo[buf].filetype = conf.ft
|
||||||
vim.bo[buf].modifiable = true
|
vim.bo[buf].modifiable = true
|
||||||
vim.api.nvim_buf_set_lines(
|
vim.api.nvim_buf_set_lines(
|
||||||
buf,
|
buf,
|
||||||
0,
|
0,
|
||||||
-1,
|
-1,
|
||||||
false,
|
false,
|
||||||
util.split_lines(content)
|
util.split_lines(stdout)
|
||||||
)
|
)
|
||||||
vim.bo[buf].modifiable = false
|
vim.bo[buf].modifiable = false
|
||||||
vim.bo[buf].modified = false
|
vim.bo[buf].modified = false
|
||||||
if obj.code ~= 0 then
|
end,
|
||||||
util.error(
|
})
|
||||||
"git %s failed: %s",
|
|
||||||
args[1] or "",
|
|
||||||
vim.trim(obj.stderr or "")
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param worktree string
|
---@param worktree string
|
||||||
@@ -244,7 +207,35 @@ function M.run(args)
|
|||||||
|
|
||||||
local sub = args[1]
|
local sub = args[1]
|
||||||
if sub == "commit" and not has_message(args) then
|
if sub == "commit" and not has_message(args) then
|
||||||
require("git.commit").commit({ amend = has_flag(args, "--amend") })
|
commit.commit({ amend = has_flag(args, "--amend") })
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- `:G show <ref>:<path>` opens the blob via the BufReadCmd loader
|
||||||
|
-- so the URI carries the path and filetype detection has something
|
||||||
|
-- to match against. Other show invocations dump output to a buffer.
|
||||||
|
if sub == "show" then
|
||||||
|
local arg = first_positional(args, 2)
|
||||||
|
if arg and arg:find(":", 1, true) then
|
||||||
|
object.open_object(worktree, arg)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
run_in_split(worktree, args, { ft = "git", needs_ref = true })
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- `:G cat-file -p <sha>` routes to the gitobject viewer so commits
|
||||||
|
-- get the full message + diff view and other types render via
|
||||||
|
-- cat-file. Other modes (-t, -s, -e) dump to a buffer.
|
||||||
|
if sub == "cat-file" then
|
||||||
|
if vim.list_contains(args, "-p") then
|
||||||
|
local ref = first_positional(args, 2)
|
||||||
|
if ref then
|
||||||
|
object.open_object(worktree, ref)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
run_in_split(worktree, args, { ft = "git", needs_ref = true })
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+1
-2
@@ -1,5 +1,4 @@
|
|||||||
local editor = require("git.editor")
|
local editor = require("git.editor")
|
||||||
local git = require("git")
|
|
||||||
local repo = require("git.repo")
|
local repo = require("git.repo")
|
||||||
local util = require("git.util")
|
local util = require("git.util")
|
||||||
|
|
||||||
@@ -30,7 +29,7 @@ function M.commit(opts)
|
|||||||
f:close()
|
f:close()
|
||||||
end
|
end
|
||||||
|
|
||||||
local buf, win = git.new_scratch({ name = file_path })
|
local buf, win = util.new_scratch({ name = file_path })
|
||||||
proxy_buf = buf
|
proxy_buf = buf
|
||||||
proxy_win = win
|
proxy_win = win
|
||||||
vim.bo[buf].buftype = "acwrite"
|
vim.bo[buf].buftype = "acwrite"
|
||||||
|
|||||||
+173
-145
@@ -3,129 +3,6 @@ local util = require("git.util")
|
|||||||
|
|
||||||
local M = {}
|
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://<revspec>` 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`, `<sha>`, `<sha>: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://<revspec>` URIs. Loads content via
|
|
||||||
---`git cat-file -p <revspec>`. Worktree comes from `vim.b[buf]
|
|
||||||
---.git_worktree` if set, else from cwd. Index entries (revspec form
|
|
||||||
---`:<path>` 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 (`:<path>` with no further `:`) are
|
|
||||||
-- editable; `:w` rewrites the index entry via `attach_index_writer`.
|
|
||||||
-- Anything else (HEAD:, <sha>:, :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
|
---@class ow.Git.EmptyBufOpts
|
||||||
---@field name string?
|
---@field name string?
|
||||||
---@field bufhidden ("hide"|"wipe")? defaults to "wipe"
|
---@field bufhidden ("hide"|"wipe")? defaults to "wipe"
|
||||||
@@ -155,21 +32,12 @@ function M.empty_buf(opts)
|
|||||||
return buf
|
return buf
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param abs_path string
|
---Name a buffer and re-run filetype detection from the (re-)set name.
|
||||||
---@return integer
|
---Wrapped in `pcall` because a buffer with that name may already exist
|
||||||
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).
|
---(E95).
|
||||||
---@param buf integer
|
---@param buf integer
|
||||||
---@param name string
|
---@param name string
|
||||||
function M.set_buf_name_and_filetype(buf, name)
|
local function set_buf_name_and_filetype(buf, name)
|
||||||
pcall(vim.api.nvim_buf_set_name, buf, name)
|
pcall(vim.api.nvim_buf_set_name, buf, name)
|
||||||
local ft = vim.filetype.match({ buf = buf })
|
local ft = vim.filetype.match({ buf = buf })
|
||||||
if ft then
|
if ft then
|
||||||
@@ -186,20 +54,170 @@ end
|
|||||||
---joining the tabpage's diff group and corrupting its render.
|
---joining the tabpage's diff group and corrupting its render.
|
||||||
---@param win integer
|
---@param win integer
|
||||||
---@param enabled boolean
|
---@param enabled boolean
|
||||||
function M.set_diff(win, enabled)
|
local function set_diff(win, enabled)
|
||||||
vim.api.nvim_win_call(win, function()
|
vim.api.nvim_win_call(win, function()
|
||||||
vim.cmd(enabled and "diffthis" or "diffoff")
|
vim.cmd(enabled and "diffthis" or "diffoff")
|
||||||
end)
|
end)
|
||||||
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] <abs>`, 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://<revspec>`
|
||||||
|
---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:<p>`
|
||||||
|
--- * stage 2 (ours) <-> stage 3 (theirs)
|
||||||
|
--- * stage 1 (base) -> bail; ambiguous (suggest `:Gdiffsplit <ref>`)
|
||||||
|
--- * any other ref -> `:0:<p>`
|
||||||
|
---
|
||||||
|
---A `<ref>` 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
|
||||||
|
if not repo.object_exists(worktree, opts.revspec) then
|
||||||
|
util.warning("invalid revspec: %s", opts.revspec)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
place_pair(
|
||||||
|
cur_buf,
|
||||||
|
object.buf_for(worktree, opts.revspec),
|
||||||
|
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 <ref>")
|
||||||
|
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
|
||||||
|
if not repo.object_exists(worktree, other_revspec) then
|
||||||
|
util.warning("invalid revspec: %s", other_revspec)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
place_pair(
|
||||||
|
cur_buf,
|
||||||
|
object.buf_for(worktree, other_revspec),
|
||||||
|
cur.stage ~= nil,
|
||||||
|
opts.vertical
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
---@class ow.Git.SplitOpts
|
---@class ow.Git.SplitOpts
|
||||||
---@field ref string '' for index, 'HEAD' for HEAD
|
---@field revspec string '' for the smart-default routing (index vs worktree); a plain ref like `'HEAD'` to compare `<ref>:<rel>` against the current path; or a full revspec containing `:` (e.g. `':2:foo'`, `'HEAD~1:other.lua'`) used as-is.
|
||||||
---@field vertical boolean
|
---@field vertical boolean
|
||||||
|
|
||||||
---@param opts ow.Git.SplitOpts
|
---@param opts ow.Git.SplitOpts
|
||||||
function M.split(opts)
|
function M.split(opts)
|
||||||
local cur_buf = vim.api.nvim_get_current_buf()
|
local cur_buf = vim.api.nvim_get_current_buf()
|
||||||
local cur_path = vim.api.nvim_buf_get_name(cur_buf)
|
local cur_path = vim.api.nvim_buf_get_name(cur_buf)
|
||||||
|
|
||||||
|
local cur_revspec = cur_path:match("^git://(.+)$")
|
||||||
|
if cur_revspec then
|
||||||
|
return uri_split(opts, cur_buf, cur_revspec)
|
||||||
|
end
|
||||||
|
|
||||||
if cur_path == "" then
|
if cur_path == "" then
|
||||||
util.warning("no file in current buffer")
|
util.warning("no file in current buffer")
|
||||||
return
|
return
|
||||||
@@ -219,16 +237,26 @@ function M.split(opts)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Stage 0 (index) is `:<path>`; named refs are `<ref>:<path>`.
|
-- A `<revspec>` containing `:` is treated as a full revspec;
|
||||||
local revspec = opts.ref == "" and (":" .. rel) or (opts.ref .. ":" .. rel)
|
-- otherwise the worktree-relative path is appended (the common
|
||||||
local uri = "git://" .. revspec
|
-- keymap form).
|
||||||
-- Stash the worktree on the buffer so the BufReadCmd handler doesn't
|
local revspec
|
||||||
-- fall back to cwd resolution (wrong when cwd != worktree).
|
if opts.revspec == "" then
|
||||||
local buf = vim.fn.bufadd(uri)
|
revspec = ":0:" .. rel
|
||||||
|
elseif opts.revspec:find(":", 1, true) then
|
||||||
|
revspec = opts.revspec
|
||||||
|
else
|
||||||
|
revspec = opts.revspec .. ":" .. rel
|
||||||
|
end
|
||||||
|
if not repo.object_exists(worktree, revspec) then
|
||||||
|
util.warning("invalid revspec: %s", revspec)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local buf = vim.fn.bufadd("git://" .. revspec)
|
||||||
vim.b[buf].git_worktree = worktree
|
vim.b[buf].git_worktree = worktree
|
||||||
|
|
||||||
local prefix = opts.vertical and "leftabove vert " or "leftabove "
|
local other_writable = util.parse_revspec(revspec).stage == 0
|
||||||
vim.cmd(prefix .. "diffsplit " .. vim.fn.fnameescape(uri))
|
place_pair(buf, cur_buf, other_writable, opts.vertical)
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
+42
-47
@@ -1,4 +1,10 @@
|
|||||||
|
local cmd = require("git.cmd")
|
||||||
|
local commit = require("git.commit")
|
||||||
|
local diff = require("git.diff")
|
||||||
|
local log = require("git.log")
|
||||||
|
local object = require("git.object")
|
||||||
local repo = require("git.repo")
|
local repo = require("git.repo")
|
||||||
|
local sidebar = require("git.sidebar")
|
||||||
|
|
||||||
local HIGHLIGHTS = {
|
local HIGHLIGHTS = {
|
||||||
GitDeleted = "Removed",
|
GitDeleted = "Removed",
|
||||||
@@ -25,38 +31,6 @@ function M.head(path)
|
|||||||
return repo.head(path)
|
return repo.head(path)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@class ow.Git.NewScratchOpts
|
|
||||||
---@field name string?
|
|
||||||
---@field bufhidden ("hide"|"wipe")? defaults to "hide"
|
|
||||||
---@field split (false|"above"|"below"|"left"|"right")? defaults to splitbelow-aware horizontal. `false` places the buffer in the current window (drops a `'` mark first so the user can jump back).
|
|
||||||
|
|
||||||
---Create a fresh non-modifiable scratch buffer and place it. Default split
|
|
||||||
---direction is horizontal, honouring `splitbelow`. Caller flips
|
|
||||||
---`modifiable`, fills the buffer, and sets `filetype` once content lands.
|
|
||||||
---@param opts ow.Git.NewScratchOpts?
|
|
||||||
---@return integer buf
|
|
||||||
---@return integer win
|
|
||||||
function M.new_scratch(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 "hide"
|
|
||||||
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
|
|
||||||
if opts.split == false then
|
|
||||||
vim.cmd.normal({ "m'", bang = true })
|
|
||||||
vim.api.nvim_set_current_buf(buf)
|
|
||||||
return buf, vim.api.nvim_get_current_win()
|
|
||||||
end
|
|
||||||
local split = opts.split or (vim.o.splitbelow and "below" or "above")
|
|
||||||
local win = vim.api.nvim_open_win(buf, true, { split = split })
|
|
||||||
return buf, win
|
|
||||||
end
|
|
||||||
|
|
||||||
function M.setup()
|
function M.setup()
|
||||||
for name, link in pairs(HIGHLIGHTS) do
|
for name, link in pairs(HIGHLIGHTS) do
|
||||||
vim.api.nvim_set_hl(0, name, { link = link, default = true })
|
vim.api.nvim_set_hl(0, name, { link = link, default = true })
|
||||||
@@ -73,7 +47,7 @@ function M.setup()
|
|||||||
pattern = "git://*",
|
pattern = "git://*",
|
||||||
group = group,
|
group = group,
|
||||||
callback = function(args)
|
callback = function(args)
|
||||||
require("git.diff").read_uri(args.buf)
|
object.read_uri(args.buf)
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
vim.api.nvim_create_autocmd(
|
vim.api.nvim_create_autocmd(
|
||||||
@@ -104,34 +78,55 @@ function M.setup()
|
|||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
vim.keymap.set("n", "<leader>gg", function()
|
vim.keymap.set(
|
||||||
require("git.sidebar").toggle()
|
"n",
|
||||||
end, { desc = "Toggle git status sidebar" })
|
"<leader>gg",
|
||||||
vim.keymap.set("n", "<leader>gl", function()
|
sidebar.toggle,
|
||||||
require("git.log").show()
|
{ desc = "Toggle git status sidebar" }
|
||||||
end, { desc = "Show git log" })
|
)
|
||||||
|
vim.keymap.set("n", "<leader>gl", log.show, { desc = "Show git log" })
|
||||||
vim.keymap.set("n", "<leader>gd", function()
|
vim.keymap.set("n", "<leader>gd", function()
|
||||||
require("git.diff").split({ ref = "", vertical = true })
|
diff.split({ revspec = "", vertical = true })
|
||||||
end, { desc = "Diff index vs worktree (vsplit)" })
|
end, { desc = "Diff index vs worktree (vsplit)" })
|
||||||
vim.keymap.set("n", "<leader>gD", function()
|
vim.keymap.set("n", "<leader>gD", function()
|
||||||
require("git.diff").split({ ref = "HEAD", vertical = true })
|
diff.split({ revspec = "HEAD", vertical = true })
|
||||||
end, { desc = "Diff HEAD vs worktree (vsplit)" })
|
end, { desc = "Diff HEAD vs worktree (vsplit)" })
|
||||||
vim.keymap.set("n", "<leader>gh", function()
|
vim.keymap.set("n", "<leader>gh", function()
|
||||||
require("git.diff").split({ ref = "", vertical = false })
|
diff.split({ revspec = "", vertical = false })
|
||||||
end, { desc = "Diff index vs worktree (split)" })
|
end, { desc = "Diff index vs worktree (split)" })
|
||||||
vim.keymap.set("n", "<leader>gH", function()
|
vim.keymap.set("n", "<leader>gH", function()
|
||||||
require("git.diff").split({ ref = "HEAD", vertical = false })
|
diff.split({ revspec = "HEAD", vertical = false })
|
||||||
end, { desc = "Diff HEAD vs worktree (split)" })
|
end, { desc = "Diff HEAD vs worktree (split)" })
|
||||||
vim.keymap.set("n", "<leader>gc", function()
|
vim.keymap.set("n", "<leader>gc", function()
|
||||||
require("git.commit").commit()
|
commit.commit()
|
||||||
end, { desc = "Git commit" })
|
end, { desc = "Git commit" })
|
||||||
vim.keymap.set("n", "<leader>ga", function()
|
vim.keymap.set("n", "<leader>ga", function()
|
||||||
require("git.commit").commit({ amend = true })
|
commit.commit({ amend = true })
|
||||||
end, { desc = "Git commit --amend" })
|
end, { desc = "Git commit --amend" })
|
||||||
vim.keymap.set("n", "<leader>gp", function()
|
vim.keymap.set("n", "<leader>gp", function()
|
||||||
require("git.cmd").run({ "push" })
|
cmd.run({ "push" })
|
||||||
end, { desc = "Git push" })
|
end, { desc = "Git push" })
|
||||||
require("git.cmd").setup()
|
|
||||||
|
local function diff_split_cmd(vertical)
|
||||||
|
return function(opts)
|
||||||
|
diff.split({
|
||||||
|
revspec = opts.args,
|
||||||
|
vertical = vertical,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
vim.api.nvim_create_user_command(
|
||||||
|
"Gdiffsplit",
|
||||||
|
diff_split_cmd(true),
|
||||||
|
{ nargs = "?", desc = "Diff against <revspec> (vsplit)" }
|
||||||
|
)
|
||||||
|
vim.api.nvim_create_user_command(
|
||||||
|
"Ghdiffsplit",
|
||||||
|
diff_split_cmd(false),
|
||||||
|
{ nargs = "?", desc = "Diff against <revspec> (split)" }
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd.setup()
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
+1
-2
@@ -1,4 +1,3 @@
|
|||||||
local git = require("git")
|
|
||||||
local repo = require("git.repo")
|
local repo = require("git.repo")
|
||||||
local util = require("git.util")
|
local util = require("git.util")
|
||||||
|
|
||||||
@@ -42,7 +41,7 @@ function M.show(opts)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local buf = git.new_scratch()
|
local buf = util.new_scratch()
|
||||||
vim.b[buf].git_worktree = worktree
|
vim.b[buf].git_worktree = worktree
|
||||||
vim.bo[buf].modifiable = true
|
vim.bo[buf].modifiable = true
|
||||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, util.split_lines(stdout))
|
vim.api.nvim_buf_set_lines(buf, 0, -1, false, util.split_lines(stdout))
|
||||||
|
|||||||
+198
-74
@@ -1,5 +1,4 @@
|
|||||||
local diff = require("git.diff")
|
local diff = require("git.diff")
|
||||||
local git = require("git")
|
|
||||||
local repo = require("git.repo")
|
local repo = require("git.repo")
|
||||||
local util = require("git.util")
|
local util = require("git.util")
|
||||||
|
|
||||||
@@ -11,12 +10,12 @@ local M = {}
|
|||||||
---@field pre_blob string?
|
---@field pre_blob string?
|
||||||
---@field post_blob string?
|
---@field post_blob string?
|
||||||
|
|
||||||
---@class ow.Git.ShowContext
|
---@class ow.Git.BufContext
|
||||||
---@field worktree string
|
---@field worktree string
|
||||||
---@field ref string resolved commit SHA of the gitobject buffer
|
---@field ref string resolved commit SHA of the gitobject buffer
|
||||||
---@field parent_ref string? resolved parent commit SHA, nil for root commits
|
---@field parent_ref string? resolved parent commit SHA, nil for root commits
|
||||||
|
|
||||||
---@return ow.Git.ShowContext?
|
---@return ow.Git.BufContext?
|
||||||
local function context()
|
local function context()
|
||||||
local worktree = vim.b.git_worktree
|
local worktree = vim.b.git_worktree
|
||||||
local ref = vim.b.git_ref
|
local ref = vim.b.git_ref
|
||||||
@@ -79,6 +78,130 @@ local function is_zero(sha)
|
|||||||
return sha == nil or sha:match("^0+$") ~= nil
|
return sha == nil or sha:match("^0+$") ~= nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---Stage-0 (`:<path>`) index entries are writable through the buffer:
|
||||||
|
---`:w` rewrites the entry via `hash-object` + `update-index`. All other
|
||||||
|
---revspecs (HEAD:, <sha>:, :1:, bare object refs) stay read-only.
|
||||||
|
---@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://<revspec>` and BufReadCmd routes through `M.read_uri`,
|
||||||
|
---which loads via `git cat-file -p`.
|
||||||
|
---@param worktree string
|
||||||
|
---@param revspec string any revspec git understands (e.g. `HEAD:foo`, `:foo`, `:1:foo`, `<sha>`, `<sha>:foo`)
|
||||||
|
---@return integer
|
||||||
|
function M.buf_for(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://<revspec>` URIs. Loads content via
|
||||||
|
---`git cat-file -p <revspec>`. Worktree comes from `vim.b[buf]
|
||||||
|
---.git_worktree` if set, else from cwd. Stage-0 index entries (revspec
|
||||||
|
---form `:<path>`) 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
|
||||||
|
|
||||||
|
local parsed = util.parse_revspec(revspec)
|
||||||
|
local index_path = parsed.stage == 0 and parsed.path or nil
|
||||||
|
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
|
||||||
|
|
||||||
---Buffer for the file at `<ref>:<path>`. A zero/nil blob (file absent on
|
---Buffer for the file at `<ref>:<path>`. A zero/nil blob (file absent on
|
||||||
---this side of the diff) yields an empty placeholder.
|
---this side of the diff) yields an empty placeholder.
|
||||||
---@param worktree string
|
---@param worktree string
|
||||||
@@ -94,22 +217,22 @@ local function blob_buf(worktree, blob, path, ref)
|
|||||||
bufhidden = "hide",
|
bufhidden = "hide",
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
return diff.git_show_buf(worktree, revspec)
|
return M.buf_for(worktree, revspec)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param worktree string
|
---@param worktree string
|
||||||
---@param blob string?
|
---@param blob string?
|
||||||
---@param path string
|
---@param path string
|
||||||
---@param ref string
|
---@param ref string
|
||||||
local function show_blob(worktree, blob, path, ref)
|
local function load_blob(worktree, blob, path, ref)
|
||||||
local buf = blob_buf(worktree, blob, path, ref)
|
local buf = blob_buf(worktree, blob, path, ref)
|
||||||
vim.cmd.normal({ "m'", bang = true })
|
vim.cmd.normal({ "m'", bang = true })
|
||||||
vim.api.nvim_set_current_buf(buf)
|
vim.api.nvim_set_current_buf(buf)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param ctx ow.Git.ShowContext
|
---@param ctx ow.Git.BufContext
|
||||||
---@param section ow.Git.DiffSection
|
---@param section ow.Git.DiffSection
|
||||||
local function show_diff(ctx, section)
|
local function open_section(ctx, section)
|
||||||
if not section.pre_blob or not section.post_blob then
|
if not section.pre_blob or not section.post_blob then
|
||||||
util.warning("no index line; cannot determine blob SHAs")
|
util.warning("no index line; cannot determine blob SHAs")
|
||||||
return
|
return
|
||||||
@@ -122,18 +245,41 @@ local function show_diff(ctx, section)
|
|||||||
blob_buf(ctx.worktree, section.pre_blob, section.pre_path, parent)
|
blob_buf(ctx.worktree, section.pre_blob, section.pre_path, parent)
|
||||||
local right =
|
local right =
|
||||||
blob_buf(ctx.worktree, section.post_blob, section.post_path, ctx.ref)
|
blob_buf(ctx.worktree, section.post_blob, section.post_path, ctx.ref)
|
||||||
vim.cmd.normal({ "m'", bang = true })
|
diff.open(left, right, true)
|
||||||
vim.api.nvim_set_current_buf(right)
|
|
||||||
-- `:diffsplit` is the same path `M.split` uses; Vim's built-in diff
|
|
||||||
-- machinery handles the diff option setup on both windows.
|
|
||||||
vim.cmd(
|
|
||||||
"leftabove vert diffsplit "
|
|
||||||
.. vim.fn.fnameescape(vim.api.nvim_buf_get_name(left))
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---@class ow.Git.OpenCommitOpts
|
---@class ow.Git.OpenObjectOpts
|
||||||
---@field split (false|"above"|"below"|"left"|"right")? forwarded to `git.new_scratch`. Default opens a new horizontal split.
|
---@field split (false|"above"|"below"|"left"|"right")? forwarded to `util.new_scratch`. Default opens a new horizontal split.
|
||||||
|
|
||||||
|
---Place a `git://<revspec>` URI buffer in a window per `opts.split`.
|
||||||
|
---`bufadd` dedups against existing buffers, so re-opening the same URI
|
||||||
|
---reuses the buffer (and `bufload` no-ops). `read_uri` defaults to
|
||||||
|
---`bufhidden=wipe` (right for diff sides), but navigation buffers
|
||||||
|
---should persist across window closes, so override to `hide`.
|
||||||
|
---@param worktree string
|
||||||
|
---@param uri string
|
||||||
|
---@param sha string written to `b:git_ref` so `<CR>` navigation in the buffer can resolve relative paths
|
||||||
|
---@param opts ow.Git.OpenObjectOpts?
|
||||||
|
---@param default_ft string? applied if filetype detection didn't pick anything (bare-sha URIs have no path for the `filetype.add` pattern to match)
|
||||||
|
local function open_uri(worktree, uri, sha, opts, default_ft)
|
||||||
|
local buf = vim.fn.bufadd(uri)
|
||||||
|
vim.b[buf].git_worktree = worktree
|
||||||
|
vim.b[buf].git_ref = sha
|
||||||
|
vim.fn.bufload(buf)
|
||||||
|
vim.bo[buf].bufhidden = "hide"
|
||||||
|
if default_ft and vim.bo[buf].filetype == "" then
|
||||||
|
vim.bo[buf].filetype = default_ft
|
||||||
|
end
|
||||||
|
local split = opts and opts.split
|
||||||
|
if split == false then
|
||||||
|
vim.cmd.normal({ "m'", bang = true })
|
||||||
|
vim.api.nvim_set_current_buf(buf)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
vim.api.nvim_open_win(buf, true, {
|
||||||
|
split = split or (vim.o.splitbelow and "below" or "above"),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
---Open a commit's body via `git cat-file -p` for the header (raw object
|
---Open a commit's body via `git cat-file -p` for the header (raw object
|
||||||
---form, flush-left message) plus `git diff-tree -p` for the patch. The
|
---form, flush-left message) plus `git diff-tree -p` for the patch. The
|
||||||
@@ -144,7 +290,7 @@ end
|
|||||||
---`<CR>` flow.
|
---`<CR>` flow.
|
||||||
---@param worktree string
|
---@param worktree string
|
||||||
---@param ref string
|
---@param ref string
|
||||||
---@param opts ow.Git.OpenCommitOpts?
|
---@param opts ow.Git.OpenObjectOpts?
|
||||||
function M.open_commit(worktree, ref, opts)
|
function M.open_commit(worktree, ref, opts)
|
||||||
local split = opts and opts.split
|
local split = opts and opts.split
|
||||||
local sha = repo.rev_parse(worktree, ref, true) or ref
|
local sha = repo.rev_parse(worktree, ref, true) or ref
|
||||||
@@ -185,7 +331,7 @@ function M.open_commit(worktree, ref, opts)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local parent = repo.rev_parse(worktree, ref .. "^", true)
|
local parent = repo.rev_parse(worktree, ref .. "^", true)
|
||||||
local buf, _ = git.new_scratch({ name = name, split = split })
|
local buf, _ = util.new_scratch({ name = name, split = split })
|
||||||
vim.b[buf].git_worktree = worktree
|
vim.b[buf].git_worktree = worktree
|
||||||
vim.b[buf].git_ref = sha
|
vim.b[buf].git_ref = sha
|
||||||
vim.b[buf].git_parent_ref = parent
|
vim.b[buf].git_parent_ref = parent
|
||||||
@@ -199,14 +345,24 @@ function M.open_commit(worktree, ref, opts)
|
|||||||
vim.bo[buf].filetype = "git"
|
vim.bo[buf].filetype = "git"
|
||||||
end
|
end
|
||||||
|
|
||||||
---Open any git object in a buffer. Commits get the full
|
---Open any git object. Accepts either a bare ref (commit/tree/blob/tag
|
||||||
---`M.open_commit` view (cat-file + diff-tree). Trees, blobs, and tags
|
---SHA, branch name, etc.) or `<commit-ref>:<path>` form. Commits route
|
||||||
---dump `git cat-file -p` output as-is. The object type is detected via
|
---to `M.open_commit` for the message + diff view; everything else flows
|
||||||
---`git cat-file -t`.
|
---through the BufReadCmd loader at the `git://<revspec>` URI.
|
||||||
---@param worktree string
|
---@param worktree string
|
||||||
---@param ref string
|
---@param ref string
|
||||||
---@param opts ow.Git.OpenCommitOpts?
|
---@param opts ow.Git.OpenObjectOpts?
|
||||||
function M.open_object(worktree, ref, opts)
|
function M.open_object(worktree, ref, opts)
|
||||||
|
-- Path-form: resolve the commit-ref to a sha so the URI stays stable
|
||||||
|
-- if the ref later moves, and the `filetype.add` pattern can pick the
|
||||||
|
-- ft from the path segment.
|
||||||
|
local commit_ref, path = ref:match("^(.-):(.+)$")
|
||||||
|
if commit_ref then
|
||||||
|
local sha = repo.rev_parse(worktree, commit_ref, true) or commit_ref
|
||||||
|
open_uri(worktree, "git://" .. sha .. ":" .. path, sha, opts)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
local type_out = util.exec(
|
local type_out = util.exec(
|
||||||
{ "git", "cat-file", "-t", ref },
|
{ "git", "cat-file", "-t", ref },
|
||||||
{ cwd = worktree, silent = true }
|
{ cwd = worktree, silent = true }
|
||||||
@@ -216,44 +372,16 @@ function M.open_object(worktree, ref, opts)
|
|||||||
util.warning("not a git object: %s", ref)
|
util.warning("not a git object: %s", ref)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if obj_type == "commit" then
|
if obj_type == "commit" then
|
||||||
M.open_commit(worktree, ref, opts)
|
M.open_commit(worktree, ref, opts)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local split = opts and opts.split
|
-- Trees, blobs, tags. The bare-sha URI has no path, so the
|
||||||
|
-- `filetype.add` pattern doesn't match; default to `git` so
|
||||||
|
-- tree / tag header lines syntax-highlight.
|
||||||
local sha = repo.rev_parse(worktree, ref, true) or ref
|
local sha = repo.rev_parse(worktree, ref, true) or ref
|
||||||
local name = "git://" .. sha
|
open_uri(worktree, "git://" .. sha, sha, opts, "git")
|
||||||
local existing = vim.fn.bufnr(name)
|
|
||||||
if existing ~= -1 and vim.api.nvim_buf_is_loaded(existing) then
|
|
||||||
if split == false then
|
|
||||||
vim.cmd.normal({ "m'", bang = true })
|
|
||||||
vim.api.nvim_set_current_buf(existing)
|
|
||||||
else
|
|
||||||
vim.api.nvim_open_win(existing, true, {
|
|
||||||
split = split or (vim.o.splitbelow and "below" or "above"),
|
|
||||||
})
|
|
||||||
end
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local stdout = util.exec(
|
|
||||||
{ "git", "cat-file", "-p", ref },
|
|
||||||
{ cwd = worktree }
|
|
||||||
)
|
|
||||||
if not stdout then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local buf, _ = git.new_scratch({ name = name, split = split })
|
|
||||||
vim.b[buf].git_worktree = worktree
|
|
||||||
vim.b[buf].git_ref = sha
|
|
||||||
vim.bo[buf].modifiable = true
|
|
||||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, util.split_lines(stdout))
|
|
||||||
vim.bo[buf].modifiable = false
|
|
||||||
vim.bo[buf].modified = false
|
|
||||||
vim.bo[buf].filetype = "git"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---@return boolean dispatched true if the cursor was on an actionable line
|
---@return boolean dispatched true if the cursor was on an actionable line
|
||||||
@@ -277,22 +405,18 @@ function M.open_under_cursor()
|
|||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Tree-entry navigation: `<mode> <type> <sha>\t<name>`. Blob entries
|
-- Tree-entry navigation: `<mode> <type> <sha>\t<name>`. Blobs are
|
||||||
-- route through `diff.git_show_buf` so the buffer URI carries the
|
-- routed by path so the URI carries the entry name and filetype
|
||||||
-- entry name and BufReadCmd / BufReadPost resolve filetype from it.
|
-- detection picks it up. Subtrees navigate by sha so the resulting
|
||||||
-- Other types (subtrees, submodule commit refs, tags) fall through
|
-- buffer's `git_ref` is the subtree's own sha (correct anchor for
|
||||||
-- to the generic SHA-based opener.
|
-- relative path navigation within it). Other types (submodule
|
||||||
|
-- commit refs, tags) also navigate by sha.
|
||||||
local entry_type, entry_sha, entry_name =
|
local entry_type, entry_sha, entry_name =
|
||||||
line:match("^%d+ (%w+) (%x+)\t(.+)$")
|
line:match("^%d+ (%w+) (%x+)\t(.+)$")
|
||||||
if entry_sha then
|
if entry_sha then
|
||||||
if entry_type == "blob" then
|
local nav_ref = entry_type == "blob" and (ctx.ref .. ":" .. entry_name)
|
||||||
local buf =
|
or entry_sha
|
||||||
diff.git_show_buf(ctx.worktree, ctx.ref .. ":" .. entry_name)
|
M.open_object(ctx.worktree, nav_ref, { split = false })
|
||||||
vim.cmd.normal({ "m'", bang = true })
|
|
||||||
vim.api.nvim_set_current_buf(buf)
|
|
||||||
else
|
|
||||||
M.open_object(ctx.worktree, entry_sha, { split = false })
|
|
||||||
end
|
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -303,23 +427,23 @@ function M.open_under_cursor()
|
|||||||
local parent = ctx.parent_ref or "0"
|
local parent = ctx.parent_ref or "0"
|
||||||
|
|
||||||
if line:match("^diff %-%-git ") then
|
if line:match("^diff %-%-git ") then
|
||||||
show_diff(ctx, section)
|
open_section(ctx, section)
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
if line:match("^%-%-%- ") then
|
if line:match("^%-%-%- ") then
|
||||||
show_blob(ctx.worktree, section.pre_blob, section.pre_path, parent)
|
load_blob(ctx.worktree, section.pre_blob, section.pre_path, parent)
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
if line:match("^%+%+%+ ") then
|
if line:match("^%+%+%+ ") then
|
||||||
show_blob(ctx.worktree, section.post_blob, section.post_path, ctx.ref)
|
load_blob(ctx.worktree, section.post_blob, section.post_path, ctx.ref)
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
local prefix = line:sub(1, 1)
|
local prefix = line:sub(1, 1)
|
||||||
if prefix == "+" then
|
if prefix == "+" then
|
||||||
show_blob(ctx.worktree, section.post_blob, section.post_path, ctx.ref)
|
load_blob(ctx.worktree, section.post_blob, section.post_path, ctx.ref)
|
||||||
return true
|
return true
|
||||||
elseif prefix == "-" then
|
elseif prefix == "-" then
|
||||||
show_blob(ctx.worktree, section.pre_blob, section.pre_path, parent)
|
load_blob(ctx.worktree, section.pre_blob, section.pre_path, parent)
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -343,4 +343,18 @@ function M.rev_parse(worktree, ref, short)
|
|||||||
return trimmed ~= "" and trimmed or nil
|
return trimmed ~= "" and trimmed or nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---Verify a revspec resolves to an existing git object. `cat-file -e` is
|
||||||
|
---git's cheapest existence check, and unlike `rev-parse --verify` it
|
||||||
|
---also accepts the `<commit>:<path>` form that BufReadCmd revspec URIs
|
||||||
|
---use.
|
||||||
|
---@param worktree string
|
||||||
|
---@param revspec string
|
||||||
|
---@return boolean
|
||||||
|
function M.object_exists(worktree, revspec)
|
||||||
|
return util.exec(
|
||||||
|
{ "git", "cat-file", "-e", revspec },
|
||||||
|
{ cwd = worktree, silent = true }
|
||||||
|
) ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
local git = require("git")
|
|
||||||
local repo = require("git.repo")
|
|
||||||
local util = require("git.util")
|
|
||||||
|
|
||||||
local M = {}
|
|
||||||
|
|
||||||
---@class ow.Git.ShowOpts
|
|
||||||
---@field split (false|"above"|"below"|"left"|"right")? forwarded to `git.new_scratch`. Default opens a new horizontal split.
|
|
||||||
|
|
||||||
---Open a commit's `git show <ref>` output in a buffer (indented message,
|
|
||||||
---unified-diff body). For the navigable raw-object form used by the
|
|
||||||
---gitlog `<CR>` flow, see `git.object` `M.open_commit`.
|
|
||||||
---@param worktree string
|
|
||||||
---@param ref string
|
|
||||||
---@param opts ow.Git.ShowOpts?
|
|
||||||
function M.show(worktree, ref, opts)
|
|
||||||
local split = opts and opts.split
|
|
||||||
local sha = repo.rev_parse(worktree, ref, true) or ref
|
|
||||||
local name = "git://" .. sha
|
|
||||||
-- Commit SHAs are immutable so a previously-opened buffer is still
|
|
||||||
-- valid. Reuse it instead of refetching.
|
|
||||||
local existing = vim.fn.bufnr(name)
|
|
||||||
if existing ~= -1 and vim.api.nvim_buf_is_loaded(existing) then
|
|
||||||
if split == false then
|
|
||||||
vim.cmd.normal({ "m'", bang = true })
|
|
||||||
vim.api.nvim_set_current_buf(existing)
|
|
||||||
else
|
|
||||||
vim.api.nvim_open_win(existing, true, {
|
|
||||||
split = split or (vim.o.splitbelow and "below" or "above"),
|
|
||||||
})
|
|
||||||
end
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local stdout = util.exec({ "git", "show", ref }, { cwd = worktree })
|
|
||||||
if not stdout then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local parent = repo.rev_parse(worktree, ref .. "^", true)
|
|
||||||
local buf, _ = git.new_scratch({ name = name, split = split })
|
|
||||||
vim.b[buf].git_worktree = worktree
|
|
||||||
vim.b[buf].git_ref = sha
|
|
||||||
vim.b[buf].git_parent_ref = parent
|
|
||||||
vim.bo[buf].modifiable = true
|
|
||||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, util.split_lines(stdout))
|
|
||||||
vim.bo[buf].modifiable = false
|
|
||||||
vim.bo[buf].modified = false
|
|
||||||
vim.bo[buf].filetype = "git"
|
|
||||||
end
|
|
||||||
|
|
||||||
return M
|
|
||||||
+22
-33
@@ -1,5 +1,5 @@
|
|||||||
local diff = require("git.diff")
|
local diff = require("git.diff")
|
||||||
local git = require("git")
|
local object = require("git.object")
|
||||||
local repo = require("git.repo")
|
local repo = require("git.repo")
|
||||||
local util = require("git.util")
|
local util = require("git.util")
|
||||||
|
|
||||||
@@ -433,7 +433,7 @@ local function refresh(bufnr, prefetched_stdout)
|
|||||||
end
|
end
|
||||||
-- Any fs-event that triggered this refresh might have changed the
|
-- Any fs-event that triggered this refresh might have changed the
|
||||||
-- worktree under the diff buffers we last opened; invalidate the
|
-- worktree under the diff buffers we last opened; invalidate the
|
||||||
-- cache so the next show_diff recomputes panes.
|
-- cache so the next view_entry recomputes panes.
|
||||||
s.last_shown_key = nil
|
s.last_shown_key = nil
|
||||||
local fp = fingerprint(branch, groups)
|
local fp = fingerprint(branch, groups)
|
||||||
if fp == s.last_render_key then
|
if fp == s.last_render_key then
|
||||||
@@ -484,18 +484,23 @@ end
|
|||||||
---@return ow.Git.DiffSide
|
---@return ow.Git.DiffSide
|
||||||
local function head_pane(worktree, path)
|
local function head_pane(worktree, path)
|
||||||
return {
|
return {
|
||||||
buf = diff.git_show_buf(worktree, "HEAD:" .. path),
|
buf = object.buf_for(worktree, "HEAD:" .. path),
|
||||||
name = "git://HEAD:" .. path,
|
name = "git://HEAD:" .. path,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param worktree string
|
---@param worktree string
|
||||||
---@param path string
|
---@param path string
|
||||||
|
---@param kind "index"|"HEAD"|"worktree"
|
||||||
---@return ow.Git.DiffSide
|
---@return ow.Git.DiffSide
|
||||||
local function absent_pane(worktree, path)
|
local function absent_pane(worktree, path, kind)
|
||||||
return {
|
return {
|
||||||
buf = diff.empty_buf(),
|
buf = diff.empty_buf(),
|
||||||
name = "[absent] " .. vim.fs.joinpath(worktree, path),
|
name = string.format(
|
||||||
|
"[absent %s] %s",
|
||||||
|
kind,
|
||||||
|
vim.fs.joinpath(worktree, path)
|
||||||
|
),
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -503,10 +508,9 @@ end
|
|||||||
---@param path string
|
---@param path string
|
||||||
---@return ow.Git.DiffSide
|
---@return ow.Git.DiffSide
|
||||||
local function worktree_pane(worktree, path)
|
local function worktree_pane(worktree, path)
|
||||||
return {
|
local buf = vim.fn.bufadd(vim.fs.joinpath(worktree, path))
|
||||||
buf = diff.load_file_buf(vim.fs.joinpath(worktree, path)),
|
vim.fn.bufload(buf)
|
||||||
name = nil,
|
return { buf = buf, name = nil }
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param s ow.Git.SidebarState
|
---@param s ow.Git.SidebarState
|
||||||
@@ -518,11 +522,11 @@ local function index_pane(s, entry)
|
|||||||
or (entry.section == "Staged" and entry.x == "D")
|
or (entry.section == "Staged" and entry.x == "D")
|
||||||
)
|
)
|
||||||
if not in_index then
|
if not in_index then
|
||||||
return absent_pane(s.worktree, entry.path)
|
return absent_pane(s.worktree, entry.path, "index")
|
||||||
end
|
end
|
||||||
return {
|
return {
|
||||||
buf = diff.git_show_buf(s.worktree, ":" .. entry.path),
|
buf = object.buf_for(s.worktree, ":0:" .. entry.path),
|
||||||
name = "git://:" .. entry.path,
|
name = "git://:0:" .. entry.path,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -534,7 +538,7 @@ local function other_pane(s, entry)
|
|||||||
local worktree = s.worktree
|
local worktree = s.worktree
|
||||||
if entry.section == "Staged" then
|
if entry.section == "Staged" then
|
||||||
if entry.x == "A" then
|
if entry.x == "A" then
|
||||||
return absent_pane(worktree, p)
|
return absent_pane(worktree, p, "HEAD")
|
||||||
end
|
end
|
||||||
if entry.x == "D" then
|
if entry.x == "D" then
|
||||||
return head_pane(worktree, p)
|
return head_pane(worktree, p)
|
||||||
@@ -544,7 +548,7 @@ local function other_pane(s, entry)
|
|||||||
end
|
end
|
||||||
if entry.section == "Unstaged" then
|
if entry.section == "Unstaged" then
|
||||||
if entry.y == "D" then
|
if entry.y == "D" then
|
||||||
return absent_pane(worktree, p)
|
return absent_pane(worktree, p, "worktree")
|
||||||
end
|
end
|
||||||
return worktree_pane(worktree, p)
|
return worktree_pane(worktree, p)
|
||||||
end
|
end
|
||||||
@@ -649,7 +653,7 @@ end
|
|||||||
---@param s ow.Git.SidebarState
|
---@param s ow.Git.SidebarState
|
||||||
---@param entry ow.Git.SidebarEntry
|
---@param entry ow.Git.SidebarEntry
|
||||||
---@param focus_left boolean
|
---@param focus_left boolean
|
||||||
local function show_diff(s, entry, focus_left)
|
local function view_entry(s, entry, focus_left)
|
||||||
if not entry.path then
|
if not entry.path then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -713,22 +717,7 @@ local function show_diff(s, entry, focus_left)
|
|||||||
s.diff_left_win = left_win
|
s.diff_left_win = left_win
|
||||||
s.diff_right_win = right_win
|
s.diff_right_win = right_win
|
||||||
|
|
||||||
-- Toggle diff off around the buffer swap so Vim tears down the old
|
diff.update_pair(left_win, right_win, pair)
|
||||||
-- diff group and re-establishes a fresh one against the new pair.
|
|
||||||
-- 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.
|
|
||||||
diff.set_diff(left_win, false)
|
|
||||||
diff.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
|
|
||||||
diff.set_buf_name_and_filetype(side.buf, side.name)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
diff.set_diff(left_win, true)
|
|
||||||
diff.set_diff(right_win, true)
|
|
||||||
s.last_shown_key = key
|
s.last_shown_key = key
|
||||||
|
|
||||||
if focus_left then
|
if focus_left then
|
||||||
@@ -744,7 +733,7 @@ local function preview_or_open(focus_left)
|
|||||||
if not s or not entry then
|
if not s or not entry then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
show_diff(s, entry, focus_left)
|
view_entry(s, entry, focus_left)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function action_stage()
|
local function action_stage()
|
||||||
@@ -876,7 +865,7 @@ local function open(worktree)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local previous_win = vim.api.nvim_get_current_win()
|
local previous_win = vim.api.nvim_get_current_win()
|
||||||
local bufnr, win = git.new_scratch({ split = "left", bufhidden = "wipe" })
|
local bufnr, win = util.new_scratch({ split = "left", bufhidden = "wipe" })
|
||||||
vim.bo[bufnr].filetype = "gitsidebar"
|
vim.bo[bufnr].filetype = "gitsidebar"
|
||||||
|
|
||||||
vim.wo[win].number = false
|
vim.wo[win].number = false
|
||||||
|
|||||||
@@ -1,5 +1,62 @@
|
|||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
|
---@class ow.Git.ParsedRevspec
|
||||||
|
---@field stage 0|1|2|3? index stage when the revspec is `:<path>` / `:0:<path>` / `:N:<path>`; nil otherwise
|
||||||
|
---@field path string? path component when the revspec carries one; nil for bare object refs
|
||||||
|
|
||||||
|
---Classify a `git://<revspec>` revspec into its stage / path components.
|
||||||
|
---Recognised forms:
|
||||||
|
--- * `:<path>` and `:0:<path>` -> stage 0 (the resolved index entry)
|
||||||
|
--- * `:1:<path>` / `:2:<path>` / `:3:<path>` -> merge stages base / ours / theirs
|
||||||
|
--- * `<commit-ref>:<path>` -> stage = nil, path set
|
||||||
|
--- * bare object ref (no `:`) -> stage = nil, path = nil
|
||||||
|
---@param revspec string
|
||||||
|
---@return ow.Git.ParsedRevspec
|
||||||
|
function M.parse_revspec(revspec)
|
||||||
|
local stage, path = revspec:match("^:([0123]):(.+)$")
|
||||||
|
if stage then
|
||||||
|
return { stage = tonumber(stage), path = path }
|
||||||
|
end
|
||||||
|
path = revspec:match("^:([^:]+)$")
|
||||||
|
if path then
|
||||||
|
return { stage = 0, path = path }
|
||||||
|
end
|
||||||
|
path = (revspec:match("^[^:]+:(.+)$"))
|
||||||
|
return { stage = nil, path = path }
|
||||||
|
end
|
||||||
|
|
||||||
|
---@class ow.Git.NewScratchOpts
|
||||||
|
---@field name string?
|
||||||
|
---@field bufhidden ("hide"|"wipe")? defaults to "hide"
|
||||||
|
---@field split (false|"above"|"below"|"left"|"right")? defaults to splitbelow-aware horizontal. `false` places the buffer in the current window (drops a `'` mark first so the user can jump back).
|
||||||
|
|
||||||
|
---Create a fresh non-modifiable scratch buffer and place it. Default split
|
||||||
|
---direction is horizontal, honouring `splitbelow`. Caller flips
|
||||||
|
---`modifiable`, fills the buffer, and sets `filetype` once content lands.
|
||||||
|
---@param opts ow.Git.NewScratchOpts?
|
||||||
|
---@return integer buf
|
||||||
|
---@return integer win
|
||||||
|
function M.new_scratch(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 "hide"
|
||||||
|
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
|
||||||
|
if opts.split == false then
|
||||||
|
vim.cmd.normal({ "m'", bang = true })
|
||||||
|
vim.api.nvim_set_current_buf(buf)
|
||||||
|
return buf, vim.api.nvim_get_current_win()
|
||||||
|
end
|
||||||
|
local split = opts.split or (vim.o.splitbelow and "below" or "above")
|
||||||
|
local win = vim.api.nvim_open_win(buf, true, { split = split })
|
||||||
|
return buf, win
|
||||||
|
end
|
||||||
|
|
||||||
---@param fmt string
|
---@param fmt string
|
||||||
---@param ... any
|
---@param ... any
|
||||||
function M.error(fmt, ...)
|
function M.error(fmt, ...)
|
||||||
|
|||||||
Reference in New Issue
Block a user