refactor(git): introduce Revision class, normalize naming, slim docs
This commit is contained in:
+14
-26
@@ -7,9 +7,8 @@ local M = {}
|
|||||||
|
|
||||||
---@class ow.Git.SplitHandler
|
---@class ow.Git.SplitHandler
|
||||||
---@field ft string
|
---@field ft string
|
||||||
---@field needs_ref boolean?
|
---@field needs_rev boolean?
|
||||||
|
|
||||||
---Subcommands whose output goes to a buffer.
|
|
||||||
---@type table<string, ow.Git.SplitHandler>
|
---@type table<string, ow.Git.SplitHandler>
|
||||||
local SPLIT_HANDLERS = {
|
local SPLIT_HANDLERS = {
|
||||||
log = { ft = "git" },
|
log = { ft = "git" },
|
||||||
@@ -34,7 +33,6 @@ local function populate_cached_cmds(result)
|
|||||||
table.sort(cached_cmds)
|
table.sort(cached_cmds)
|
||||||
end
|
end
|
||||||
|
|
||||||
---Prime `cached_cmds` asynchronously so the first `:G <Tab>` doesn't block.
|
|
||||||
local function prefetch_cmds()
|
local function prefetch_cmds()
|
||||||
vim.system(
|
vim.system(
|
||||||
{ "git", "--list-cmds=main,others,alias" },
|
{ "git", "--list-cmds=main,others,alias" },
|
||||||
@@ -85,10 +83,6 @@ local function place_split(name)
|
|||||||
return buf
|
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
|
||||||
@@ -104,15 +98,15 @@ local function run_in_split(worktree, args, conf)
|
|||||||
local name = "[git " .. table.concat(args, " ") .. "]"
|
local name = "[git " .. table.concat(args, " ") .. "]"
|
||||||
local buf = place_split(name)
|
local buf = place_split(name)
|
||||||
vim.b[buf].git_worktree = worktree
|
vim.b[buf].git_worktree = worktree
|
||||||
vim.b[buf].git_ref = nil
|
vim.b[buf].git_sha = nil
|
||||||
vim.b[buf].git_parent_ref = nil
|
vim.b[buf].git_parent_sha = nil
|
||||||
if conf.needs_ref then
|
if conf.needs_rev then
|
||||||
local user_ref = first_positional(args, 2) or "HEAD"
|
local user_rev = first_positional(args, 2) or "HEAD"
|
||||||
local sha = repo.rev_parse(worktree, user_ref, true)
|
local sha = repo.rev_parse(worktree, user_rev, true)
|
||||||
if sha then
|
if sha then
|
||||||
vim.b[buf].git_ref = sha
|
vim.b[buf].git_sha = sha
|
||||||
vim.b[buf].git_parent_ref =
|
vim.b[buf].git_parent_sha =
|
||||||
repo.rev_parse(worktree, user_ref .. "^", true)
|
repo.rev_parse(worktree, user_rev .. "^", true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
vim.bo[buf].filetype = conf.ft
|
vim.bo[buf].filetype = conf.ft
|
||||||
@@ -206,31 +200,25 @@ function M.run(args)
|
|||||||
return
|
return
|
||||||
end
|
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
|
if sub == "show" then
|
||||||
local arg = first_positional(args, 2)
|
local arg = first_positional(args, 2)
|
||||||
if arg and arg:find(":", 1, true) then
|
if arg and arg:find(":", 1, true) then
|
||||||
object.open_object(worktree, arg)
|
object.open_object(worktree, arg)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
run_in_split(worktree, args, { ft = "git", needs_ref = true })
|
run_in_split(worktree, args, { ft = "git", needs_rev = true })
|
||||||
return
|
return
|
||||||
end
|
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 sub == "cat-file" then
|
||||||
if vim.list_contains(args, "-p") then
|
if vim.list_contains(args, "-p") then
|
||||||
local ref = first_positional(args, 2)
|
local rev = first_positional(args, 2)
|
||||||
if ref then
|
if rev then
|
||||||
object.open_object(worktree, ref)
|
object.open_object(worktree, rev)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
run_in_split(worktree, args, { ft = "git", needs_ref = true })
|
run_in_split(worktree, args, { ft = "git", needs_rev = true })
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+41
-79
@@ -1,15 +1,9 @@
|
|||||||
|
local Revision = require("git.revision")
|
||||||
local repo = require("git.repo")
|
local repo = require("git.repo")
|
||||||
local util = require("git.util")
|
local util = require("git.util")
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
---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 end up 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)
|
function M.set_diff(win, enabled)
|
||||||
@@ -18,16 +12,10 @@ function M.set_diff(win, enabled)
|
|||||||
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`). Drops a `'` jumplist mark first so `''` jumps back.
|
|
||||||
---@param left integer
|
---@param left integer
|
||||||
---@param right integer
|
---@param right integer
|
||||||
---@param vertical boolean
|
---@param vertical boolean
|
||||||
function M.open(left, right, vertical)
|
function M.open(left, right, vertical)
|
||||||
-- Read the name first: an empty placeholder has `bufhidden=wipe`,
|
|
||||||
-- so `nvim_set_current_buf(right)` below wipes `left` before a
|
|
||||||
-- name lookup could see it. `:diffsplit` re-creates from the name.
|
|
||||||
local left_name = vim.api.nvim_buf_get_name(left)
|
local left_name = vim.api.nvim_buf_get_name(left)
|
||||||
vim.cmd.normal({ "m'", bang = true })
|
vim.cmd.normal({ "m'", bang = true })
|
||||||
vim.api.nvim_set_current_buf(right)
|
vim.api.nvim_set_current_buf(right)
|
||||||
@@ -38,13 +26,6 @@ function M.open(left, right, vertical)
|
|||||||
})
|
})
|
||||||
end
|
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 are renamed
|
|
||||||
---and re-run filetype detection.
|
|
||||||
---@param left_win integer
|
---@param left_win integer
|
||||||
---@param right_win integer
|
---@param right_win integer
|
||||||
---@param pair ow.Git.DiffPair
|
---@param pair ow.Git.DiffPair
|
||||||
@@ -65,8 +46,6 @@ function M.update_pair(left_win, right_win, pair)
|
|||||||
vim.cmd.syncbind()
|
vim.cmd.syncbind()
|
||||||
end
|
end
|
||||||
|
|
||||||
---Open two buffers as a diff. `a_left` puts `buf_a` in the leftabove
|
|
||||||
---slot (where `M.open` parks the cursor).
|
|
||||||
---@param buf_a integer
|
---@param buf_a integer
|
||||||
---@param buf_b integer
|
---@param buf_b integer
|
||||||
---@param a_left boolean
|
---@param a_left boolean
|
||||||
@@ -79,97 +58,82 @@ local function place_pair(buf_a, buf_b, a_left, vertical)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
---Dispatch for `M.split` when the current buffer is a `git://<revspec>`
|
|
||||||
---URI. Placement follows the older-on-left convention.
|
|
||||||
---
|
|
||||||
---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 the
|
|
||||||
---above and pairs cur with that revspec literally.
|
|
||||||
---@param opts ow.Git.SplitOpts
|
---@param opts ow.Git.SplitOpts
|
||||||
---@param cur_buf integer
|
---@param buf integer
|
||||||
---@param cur_revspec string
|
---@param rev ow.Git.Revision
|
||||||
local function uri_split(opts, cur_buf, cur_revspec)
|
local function uri_split(opts, buf, rev)
|
||||||
local worktree = vim.b[cur_buf].git_worktree
|
local worktree = vim.b[buf].git_worktree or select(2, repo.resolve_cwd())
|
||||||
or select(2, repo.resolve_cwd())
|
|
||||||
if not worktree then
|
if not worktree then
|
||||||
util.warning("git URI buffer has no worktree")
|
util.warning("git URI buffer has no worktree")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local cur = util.parse_revspec(cur_revspec)
|
if not rev.path then
|
||||||
if not cur.path then
|
|
||||||
util.warning("git URI has no path, cannot diff against worktree")
|
util.warning("git URI has no path, cannot diff against worktree")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
-- Lazy-required to break the load-time cycle with `git.object`.
|
|
||||||
local object = require("git.object")
|
local object = require("git.object")
|
||||||
|
|
||||||
if opts.revspec ~= "" and opts.revspec:find(":", 1, true) then
|
if opts.rev and opts.rev:find(":", 1, true) then
|
||||||
local content = util.exec(
|
local content = util.exec(
|
||||||
{ "git", "cat-file", "-p", opts.revspec },
|
{ "git", "cat-file", "-p", opts.rev },
|
||||||
{ cwd = worktree, silent = true }
|
{ cwd = worktree, silent = true }
|
||||||
)
|
)
|
||||||
if not content then
|
if not content then
|
||||||
util.warning("invalid revspec: %s", opts.revspec)
|
util.warning("invalid rev: %s", opts.rev)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
place_pair(
|
place_pair(
|
||||||
cur_buf,
|
buf,
|
||||||
object.buf_for(worktree, opts.revspec, content),
|
object.buf_for(worktree, Revision.parse(opts.rev), content),
|
||||||
false,
|
false,
|
||||||
opts.vertical
|
opts.vertical
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if opts.revspec == "" then
|
if not opts.rev then
|
||||||
local worktree_path = vim.fs.joinpath(worktree, cur.path)
|
local worktree_path = vim.fs.joinpath(worktree, rev.path)
|
||||||
if not vim.uv.fs_stat(worktree_path) then
|
if not vim.uv.fs_stat(worktree_path) then
|
||||||
util.warning("worktree file does not exist: %s", cur.path)
|
util.warning("worktree file does not exist: %s", rev.path)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local worktree_buf = vim.fn.bufadd(worktree_path)
|
local worktree_buf = vim.fn.bufadd(worktree_path)
|
||||||
vim.fn.bufload(worktree_buf)
|
vim.fn.bufload(worktree_buf)
|
||||||
place_pair(cur_buf, worktree_buf, true, opts.vertical)
|
place_pair(buf, worktree_buf, true, opts.vertical)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if cur.stage == 1 then
|
if rev.stage == 1 then
|
||||||
util.warning("gD on merge base is ambiguous, use :Gdiffsplit <ref>")
|
util.warning("gD on merge base is ambiguous, use :Gdiffsplit <rev>")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local mapping = {
|
local mapping = {
|
||||||
[2] = { ":3:" .. cur.path, true },
|
[2] = { Revision.new({ stage = 3, path = rev.path }), true },
|
||||||
[3] = { ":2:" .. cur.path, false },
|
[3] = { Revision.new({ stage = 2, path = rev.path }), false },
|
||||||
[0] = { "HEAD:" .. cur.path, false },
|
[0] = { Revision.new({ base = "HEAD", path = rev.path }), false },
|
||||||
}
|
}
|
||||||
local m = mapping[cur.stage] or { ":0:" .. cur.path, true }
|
local m = mapping[rev.stage]
|
||||||
local other_revspec, cur_left = m[1], m[2]
|
or { Revision.new({ stage = 0, path = rev.path }), true }
|
||||||
|
local other_rev, left = m[1], m[2]
|
||||||
local content = util.exec(
|
local content = util.exec(
|
||||||
{ "git", "cat-file", "-p", other_revspec },
|
{ "git", "cat-file", "-p", other_rev:format() },
|
||||||
{ cwd = worktree, silent = true }
|
{ cwd = worktree, silent = true }
|
||||||
)
|
)
|
||||||
if not content then
|
if not content then
|
||||||
util.warning("invalid revspec: %s", other_revspec)
|
util.warning("invalid rev: %s", other_rev:format())
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
place_pair(
|
place_pair(
|
||||||
cur_buf,
|
buf,
|
||||||
object.buf_for(worktree, other_revspec, content),
|
object.buf_for(worktree, other_rev, content),
|
||||||
cur_left,
|
left,
|
||||||
opts.vertical
|
opts.vertical
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@class ow.Git.SplitOpts
|
---@class ow.Git.SplitOpts
|
||||||
---@field revspec string `''` for smart-default routing (index vs worktree). A plain ref like `'HEAD'` compares `<ref>:<rel>` against the current path. A full revspec containing `:` (e.g. `':2:foo'`, `'HEAD~1:other.lua'`) is used as-is.
|
---@field rev string?
|
||||||
---@field vertical boolean
|
---@field vertical boolean
|
||||||
|
|
||||||
---@param opts ow.Git.SplitOpts
|
---@param opts ow.Git.SplitOpts
|
||||||
@@ -177,9 +141,9 @@ 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 = util.parse_uri(cur_path)
|
local cur_rev = Revision.from_uri(cur_path)
|
||||||
if cur_revspec then
|
if cur_rev then
|
||||||
return uri_split(opts, cur_buf, cur_revspec)
|
return uri_split(opts, cur_buf, cur_rev)
|
||||||
end
|
end
|
||||||
|
|
||||||
if cur_path == "" then
|
if cur_path == "" then
|
||||||
@@ -201,25 +165,23 @@ function M.split(opts)
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local revspec
|
local rev
|
||||||
if opts.revspec == "" then
|
if not opts.rev then
|
||||||
revspec = ":0:" .. rel
|
rev = Revision.new({ stage = 0, path = rel })
|
||||||
elseif opts.revspec:find(":", 1, true) then
|
elseif opts.rev:find(":", 1, true) then
|
||||||
revspec = opts.revspec
|
rev = Revision.parse(opts.rev)
|
||||||
else
|
else
|
||||||
revspec = opts.revspec .. ":" .. rel
|
rev = Revision.new({ base = opts.rev, path = rel })
|
||||||
end
|
end
|
||||||
local content = util.exec(
|
local content = util.exec(
|
||||||
{ "git", "cat-file", "-p", revspec },
|
{ "git", "cat-file", "-p", rev:format() },
|
||||||
{ cwd = worktree, silent = true }
|
{ cwd = worktree, silent = true }
|
||||||
)
|
)
|
||||||
if not content then
|
if not content then
|
||||||
util.warning("invalid revspec: %s", revspec)
|
util.warning("invalid rev: %s", rev:format())
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local object = require("git.object")
|
local buf = require("git.object").buf_for(worktree, rev, content)
|
||||||
local buf = object.buf_for(worktree, revspec, content)
|
|
||||||
-- Revspec snapshot is older than the worktree, so it goes left.
|
|
||||||
place_pair(buf, cur_buf, true, opts.vertical)
|
place_pair(buf, cur_buf, true, opts.vertical)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,6 @@ local M = {}
|
|||||||
|
|
||||||
local SENTINEL = "__NVIM_GIT_EDIT__"
|
local SENTINEL = "__NVIM_GIT_EDIT__"
|
||||||
|
|
||||||
-- Per-invocation: each `sh -c` body picks a flag file via `$$`,
|
|
||||||
-- prints sentinel + flag-path + abs-path on stderr, then polls the
|
|
||||||
-- flag until Neovim writes it. `rebase -i` fires the editor many
|
|
||||||
-- times (todo + each reword). Each call is a fresh shell with a
|
|
||||||
-- fresh `$$`, so flags don't collide.
|
|
||||||
local SCRIPT = string.format(
|
local SCRIPT = string.format(
|
||||||
[=[set -eu
|
[=[set -eu
|
||||||
flag="${TMPDIR:-/tmp}/nvim-git-editor-$$.done"
|
flag="${TMPDIR:-/tmp}/nvim-git-editor-$$.done"
|
||||||
@@ -22,7 +17,6 @@ done
|
|||||||
SENTINEL
|
SENTINEL
|
||||||
)
|
)
|
||||||
|
|
||||||
---POSIX shell single-quote escape: foo'bar -> 'foo'\''bar'.
|
|
||||||
---@param s string
|
---@param s string
|
||||||
---@return string
|
---@return string
|
||||||
local function shq(s)
|
local function shq(s)
|
||||||
@@ -31,11 +25,6 @@ end
|
|||||||
|
|
||||||
local GIT_EDITOR = "sh -c " .. shq(SCRIPT) .. " --"
|
local GIT_EDITOR = "sh -c " .. shq(SCRIPT) .. " --"
|
||||||
|
|
||||||
---Build a stderr callback that strips our sentinel lines, accumulates
|
|
||||||
---the rest, and dispatches `on_open(abs, done)` for each sentinel seen.
|
|
||||||
---The `finalize(result)` helper drains any trailing partial line into
|
|
||||||
---`result.stderr` so the caller can treat `result` like a plain
|
|
||||||
---`vim.system` result.
|
|
||||||
---@param on_open fun(file_path: string, done: fun())
|
---@param on_open fun(file_path: string, done: fun())
|
||||||
---@return fun(err: string?, data: string?), fun(result: vim.SystemCompleted)
|
---@return fun(err: string?, data: string?), fun(result: vim.SystemCompleted)
|
||||||
local function build_stderr_handler(on_open)
|
local function build_stderr_handler(on_open)
|
||||||
@@ -97,17 +86,6 @@ local function build_stderr_handler(on_open)
|
|||||||
return on_stderr, finalize
|
return on_stderr, finalize
|
||||||
end
|
end
|
||||||
|
|
||||||
---Run a git command with an editor proxy active. When git invokes the
|
|
||||||
---editor, `on_open` fires with the absolute file path git wants edited
|
|
||||||
---plus a `done` callback. The caller opens the file in a buffer and
|
|
||||||
---invokes `done()` once the user is finished (typically from a
|
|
||||||
---`BufWipeout` autocmd). For commands that fire the editor more than
|
|
||||||
---once in a single git invocation (`git rebase -i`, with one call for
|
|
||||||
---the todo and one per `reword`), `on_open` is invoked once per
|
|
||||||
---editor handoff.
|
|
||||||
---
|
|
||||||
---`on_exit` is called on the main loop with a `vim.SystemCompleted`-
|
|
||||||
---shaped result. `result.stderr` has our protocol sentinels stripped.
|
|
||||||
---@param cmd string[]
|
---@param cmd string[]
|
||||||
---@param opts? { cwd?: string, env?: table<string,string> }
|
---@param opts? { cwd?: string, env?: table<string,string> }
|
||||||
---@param on_open fun(file_path: string, done: fun())
|
---@param on_open fun(file_path: string, done: fun())
|
||||||
|
|||||||
+19
-19
@@ -1,11 +1,3 @@
|
|||||||
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 sidebar = require("git.sidebar")
|
|
||||||
|
|
||||||
local HIGHLIGHTS = {
|
local HIGHLIGHTS = {
|
||||||
GitDeleted = "Removed",
|
GitDeleted = "Removed",
|
||||||
GitIgnored = "Comment",
|
GitIgnored = "Comment",
|
||||||
@@ -28,10 +20,16 @@ end
|
|||||||
---@param path string
|
---@param path string
|
||||||
---@return string?
|
---@return string?
|
||||||
function M.head(path)
|
function M.head(path)
|
||||||
return repo.head(path)
|
return require("git.repo").head(path)
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.setup()
|
function M.setup()
|
||||||
|
local cmd = require("git.cmd")
|
||||||
|
local commit = require("git.commit")
|
||||||
|
local diff = require("git.diff")
|
||||||
|
local log = require("git.log")
|
||||||
|
local repo = require("git.repo")
|
||||||
|
|
||||||
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 })
|
||||||
end
|
end
|
||||||
@@ -40,7 +38,7 @@ function M.setup()
|
|||||||
pattern = "git://*",
|
pattern = "git://*",
|
||||||
group = group,
|
group = group,
|
||||||
callback = function(args)
|
callback = function(args)
|
||||||
object.read_uri(args.buf)
|
require("git.object").read_uri(args.buf)
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
vim.api.nvim_create_autocmd("BufReadCmd", {
|
vim.api.nvim_create_autocmd("BufReadCmd", {
|
||||||
@@ -81,21 +79,23 @@ function M.setup()
|
|||||||
vim.keymap.set(
|
vim.keymap.set(
|
||||||
"n",
|
"n",
|
||||||
"<leader>gg",
|
"<leader>gg",
|
||||||
sidebar.toggle,
|
require("git.sidebar").toggle,
|
||||||
{ desc = "Toggle git status sidebar" }
|
{ desc = "Toggle git status sidebar" }
|
||||||
)
|
)
|
||||||
vim.keymap.set("n", "<leader>gl", log.open, { desc = "Show git log" })
|
vim.keymap.set("n", "<leader>gl", function()
|
||||||
|
log.open({ max_count = 1000 })
|
||||||
|
end, { desc = "Show git log" })
|
||||||
vim.keymap.set("n", "<leader>gd", function()
|
vim.keymap.set("n", "<leader>gd", function()
|
||||||
diff.split({ revspec = "", vertical = true })
|
diff.split({ 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()
|
||||||
diff.split({ revspec = "HEAD", vertical = true })
|
diff.split({ rev = "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()
|
||||||
diff.split({ revspec = "", vertical = false })
|
diff.split({ 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()
|
||||||
diff.split({ revspec = "HEAD", vertical = false })
|
diff.split({ rev = "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()
|
||||||
commit.commit()
|
commit.commit()
|
||||||
@@ -110,7 +110,7 @@ function M.setup()
|
|||||||
local function diff_split_cmd(vertical)
|
local function diff_split_cmd(vertical)
|
||||||
return function(opts)
|
return function(opts)
|
||||||
diff.split({
|
diff.split({
|
||||||
revspec = opts.args,
|
rev = opts.args ~= "" and opts.args or nil,
|
||||||
vertical = vertical,
|
vertical = vertical,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
@@ -118,12 +118,12 @@ function M.setup()
|
|||||||
vim.api.nvim_create_user_command(
|
vim.api.nvim_create_user_command(
|
||||||
"Gdiffsplit",
|
"Gdiffsplit",
|
||||||
diff_split_cmd(true),
|
diff_split_cmd(true),
|
||||||
{ nargs = "?", desc = "Diff against <revspec> (vsplit)" }
|
{ nargs = "?", desc = "Diff against <rev> (vsplit)" }
|
||||||
)
|
)
|
||||||
vim.api.nvim_create_user_command(
|
vim.api.nvim_create_user_command(
|
||||||
"Ghdiffsplit",
|
"Ghdiffsplit",
|
||||||
diff_split_cmd(false),
|
diff_split_cmd(false),
|
||||||
{ nargs = "?", desc = "Diff against <revspec> (split)" }
|
{ nargs = "?", desc = "Diff against <rev> (split)" }
|
||||||
)
|
)
|
||||||
|
|
||||||
cmd.setup()
|
cmd.setup()
|
||||||
|
|||||||
+5
-12
@@ -4,11 +4,10 @@ local util = require("git.util")
|
|||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
local LOG_FORMAT = "%h %ad {%an}%d %s"
|
local LOG_FORMAT = "%h %ad {%an}%d %s"
|
||||||
local DEFAULT_MAX_COUNT = 1000
|
|
||||||
local URI_PREFIX = "gitlog://"
|
local URI_PREFIX = "gitlog://"
|
||||||
|
|
||||||
---@param worktree string
|
---@param worktree string
|
||||||
---@param max_count integer
|
---@param max_count integer?
|
||||||
---@return string?
|
---@return string?
|
||||||
local function fetch(worktree, max_count)
|
local function fetch(worktree, max_count)
|
||||||
local cmd = {
|
local cmd = {
|
||||||
@@ -20,7 +19,7 @@ local function fetch(worktree, max_count)
|
|||||||
"--date=short",
|
"--date=short",
|
||||||
"--format=format:" .. LOG_FORMAT,
|
"--format=format:" .. LOG_FORMAT,
|
||||||
}
|
}
|
||||||
if max_count > 0 then
|
if max_count then
|
||||||
table.insert(cmd, "--max-count=" .. max_count)
|
table.insert(cmd, "--max-count=" .. max_count)
|
||||||
end
|
end
|
||||||
return util.exec(cmd, { cwd = worktree })
|
return util.exec(cmd, { cwd = worktree })
|
||||||
@@ -29,8 +28,7 @@ end
|
|||||||
---@param buf integer
|
---@param buf integer
|
||||||
local function populate(buf)
|
local function populate(buf)
|
||||||
local worktree = vim.b[buf].git_worktree
|
local worktree = vim.b[buf].git_worktree
|
||||||
local max_count = vim.b[buf].git_log_max_count or DEFAULT_MAX_COUNT
|
local stdout = fetch(worktree, vim.b[buf].git_log_max_count)
|
||||||
local stdout = fetch(worktree, max_count)
|
|
||||||
if not stdout then
|
if not stdout then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -64,7 +62,6 @@ local function populate(buf)
|
|||||||
vim.bo[buf].modified = false
|
vim.bo[buf].modified = false
|
||||||
end
|
end
|
||||||
|
|
||||||
---BufReadCmd handler for `gitlog://<worktree>` URIs.
|
|
||||||
---@param buf integer
|
---@param buf integer
|
||||||
function M.read_uri(buf)
|
function M.read_uri(buf)
|
||||||
local name = vim.api.nvim_buf_get_name(buf)
|
local name = vim.api.nvim_buf_get_name(buf)
|
||||||
@@ -77,8 +74,6 @@ function M.read_uri(buf)
|
|||||||
vim.bo[buf].swapfile = false
|
vim.bo[buf].swapfile = false
|
||||||
vim.bo[buf].bufhidden = "hide"
|
vim.bo[buf].bufhidden = "hide"
|
||||||
vim.bo[buf].buftype = "nofile"
|
vim.bo[buf].buftype = "nofile"
|
||||||
-- Skip the assignment when ft is already set so re-runs don't
|
|
||||||
-- re-fire `FileType` autocmds (ftplugin reload, treesitter attach).
|
|
||||||
if vim.bo[buf].filetype ~= "gitlog" then
|
if vim.bo[buf].filetype ~= "gitlog" then
|
||||||
vim.bo[buf].filetype = "gitlog"
|
vim.bo[buf].filetype = "gitlog"
|
||||||
end
|
end
|
||||||
@@ -87,7 +82,7 @@ function M.read_uri(buf)
|
|||||||
end
|
end
|
||||||
|
|
||||||
---@class ow.Git.LogOpts
|
---@class ow.Git.LogOpts
|
||||||
---@field max_count integer? cap on commits to show. Nil uses the default, <= 0 means "all"
|
---@field max_count integer?
|
||||||
|
|
||||||
---@param opts ow.Git.LogOpts?
|
---@param opts ow.Git.LogOpts?
|
||||||
function M.open(opts)
|
function M.open(opts)
|
||||||
@@ -100,7 +95,7 @@ function M.open(opts)
|
|||||||
|
|
||||||
local buf = vim.fn.bufadd(URI_PREFIX .. worktree)
|
local buf = vim.fn.bufadd(URI_PREFIX .. worktree)
|
||||||
vim.b[buf].git_worktree = worktree
|
vim.b[buf].git_worktree = worktree
|
||||||
vim.b[buf].git_log_max_count = opts.max_count or DEFAULT_MAX_COUNT
|
vim.b[buf].git_log_max_count = opts.max_count
|
||||||
local was_loaded = vim.api.nvim_buf_is_loaded(buf)
|
local was_loaded = vim.api.nvim_buf_is_loaded(buf)
|
||||||
|
|
||||||
local win = vim.fn.bufwinid(buf)
|
local win = vim.fn.bufwinid(buf)
|
||||||
@@ -110,8 +105,6 @@ function M.open(opts)
|
|||||||
vim.api.nvim_set_current_win(win)
|
vim.api.nvim_set_current_win(win)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- `place_buf` triggers `bufload` -> `read_uri` for a fresh buffer.
|
|
||||||
-- An already-loaded buffer needs an explicit refresh.
|
|
||||||
if was_loaded then
|
if was_loaded then
|
||||||
populate(buf)
|
populate(buf)
|
||||||
end
|
end
|
||||||
|
|||||||
+77
-126
@@ -1,36 +1,30 @@
|
|||||||
local diff = require("git.diff")
|
local Revision = require("git.revision")
|
||||||
local repo = require("git.repo")
|
local repo = require("git.repo")
|
||||||
local util = require("git.util")
|
local util = require("git.util")
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
---@class ow.Git.DiffSection
|
---@class ow.Git.DiffSection
|
||||||
---@field pre_path string path on the parent side (`a/...`)
|
---@field path_a string
|
||||||
---@field post_path string path on the current side (`b/...`)
|
---@field path_b string
|
||||||
---@field pre_blob string?
|
---@field blob_a string?
|
||||||
---@field post_blob string?
|
---@field blob_b string?
|
||||||
|
|
||||||
---@class ow.Git.BufContext
|
---@class ow.Git.BufContext
|
||||||
---@field worktree string
|
---@field worktree string
|
||||||
---@field ref string resolved commit SHA of the gitobject buffer
|
---@field sha string
|
||||||
---@field parent_ref string? resolved parent commit SHA, nil for root commits
|
---@field parent_sha string?
|
||||||
|
|
||||||
---@return ow.Git.BufContext?
|
---@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 sha = vim.b.git_sha
|
||||||
if not worktree or not ref then
|
if not worktree or not sha then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
return { worktree = worktree, ref = ref, parent_ref = vim.b.git_parent_ref }
|
return { worktree = worktree, sha = sha, parent_sha = vim.b.git_parent_sha }
|
||||||
end
|
end
|
||||||
|
|
||||||
---Find the enclosing `diff --git` line and parse the section's pre/post
|
|
||||||
---paths plus the pre/post blob SHAs from the `index` line.
|
|
||||||
---
|
|
||||||
---Uses `vim.fn.search('bcnW')` (backward, accept cursor pos, no move, no
|
|
||||||
---wrap) so a giant `git show <merge>` buffer doesn't pay an O(cursor_lnum)
|
|
||||||
---array allocation on every <CR>.
|
|
||||||
---@return ow.Git.DiffSection?
|
---@return ow.Git.DiffSection?
|
||||||
local function diff_section()
|
local function diff_section()
|
||||||
local diff_lnum = vim.fn.search("^diff --git ", "bcnW")
|
local diff_lnum = vim.fn.search("^diff --git ", "bcnW")
|
||||||
@@ -42,33 +36,30 @@ local function diff_section()
|
|||||||
if not diff_line then
|
if not diff_line then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
local pre_path, post_path = diff_line:match("^diff %-%-git a/(.-) b/(.+)$")
|
local path_a, path_b = diff_line:match("^diff %-%-git a/(.-) b/(.+)$")
|
||||||
if not pre_path or not post_path then
|
if not path_a or not path_b then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Header lines (mode/index/oldfile/newfile/etc) sit between the
|
|
||||||
-- `diff --git` line and the first `@@` hunk. Cap the read at 20 to
|
|
||||||
-- bound work even for unusual diff headers.
|
|
||||||
local header =
|
local header =
|
||||||
vim.api.nvim_buf_get_lines(0, diff_lnum, diff_lnum + 20, false)
|
vim.api.nvim_buf_get_lines(0, diff_lnum, diff_lnum + 20, false)
|
||||||
local pre_blob, post_blob
|
local blob_a, blob_b
|
||||||
for _, l in ipairs(header) do
|
for _, l in ipairs(header) do
|
||||||
if l:sub(1, 3) == "@@ " or l:match("^diff %-%-git ") then
|
if l:sub(1, 3) == "@@ " or l:match("^diff %-%-git ") then
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
local pre, post = l:match("^index (%x+)%.%.(%x+)")
|
local pre, post = l:match("^index (%x+)%.%.(%x+)")
|
||||||
if pre then
|
if pre then
|
||||||
pre_blob = pre
|
blob_a = pre
|
||||||
post_blob = post
|
blob_b = post
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return {
|
return {
|
||||||
pre_path = pre_path,
|
path_a = path_a,
|
||||||
post_path = post_path,
|
path_b = path_b,
|
||||||
pre_blob = pre_blob,
|
blob_a = blob_a,
|
||||||
post_blob = post_blob,
|
blob_b = blob_b,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -78,9 +69,6 @@ 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 buf integer
|
||||||
---@param worktree string
|
---@param worktree string
|
||||||
---@param path string
|
---@param path string
|
||||||
@@ -115,9 +103,9 @@ local function attach_index_writer(buf, worktree, path)
|
|||||||
end
|
end
|
||||||
vim.b[buf].git_index_mode = mode
|
vim.b[buf].git_index_mode = mode
|
||||||
end
|
end
|
||||||
-- Use the 3-arg form (mode sha path) instead of the comma form
|
-- Use the 3-arg form (mode sha path) instead of the comma
|
||||||
-- (mode,sha,path), which doesn't survive paths containing a
|
-- form (mode,sha,path), which doesn't survive paths
|
||||||
-- comma.
|
-- containing a comma.
|
||||||
if
|
if
|
||||||
not util.exec({
|
not util.exec({
|
||||||
"git",
|
"git",
|
||||||
@@ -135,18 +123,15 @@ local function attach_index_writer(buf, worktree, path)
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
---Pre-fetched content keyed by bufnr, consumed once by `read_uri`.
|
|
||||||
---@type table<integer, string>
|
---@type table<integer, string>
|
||||||
local pending_content = {}
|
local pending_content = {}
|
||||||
|
|
||||||
---Return a `git://<revspec>` URI buffer. Pass `content` to prime
|
|
||||||
---`read_uri`'s cache and skip its `cat-file -p` fetch.
|
|
||||||
---@param worktree string
|
---@param worktree string
|
||||||
---@param revspec string any revspec git understands (e.g. `HEAD:foo`, `:foo`, `:1:foo`, `<sha>`, `<sha>:foo`)
|
---@param rev ow.Git.Revision
|
||||||
---@param content string?
|
---@param content string?
|
||||||
---@return integer
|
---@return integer
|
||||||
function M.buf_for(worktree, revspec, content)
|
function M.buf_for(worktree, rev, content)
|
||||||
local buf = vim.fn.bufadd(util.uri(revspec))
|
local buf = vim.fn.bufadd(rev:uri())
|
||||||
vim.b[buf].git_worktree = worktree
|
vim.b[buf].git_worktree = worktree
|
||||||
if content then
|
if content then
|
||||||
pending_content[buf] = content
|
pending_content[buf] = content
|
||||||
@@ -155,17 +140,14 @@ function M.buf_for(worktree, revspec, content)
|
|||||||
return buf
|
return buf
|
||||||
end
|
end
|
||||||
|
|
||||||
---BufReadCmd handler for `git://<revspec>` URIs. Worktree comes from
|
|
||||||
---`b:git_worktree` if set, else from cwd. Stage-0 index entries
|
|
||||||
---(`:<path>`) are made writable so `:w` updates the index. Other
|
|
||||||
---revspecs are read-only.
|
|
||||||
---@param buf integer
|
---@param buf integer
|
||||||
function M.read_uri(buf)
|
function M.read_uri(buf)
|
||||||
local name = vim.api.nvim_buf_get_name(buf)
|
local name = vim.api.nvim_buf_get_name(buf)
|
||||||
local revspec = util.parse_uri(name)
|
local rev = Revision.from_uri(name)
|
||||||
if not revspec then
|
if not rev then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
local rev_str = rev:format()
|
||||||
|
|
||||||
local worktree = vim.b[buf].git_worktree or select(2, repo.resolve_cwd())
|
local worktree = vim.b[buf].git_worktree or select(2, repo.resolve_cwd())
|
||||||
if not worktree then
|
if not worktree then
|
||||||
@@ -182,25 +164,14 @@ function M.read_uri(buf)
|
|||||||
pending_content[buf] = nil
|
pending_content[buf] = nil
|
||||||
if stdout == nil then
|
if stdout == nil then
|
||||||
stdout = util.exec(
|
stdout = util.exec(
|
||||||
{ "git", "cat-file", "-p", revspec },
|
{ "git", "cat-file", "-p", rev_str },
|
||||||
{ cwd = worktree }
|
{ cwd = worktree }
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
local parsed = util.parse_revspec(revspec)
|
if stdout and rev.path == nil then
|
||||||
|
|
||||||
-- Bare-ref objects that dereference to a commit (commits, stashes,
|
|
||||||
-- annotated tags pointing at a commit, lightweight tags) get their
|
|
||||||
-- `diff-tree -p` patch appended so the buffer is navigable. The
|
|
||||||
-- `<CR>` parser walks `diff --git` blocks. `^{commit}` is git's
|
|
||||||
-- standard "deref to commit" suffix. rev-parse fails for non-commit
|
|
||||||
-- objects (trees, blobs, tags pointing at non-commits) so they
|
|
||||||
-- naturally skip the append. `-m --first-parent` collapses merges
|
|
||||||
-- and stashes into one diff per file (vs `diff --cc` combined
|
|
||||||
-- diffs, which the parser can't follow).
|
|
||||||
if stdout and parsed.path == nil then
|
|
||||||
local commit_sha =
|
local commit_sha =
|
||||||
repo.rev_parse(worktree, revspec .. "^{commit}", true)
|
repo.rev_parse(worktree, rev_str .. "^{commit}", true)
|
||||||
if commit_sha then
|
if commit_sha then
|
||||||
local patch = util.exec({
|
local patch = util.exec({
|
||||||
"git",
|
"git",
|
||||||
@@ -215,31 +186,25 @@ function M.read_uri(buf)
|
|||||||
if patch then
|
if patch then
|
||||||
stdout = (stdout:gsub("\n*$", "\n\n")) .. patch
|
stdout = (stdout:gsub("\n*$", "\n\n")) .. patch
|
||||||
end
|
end
|
||||||
vim.b[buf].git_parent_ref =
|
vim.b[buf].git_parent_sha =
|
||||||
repo.rev_parse(worktree, commit_sha .. "^", true)
|
repo.rev_parse(worktree, commit_sha .. "^", true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if stdout then
|
if stdout then
|
||||||
-- Reload paths (`:e`, `<C-o>` to an unloaded buf) re-enter
|
|
||||||
-- with `modifiable = false` from the prior load.
|
|
||||||
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))
|
||||||
end
|
end
|
||||||
|
|
||||||
-- `b:git_ref` anchors `<CR>`-driven navigation in this buffer.
|
local rev_sha = repo.rev_parse(worktree, rev_str, true)
|
||||||
local ref_sha = repo.rev_parse(worktree, revspec, true)
|
if rev_sha then
|
||||||
if ref_sha then
|
vim.b[buf].git_sha = rev_sha
|
||||||
vim.b[buf].git_ref = ref_sha
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if parsed.stage == 0 and parsed.path then
|
if rev.stage == 0 and rev.path then
|
||||||
vim.bo[buf].buftype = "acwrite"
|
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
|
if not vim.b[buf].git_index_writer then
|
||||||
attach_index_writer(buf, worktree, parsed.path)
|
attach_index_writer(buf, worktree, rev.path)
|
||||||
vim.b[buf].git_index_writer = true
|
vim.b[buf].git_index_writer = true
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
@@ -248,15 +213,12 @@ function M.read_uri(buf)
|
|||||||
end
|
end
|
||||||
vim.bo[buf].modified = false
|
vim.bo[buf].modified = false
|
||||||
|
|
||||||
-- Filetype from the inner path. We can't lean on `vim.filetype.add`
|
-- Match on the inner path directly. `vim.filetype.add` patterns
|
||||||
-- because Vim normalises `git://` filenames (cwd-prefix + collapses
|
-- don't work because Vim normalises `git://` filenames (cwd-prefix
|
||||||
-- `://` to `:/`) before matching, breaking any pattern keyed on the
|
-- + `://` -> `:/`) before matching, breaking any pattern keyed on
|
||||||
-- raw scheme as well as any built-in pattern that doesn't catch a
|
-- the raw scheme.
|
||||||
-- recognisable extension on the mangled form (.Xresources is the
|
if rev.path then
|
||||||
-- canonical example). Bare-ref content (commit/tag headers + tree
|
local ft = vim.filetype.match({ filename = rev.path, buf = buf })
|
||||||
-- listings) uses the built-in `git` filetype.
|
|
||||||
if parsed.path then
|
|
||||||
local ft = vim.filetype.match({ filename = parsed.path, buf = buf })
|
|
||||||
if ft then
|
if ft then
|
||||||
vim.bo[buf].filetype = ft
|
vim.bo[buf].filetype = ft
|
||||||
end
|
end
|
||||||
@@ -264,34 +226,29 @@ function M.read_uri(buf)
|
|||||||
vim.bo[buf].filetype = "git"
|
vim.bo[buf].filetype = "git"
|
||||||
end
|
end
|
||||||
|
|
||||||
-- BufReadCmd suppresses the normal BufReadPost dispatch, so
|
|
||||||
-- modeline parsing doesn't run unless we fire it ourselves. The
|
|
||||||
-- modeline can still override the filetype set above (standard Vim
|
|
||||||
-- precedence).
|
|
||||||
vim.api.nvim_exec_autocmds("BufReadPost", { buffer = buf })
|
vim.api.nvim_exec_autocmds("BufReadPost", { buffer = buf })
|
||||||
end
|
end
|
||||||
|
|
||||||
---Buffer for the file at `<ref>:<path>`. Returns nil for a zero/nil blob.
|
|
||||||
---@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 sha string
|
||||||
---@return integer?
|
---@return integer?
|
||||||
local function blob_buf(worktree, blob, path, ref)
|
local function blob_buf(worktree, blob, path, sha)
|
||||||
if is_zero(blob) then
|
if is_zero(blob) then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
return M.buf_for(worktree, ref .. ":" .. path)
|
return M.buf_for(worktree, Revision.new({ base = sha, path = path }))
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param worktree string
|
---@param worktree string
|
||||||
---@param blob string?
|
---@param blob string?
|
||||||
---@param path string
|
---@param path string
|
||||||
---@param ref string
|
---@param sha string
|
||||||
local function load_blob(worktree, blob, path, ref)
|
local function load_blob(worktree, blob, path, sha)
|
||||||
local buf = blob_buf(worktree, blob, path, ref)
|
local buf = blob_buf(worktree, blob, path, sha)
|
||||||
if not buf then
|
if not buf then
|
||||||
util.warning("no content for %s at %s", path, ref)
|
util.warning("no content for %s at %s", path, sha)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
vim.cmd.normal({ "m'", bang = true })
|
vim.cmd.normal({ "m'", bang = true })
|
||||||
@@ -301,21 +258,20 @@ end
|
|||||||
---@param ctx ow.Git.BufContext
|
---@param ctx ow.Git.BufContext
|
||||||
---@param section ow.Git.DiffSection
|
---@param section ow.Git.DiffSection
|
||||||
local function open_section(ctx, section)
|
local function open_section(ctx, section)
|
||||||
if not section.pre_blob or not section.post_blob then
|
if not section.blob_a or not section.blob_b then
|
||||||
util.warning("no index line, cannot determine blob SHAs")
|
util.warning("no index line, cannot determine blob SHAs")
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local parent = ctx.parent_ref or "0"
|
local parent = ctx.parent_sha or "0"
|
||||||
local left =
|
local left = blob_buf(ctx.worktree, section.blob_a, section.path_a, 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.blob_b, section.path_b, ctx.sha)
|
||||||
if left and right then
|
if left and right then
|
||||||
diff.open(left, right, true)
|
require("git.diff").open(left, right, true)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if not left and not right then
|
if not left and not right then
|
||||||
util.warning("no content for %s", section.post_path)
|
util.warning("no content for %s", section.path_b)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local buf = left or right
|
local buf = left or right
|
||||||
@@ -325,36 +281,32 @@ local function open_section(ctx, section)
|
|||||||
end
|
end
|
||||||
|
|
||||||
---@class ow.Git.OpenObjectOpts
|
---@class ow.Git.OpenObjectOpts
|
||||||
---@field split (false|"above"|"below"|"left"|"right")? forwarded to `util.place_buf`. Default opens a new horizontal split.
|
---@field split (false|"above"|"below"|"left"|"right")?
|
||||||
|
|
||||||
---Open any git object. Accepts a bare ref (commit / tree / blob / tag
|
|
||||||
---sha, branch, tag name, `stash@{N}`, etc.) or `<commit-ref>:<path>`.
|
|
||||||
---Resolves to a sha so the URI stays stable if the ref later moves.
|
|
||||||
---@param worktree string
|
---@param worktree string
|
||||||
---@param ref string
|
---@param rev string
|
||||||
---@param opts ow.Git.OpenObjectOpts?
|
---@param opts ow.Git.OpenObjectOpts?
|
||||||
function M.open_object(worktree, ref, opts)
|
function M.open_object(worktree, rev, opts)
|
||||||
local commit_ref, path = ref:match("^(.-):(.+)$")
|
local parsed = Revision.parse(rev)
|
||||||
local revspec
|
if parsed.base then
|
||||||
if commit_ref then
|
local sha = repo.rev_parse(worktree, parsed.base, true)
|
||||||
local sha = repo.rev_parse(worktree, commit_ref, true) or commit_ref
|
if sha then
|
||||||
revspec = sha .. ":" .. path
|
parsed.base = sha
|
||||||
else
|
end
|
||||||
revspec = repo.rev_parse(worktree, ref, true) or ref
|
|
||||||
end
|
end
|
||||||
local content = util.exec(
|
local content = util.exec(
|
||||||
{ "git", "cat-file", "-p", revspec },
|
{ "git", "cat-file", "-p", parsed:format() },
|
||||||
{ cwd = worktree, silent = true }
|
{ cwd = worktree, silent = true }
|
||||||
)
|
)
|
||||||
if not content then
|
if not content then
|
||||||
util.warning("not a git object: %s", ref)
|
util.warning("not a git object: %s", rev)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local buf = M.buf_for(worktree, revspec, content)
|
local buf = M.buf_for(worktree, parsed, content)
|
||||||
util.place_buf(buf, opts and opts.split)
|
util.place_buf(buf, opts and opts.split)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@return boolean dispatched true if the cursor was on an actionable line
|
---@return boolean dispatched
|
||||||
function M.open_under_cursor()
|
function M.open_under_cursor()
|
||||||
local ctx = context()
|
local ctx = context()
|
||||||
if not ctx then
|
if not ctx then
|
||||||
@@ -372,14 +324,13 @@ function M.open_under_cursor()
|
|||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Blobs navigate by path so the URI carries the entry name (filetype
|
|
||||||
-- detection wants the extension). Other types 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
|
||||||
local nav_ref = entry_type == "blob" and (ctx.ref .. ":" .. entry_name)
|
local nav_rev = entry_type == "blob"
|
||||||
|
and Revision.new({ base = ctx.sha, path = entry_name }):format()
|
||||||
or entry_sha
|
or entry_sha
|
||||||
M.open_object(ctx.worktree, nav_ref, { split = false })
|
M.open_object(ctx.worktree, nav_rev, { split = false })
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -387,26 +338,26 @@ function M.open_under_cursor()
|
|||||||
if not section then
|
if not section then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
local parent = ctx.parent_ref or "0"
|
local parent = ctx.parent_sha or "0"
|
||||||
|
|
||||||
if line:match("^diff %-%-git ") then
|
if line:match("^diff %-%-git ") then
|
||||||
open_section(ctx, section)
|
open_section(ctx, section)
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
if line:match("^%-%-%- ") then
|
if line:match("^%-%-%- ") then
|
||||||
load_blob(ctx.worktree, section.pre_blob, section.pre_path, parent)
|
load_blob(ctx.worktree, section.blob_a, section.path_a, parent)
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
if line:match("^%+%+%+ ") then
|
if line:match("^%+%+%+ ") then
|
||||||
load_blob(ctx.worktree, section.post_blob, section.post_path, ctx.ref)
|
load_blob(ctx.worktree, section.blob_b, section.path_b, ctx.sha)
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
local prefix = line:sub(1, 1)
|
local prefix = line:sub(1, 1)
|
||||||
if prefix == "+" then
|
if prefix == "+" then
|
||||||
load_blob(ctx.worktree, section.post_blob, section.post_path, ctx.ref)
|
load_blob(ctx.worktree, section.blob_b, section.path_b, ctx.sha)
|
||||||
return true
|
return true
|
||||||
elseif prefix == "-" then
|
elseif prefix == "-" then
|
||||||
load_blob(ctx.worktree, section.pre_blob, section.pre_path, parent)
|
load_blob(ctx.worktree, section.blob_a, section.path_a, parent)
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
return false
|
return false
|
||||||
|
|||||||
+7
-25
@@ -12,7 +12,7 @@ M.UNMERGED = {
|
|||||||
UU = true,
|
UU = true,
|
||||||
}
|
}
|
||||||
|
|
||||||
---@param code string porcelain v1 XY code
|
---@param code string
|
||||||
---@return string? char
|
---@return string? char
|
||||||
---@return string? hl_group
|
---@return string? hl_group
|
||||||
function M.indicator(code)
|
function M.indicator(code)
|
||||||
@@ -84,10 +84,6 @@ function M.resolve(path)
|
|||||||
return vim.fs.normalize(gitdir), worktree
|
return vim.fs.normalize(gitdir), worktree
|
||||||
end
|
end
|
||||||
|
|
||||||
---Resolve the gitdir/worktree from the current buffer's file path,
|
|
||||||
---falling back to `vim.fn.getcwd()` when the buffer is unnamed or
|
|
||||||
---carries a synthetic URI (`git://`, `gitlog://`) that isn't a real
|
|
||||||
---filesystem path. Returns nil for both when not inside a git repo.
|
|
||||||
---@return string? gitdir
|
---@return string? gitdir
|
||||||
---@return string? worktree
|
---@return string? worktree
|
||||||
function M.resolve_cwd()
|
function M.resolve_cwd()
|
||||||
@@ -101,7 +97,7 @@ end
|
|||||||
---@class ow.Git.Repo
|
---@class ow.Git.Repo
|
||||||
---@field gitdir string
|
---@field gitdir string
|
||||||
---@field worktree string
|
---@field worktree string
|
||||||
---@field buffers table<integer, true> set of registered buffer numbers
|
---@field buffers table<integer, true>
|
||||||
---@field watcher? uv.uv_fs_event_t
|
---@field watcher? uv.uv_fs_event_t
|
||||||
---@field refresh fun(self: ow.Git.Repo)
|
---@field refresh fun(self: ow.Git.Repo)
|
||||||
---@field refresh_handle ow.Git.Util.DebounceHandle
|
---@field refresh_handle ow.Git.Util.DebounceHandle
|
||||||
@@ -121,9 +117,8 @@ end
|
|||||||
|
|
||||||
function Repo:stop_watcher()
|
function Repo:stop_watcher()
|
||||||
-- Stop the libuv watcher first so no further fs-events can trigger
|
-- Stop the libuv watcher first so no further fs-events can trigger
|
||||||
-- self:refresh(). Only then tear down the debounce handle. The reverse
|
-- self:refresh(). Tearing down the debounce handle first leaves a
|
||||||
-- order leaves a window where an in-flight watcher callback would call
|
-- window where an in-flight watcher callback hits a closed timer.
|
||||||
-- a closed debounce timer.
|
|
||||||
if self.watcher then
|
if self.watcher then
|
||||||
self.watcher:stop()
|
self.watcher:stop()
|
||||||
self.watcher:close()
|
self.watcher:close()
|
||||||
@@ -149,11 +144,6 @@ end
|
|||||||
|
|
||||||
---@param repo ow.Git.Repo
|
---@param repo ow.Git.Repo
|
||||||
local function do_refresh(repo)
|
local function do_refresh(repo)
|
||||||
-- `--branch` adds the `## branch...upstream [ahead/behind]` line that
|
|
||||||
-- the sidebar parses. The per-buffer indicator only needs the XY +
|
|
||||||
-- path lines, so it ignores `##` lines below. Running with `--branch`
|
|
||||||
-- lets the sidebar reuse this single subprocess via the GitRefresh
|
|
||||||
-- data payload instead of spawning its own.
|
|
||||||
vim.system(
|
vim.system(
|
||||||
{
|
{
|
||||||
"git",
|
"git",
|
||||||
@@ -173,10 +163,6 @@ local function do_refresh(repo)
|
|||||||
local x = code:sub(1, 1)
|
local x = code:sub(1, 1)
|
||||||
local y = code:sub(2, 2)
|
local y = code:sub(2, 2)
|
||||||
local path_part = line:sub(4)
|
local path_part = line:sub(4)
|
||||||
-- ` -> ` only appears in renames/copies. Without
|
|
||||||
-- this guard, a literal filename containing the
|
|
||||||
-- arrow (rare with `core.quotePath=false`) would
|
|
||||||
-- be mis-parsed.
|
|
||||||
if x == "R" or x == "C" or y == "R" or y == "C" then
|
if x == "R" or x == "C" or y == "R" or y == "C" then
|
||||||
local arrow = path_part:find(" -> ", 1, true)
|
local arrow = path_part:find(" -> ", 1, true)
|
||||||
if arrow then
|
if arrow then
|
||||||
@@ -325,20 +311,16 @@ function M.head(path)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
---Resolve a git revision to its object SHA. Returns nil if the ref can't
|
|
||||||
---be parsed (root-commit's `^`, blob's `^`, malformed ref, etc.). When
|
|
||||||
---`short` is true, the result is abbreviated via `core.abbrev`
|
|
||||||
---(auto-extended by git to keep the prefix unique in the current repo).
|
|
||||||
---@param worktree string
|
---@param worktree string
|
||||||
---@param ref string
|
---@param rev string
|
||||||
---@param short boolean
|
---@param short boolean
|
||||||
---@return string?
|
---@return string?
|
||||||
function M.rev_parse(worktree, ref, short)
|
function M.rev_parse(worktree, rev, short)
|
||||||
local cmd = { "git", "rev-parse", "--verify", "--quiet" }
|
local cmd = { "git", "rev-parse", "--verify", "--quiet" }
|
||||||
if short then
|
if short then
|
||||||
table.insert(cmd, "--short")
|
table.insert(cmd, "--short")
|
||||||
end
|
end
|
||||||
table.insert(cmd, ref)
|
table.insert(cmd, rev)
|
||||||
local stdout = util.exec(cmd, { cwd = worktree, silent = true })
|
local stdout = util.exec(cmd, { cwd = worktree, silent = true })
|
||||||
local trimmed = stdout and vim.trim(stdout) or ""
|
local trimmed = stdout and vim.trim(stdout) or ""
|
||||||
return trimmed ~= "" and trimmed or nil
|
return trimmed ~= "" and trimmed or nil
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
---@class ow.Git.Revision
|
||||||
|
---@field stage 0|1|2|3?
|
||||||
|
---@field path string?
|
||||||
|
---@field base string?
|
||||||
|
local Revision = {}
|
||||||
|
Revision.__index = Revision
|
||||||
|
|
||||||
|
local URI_PREFIX = "git://"
|
||||||
|
|
||||||
|
---@return string
|
||||||
|
function Revision:format()
|
||||||
|
if self.stage then
|
||||||
|
return ":" .. self.stage .. ":" .. self.path
|
||||||
|
elseif self.path then
|
||||||
|
return self.base .. ":" .. self.path
|
||||||
|
end
|
||||||
|
return self.base or error("Revision:format: empty Revision")
|
||||||
|
end
|
||||||
|
|
||||||
|
---@return string
|
||||||
|
function Revision:uri()
|
||||||
|
return URI_PREFIX .. self:format()
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param parts { stage?: integer, base?: string, path?: string }
|
||||||
|
---@return ow.Git.Revision
|
||||||
|
function Revision.new(parts)
|
||||||
|
return setmetatable(parts, Revision)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param str string
|
||||||
|
---@return ow.Git.Revision
|
||||||
|
function Revision.parse(str)
|
||||||
|
local stage, path = str:match("^:([0123]):(.+)$")
|
||||||
|
if stage then
|
||||||
|
return Revision.new({
|
||||||
|
stage = tonumber(stage) --[[@as (0|1|2|3)?]],
|
||||||
|
path = path,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
path = str:match("^:([^:]+)$")
|
||||||
|
if path then
|
||||||
|
return Revision.new({ stage = 0, path = path })
|
||||||
|
end
|
||||||
|
local base, p = str:match("^([^:]+):(.+)$")
|
||||||
|
if base then
|
||||||
|
return Revision.new({ base = base, path = p })
|
||||||
|
end
|
||||||
|
return Revision.new({ base = str })
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param str string
|
||||||
|
---@return ow.Git.Revision?
|
||||||
|
function Revision.from_uri(str)
|
||||||
|
local raw = str:match("^" .. URI_PREFIX .. "(.+)$")
|
||||||
|
if raw then
|
||||||
|
return Revision.parse(raw)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return Revision
|
||||||
+13
-47
@@ -1,3 +1,4 @@
|
|||||||
|
local Revision = require("git.revision")
|
||||||
local diff = require("git.diff")
|
local diff = require("git.diff")
|
||||||
local object = require("git.object")
|
local object = require("git.object")
|
||||||
local repo = require("git.repo")
|
local repo = require("git.repo")
|
||||||
@@ -19,8 +20,8 @@ local SIDEBAR_WIDTH = 50
|
|||||||
---@field section string
|
---@field section string
|
||||||
---@field path string
|
---@field path string
|
||||||
---@field orig string?
|
---@field orig string?
|
||||||
---@field x string porcelain v1 column 1 (always set, may be a literal space)
|
---@field x string
|
||||||
---@field y string porcelain v1 column 2 (always set, may be a literal space)
|
---@field y string
|
||||||
|
|
||||||
---@class ow.Git.CommitEntry
|
---@class ow.Git.CommitEntry
|
||||||
---@field section string
|
---@field section string
|
||||||
@@ -34,7 +35,7 @@ local SIDEBAR_WIDTH = 50
|
|||||||
---@field worktree string
|
---@field worktree string
|
||||||
---@field lines table<integer, ow.Git.SidebarEntry>
|
---@field lines table<integer, ow.Git.SidebarEntry>
|
||||||
---@field sidebar_win integer?
|
---@field sidebar_win integer?
|
||||||
---@field invocation_win integer? window focused when the sidebar opened. The first diff repurposes it as the right pane
|
---@field invocation_win integer?
|
||||||
---@field diff_left_win integer?
|
---@field diff_left_win integer?
|
||||||
---@field diff_right_win integer?
|
---@field diff_right_win integer?
|
||||||
---@field user_aucmd integer?
|
---@field user_aucmd integer?
|
||||||
@@ -47,7 +48,6 @@ local state = {}
|
|||||||
local group = vim.api.nvim_create_augroup("ow.git.sidebar", { clear = false })
|
local group = vim.api.nvim_create_augroup("ow.git.sidebar", { clear = false })
|
||||||
local ns = vim.api.nvim_create_namespace("ow.git.sidebar")
|
local ns = vim.api.nvim_create_namespace("ow.git.sidebar")
|
||||||
|
|
||||||
---Find the sidebar window in the current tabpage by filetype.
|
|
||||||
---@return integer? win
|
---@return integer? win
|
||||||
---@return integer? bufnr
|
---@return integer? bufnr
|
||||||
local function find_sidebar()
|
local function find_sidebar()
|
||||||
@@ -59,8 +59,6 @@ local function find_sidebar()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
---Return the sidebar window stashed on `s`, validating that it's still
|
|
||||||
---live. Falls back to `find_sidebar` if the stashed handle is gone.
|
|
||||||
---@param s ow.Git.SidebarState
|
---@param s ow.Git.SidebarState
|
||||||
---@return integer?
|
---@return integer?
|
||||||
local function sidebar_win_for(s)
|
local function sidebar_win_for(s)
|
||||||
@@ -90,7 +88,7 @@ end
|
|||||||
---@param entry ow.Git.SidebarEntry
|
---@param entry ow.Git.SidebarEntry
|
||||||
---@return string? line
|
---@return string? line
|
||||||
---@return string? hl_group
|
---@return string? hl_group
|
||||||
---@return integer? hl_len byte length of the symbol portion at column 2
|
---@return integer? hl_len
|
||||||
local function format_entry(entry)
|
local function format_entry(entry)
|
||||||
if entry.sha then
|
if entry.sha then
|
||||||
return string.format(" %s %s", entry.sha, entry.subject or ""),
|
return string.format(" %s %s", entry.sha, entry.subject or ""),
|
||||||
@@ -116,7 +114,7 @@ end
|
|||||||
---@field ahead integer
|
---@field ahead integer
|
||||||
---@field behind integer
|
---@field behind integer
|
||||||
|
|
||||||
---@param line string '## branch.line' from porcelain v1
|
---@param line string
|
||||||
---@return ow.Git.BranchInfo
|
---@return ow.Git.BranchInfo
|
||||||
local function parse_branch_line(line)
|
local function parse_branch_line(line)
|
||||||
local info = { ahead = 0, behind = 0 }
|
local info = { ahead = 0, behind = 0 }
|
||||||
@@ -142,9 +140,6 @@ local function parse_branch_line(line)
|
|||||||
return info
|
return info
|
||||||
end
|
end
|
||||||
|
|
||||||
---Parse `git status --porcelain=v1 --branch` output into a (branch, groups)
|
|
||||||
---pair. `Unpushed` and `Unpulled` start empty here. Ahead/behind commits are
|
|
||||||
---filled in by a follow-up `git log` once we know the upstream is set.
|
|
||||||
---@param stdout string
|
---@param stdout string
|
||||||
---@return ow.Git.BranchInfo, table<string, ow.Git.SidebarEntry[]>
|
---@return ow.Git.BranchInfo, table<string, ow.Git.SidebarEntry[]>
|
||||||
local function parse_porcelain(stdout)
|
local function parse_porcelain(stdout)
|
||||||
@@ -165,9 +160,6 @@ local function parse_porcelain(stdout)
|
|||||||
local y = line:sub(2, 2)
|
local y = line:sub(2, 2)
|
||||||
local rest = line:sub(4)
|
local rest = line:sub(4)
|
||||||
local orig
|
local orig
|
||||||
-- ` -> ` only appears in renames/copies (R/C codes). Without
|
|
||||||
-- this guard, a literal filename containing the arrow would
|
|
||||||
-- be mis-parsed.
|
|
||||||
if x == "R" or x == "C" or y == "R" or y == "C" then
|
if x == "R" or x == "C" or y == "R" or y == "C" then
|
||||||
local arrow = rest:find(" -> ", 1, true)
|
local arrow = rest:find(" -> ", 1, true)
|
||||||
if arrow then
|
if arrow then
|
||||||
@@ -213,9 +205,6 @@ local function parse_porcelain(stdout)
|
|||||||
return branch, groups
|
return branch, groups
|
||||||
end
|
end
|
||||||
|
|
||||||
---Fill in the Unpushed/Unpulled groups from `git log` for any non-zero
|
|
||||||
---ahead/behind counter. Capped at 200 commits per range so a wildly
|
|
||||||
---divergent branch can't blow the sidebar's render budget.
|
|
||||||
---@param worktree string
|
---@param worktree string
|
||||||
---@param branch ow.Git.BranchInfo
|
---@param branch ow.Git.BranchInfo
|
||||||
---@param groups table<string, ow.Git.SidebarEntry[]>
|
---@param groups table<string, ow.Git.SidebarEntry[]>
|
||||||
@@ -233,8 +222,6 @@ local function enrich_with_log(worktree, branch, groups)
|
|||||||
{ section = "Unpulled", range = "HEAD..@{upstream}" }
|
{ section = "Unpulled", range = "HEAD..@{upstream}" }
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
-- Submit both subprocesses before waiting so they run concurrently
|
|
||||||
-- rather than sequentially. Total time = max, not sum.
|
|
||||||
local pending = {}
|
local pending = {}
|
||||||
for _, f in ipairs(fetches) do
|
for _, f in ipairs(fetches) do
|
||||||
table.insert(pending, {
|
table.insert(pending, {
|
||||||
@@ -271,10 +258,6 @@ local function enrich_with_log(worktree, branch, groups)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
---Build the (branch, groups) tuple for the sidebar. When `prefetched_stdout`
|
|
||||||
---is provided (typical case: dispatched via the `User GitRefresh` autocmd
|
|
||||||
---that already ran `git status --porcelain=v1 --branch` for the indicator),
|
|
||||||
---we skip the duplicate subprocess. Otherwise the sidebar fetches its own.
|
|
||||||
---@param worktree string
|
---@param worktree string
|
||||||
---@param prefetched_stdout string?
|
---@param prefetched_stdout string?
|
||||||
---@param callback fun(branch: ow.Git.BranchInfo, groups: table<string, ow.Git.SidebarEntry[]>)
|
---@param callback fun(branch: ow.Git.BranchInfo, groups: table<string, ow.Git.SidebarEntry[]>)
|
||||||
@@ -372,9 +355,6 @@ local function render(bufnr, branch, groups)
|
|||||||
state[bufnr].lines = meta
|
state[bufnr].lines = meta
|
||||||
end
|
end
|
||||||
|
|
||||||
---Build a stable fingerprint of the parsed branch + groups so refresh can
|
|
||||||
---short-circuit when the porcelain state is byte-identical to the last
|
|
||||||
---successful render.
|
|
||||||
---@param branch ow.Git.BranchInfo
|
---@param branch ow.Git.BranchInfo
|
||||||
---@param groups table<string, ow.Git.SidebarEntry[]>
|
---@param groups table<string, ow.Git.SidebarEntry[]>
|
||||||
---@return string
|
---@return string
|
||||||
@@ -408,7 +388,7 @@ local function fingerprint(branch, groups)
|
|||||||
end
|
end
|
||||||
|
|
||||||
---@param bufnr integer
|
---@param bufnr integer
|
||||||
---@param prefetched_stdout string? porcelain output from a piggybacked GitRefresh
|
---@param prefetched_stdout string?
|
||||||
local function refresh(bufnr, prefetched_stdout)
|
local function refresh(bufnr, prefetched_stdout)
|
||||||
local s = state[bufnr]
|
local s = state[bufnr]
|
||||||
if not s then
|
if not s then
|
||||||
@@ -430,9 +410,6 @@ local function refresh(bufnr, prefetched_stdout)
|
|||||||
if not vim.api.nvim_buf_is_valid(bufnr) then
|
if not vim.api.nvim_buf_is_valid(bufnr) then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
-- Any fs-event that triggered this refresh might have changed the
|
|
||||||
-- worktree under the diff buffers we last opened. Invalidate the
|
|
||||||
-- 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
|
||||||
@@ -482,9 +459,10 @@ end
|
|||||||
---@param path string
|
---@param path string
|
||||||
---@return ow.Git.DiffSide
|
---@return ow.Git.DiffSide
|
||||||
local function head_pane(worktree, path)
|
local function head_pane(worktree, path)
|
||||||
|
local rev = Revision.new({ base = "HEAD", path = path })
|
||||||
return {
|
return {
|
||||||
buf = object.buf_for(worktree, "HEAD:" .. path),
|
buf = object.buf_for(worktree, rev),
|
||||||
name = util.uri("HEAD:" .. path),
|
name = rev:uri(),
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -501,9 +479,10 @@ end
|
|||||||
---@param entry ow.Git.FileEntry
|
---@param entry ow.Git.FileEntry
|
||||||
---@return ow.Git.DiffSide
|
---@return ow.Git.DiffSide
|
||||||
local function index_pane(s, entry)
|
local function index_pane(s, entry)
|
||||||
|
local rev = Revision.new({ stage = 0, path = entry.path })
|
||||||
return {
|
return {
|
||||||
buf = object.buf_for(s.worktree, ":0:" .. entry.path),
|
buf = object.buf_for(s.worktree, rev),
|
||||||
name = util.uri(":0:" .. entry.path),
|
name = rev:uri(),
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -515,7 +494,6 @@ local function older_pane(s, entry)
|
|||||||
if entry.x == "A" then
|
if entry.x == "A" then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
-- HEAD holds the pre-rename path
|
|
||||||
return head_pane(s.worktree, entry.orig or entry.path)
|
return head_pane(s.worktree, entry.orig or entry.path)
|
||||||
end
|
end
|
||||||
if entry.section == "Unstaged" then
|
if entry.section == "Unstaged" then
|
||||||
@@ -555,10 +533,6 @@ local function reset_diff_win(win)
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
---Validate the window the user was in when the sidebar opened. The first
|
|
||||||
---diff repurposes it as the right pane, regardless of whether it holds an
|
|
||||||
---empty buffer or a real file. Returns nil if the user closed it, moved
|
|
||||||
---to another tabpage, or it's somehow the sidebar itself.
|
|
||||||
---@param s ow.Git.SidebarState
|
---@param s ow.Git.SidebarState
|
||||||
---@return integer?
|
---@return integer?
|
||||||
local function invocation_win_for(s)
|
local function invocation_win_for(s)
|
||||||
@@ -613,8 +587,6 @@ local function entry_key(entry)
|
|||||||
return entry.section .. "|" .. entry.path .. "|" .. (entry.orig or "")
|
return entry.section .. "|" .. entry.path .. "|" .. (entry.orig or "")
|
||||||
end
|
end
|
||||||
|
|
||||||
---Split `target_win` to the given side. The new window inherits
|
|
||||||
---`target_win`'s buffer, which the caller swaps afterwards.
|
|
||||||
---@param target_win integer
|
---@param target_win integer
|
||||||
---@param dir "left"|"right"
|
---@param dir "left"|"right"
|
||||||
---@return integer
|
---@return integer
|
||||||
@@ -628,8 +600,6 @@ local function vsplit_at(target_win, dir)
|
|||||||
return win
|
return win
|
||||||
end
|
end
|
||||||
|
|
||||||
---Make sure `right_win` exists, repurposing the invocation window or
|
|
||||||
---splitting the sidebar. Returns the right window.
|
|
||||||
---@param s ow.Git.SidebarState
|
---@param s ow.Git.SidebarState
|
||||||
---@param sidebar_win integer
|
---@param sidebar_win integer
|
||||||
---@param right_win integer?
|
---@param right_win integer?
|
||||||
@@ -642,7 +612,6 @@ local function ensure_right_win(s, sidebar_win, right_win)
|
|||||||
if target then
|
if target then
|
||||||
right_win = target
|
right_win = target
|
||||||
else
|
else
|
||||||
-- Sidebar-only case: split steals from sidebar, restore width.
|
|
||||||
right_win = vsplit_at(sidebar_win, "right")
|
right_win = vsplit_at(sidebar_win, "right")
|
||||||
vim.api.nvim_win_set_width(sidebar_win, SIDEBAR_WIDTH)
|
vim.api.nvim_win_set_width(sidebar_win, SIDEBAR_WIDTH)
|
||||||
end
|
end
|
||||||
@@ -804,9 +773,6 @@ local function action_discard()
|
|||||||
|
|
||||||
local prompt, action
|
local prompt, action
|
||||||
if entry.section == "Untracked" then
|
if entry.section == "Untracked" then
|
||||||
-- Porcelain v1 collapses untracked directories into a single
|
|
||||||
-- entry with a trailing slash, so plain `os.remove` (which only
|
|
||||||
-- deletes files / empty dirs) won't do.
|
|
||||||
local is_dir = entry.path:sub(-1) == "/"
|
local is_dir = entry.path:sub(-1) == "/"
|
||||||
prompt = string.format(
|
prompt = string.format(
|
||||||
"Delete untracked %s %s?",
|
"Delete untracked %s %s?",
|
||||||
|
|||||||
+7
-72
@@ -1,54 +1,9 @@
|
|||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
local URI_PREFIX = "git://"
|
|
||||||
|
|
||||||
---@param revspec string
|
|
||||||
---@return string
|
|
||||||
function M.uri(revspec)
|
|
||||||
return URI_PREFIX .. revspec
|
|
||||||
end
|
|
||||||
|
|
||||||
---Extract the revspec from a `git://<revspec>` buffer name. Returns
|
|
||||||
---nil if the name doesn't carry the scheme.
|
|
||||||
---@param name string
|
|
||||||
---@return string?
|
|
||||||
function M.parse_uri(name)
|
|
||||||
return name:match("^" .. URI_PREFIX .. "(.+)$")
|
|
||||||
end
|
|
||||||
|
|
||||||
---@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) --[[@as (0|1|2|3)?]],
|
|
||||||
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.ScratchOpts
|
---@class ow.Git.ScratchOpts
|
||||||
---@field name string?
|
---@field name string?
|
||||||
---@field bufhidden ("hide"|"wipe")? defaults to "wipe"
|
---@field bufhidden ("hide"|"wipe")?
|
||||||
|
|
||||||
---Configure a fresh buffer as a read-only scratch and optionally name it.
|
|
||||||
---@param buf integer
|
---@param buf integer
|
||||||
---@param opts ow.Git.ScratchOpts
|
---@param opts ow.Git.ScratchOpts
|
||||||
local function setup_scratch(buf, opts)
|
local function setup_scratch(buf, opts)
|
||||||
@@ -62,8 +17,6 @@ local function setup_scratch(buf, opts)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
---Set a buffer's name and re-run filetype detection from it. Wrapped
|
|
||||||
---in `pcall` because a buffer with that name may already exist (E95).
|
|
||||||
---@param buf integer
|
---@param buf integer
|
||||||
---@param name string
|
---@param name string
|
||||||
function M.set_buf_name(buf, name)
|
function M.set_buf_name(buf, name)
|
||||||
@@ -74,10 +27,6 @@ function M.set_buf_name(buf, name)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
---Place a buffer in the current window or a new split per `split`.
|
|
||||||
---`false` replaces the current buffer, dropping a `'` mark first so
|
|
||||||
---`''` jumps back. A direction string opens a split on that side. Nil
|
|
||||||
---falls back to `'splitbelow'` for the direction.
|
|
||||||
---@param buf integer
|
---@param buf integer
|
||||||
---@param split (false|"above"|"below"|"left"|"right")?
|
---@param split (false|"above"|"below"|"left"|"right")?
|
||||||
---@return integer win
|
---@return integer win
|
||||||
@@ -95,11 +44,8 @@ function M.place_buf(buf, split)
|
|||||||
end
|
end
|
||||||
|
|
||||||
---@class ow.Git.NewScratchOpts : ow.Git.ScratchOpts
|
---@class ow.Git.NewScratchOpts : ow.Git.ScratchOpts
|
||||||
---@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).
|
---@field split (false|"above"|"below"|"left"|"right")?
|
||||||
|
|
||||||
---Create a fresh read-only 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?
|
---@param opts ow.Git.NewScratchOpts?
|
||||||
---@return integer buf
|
---@return integer buf
|
||||||
---@return integer win
|
---@return integer win
|
||||||
@@ -134,9 +80,6 @@ function M.debug(fmt, ...)
|
|||||||
vim.notify(fmt:format(...), vim.log.levels.DEBUG)
|
vim.notify(fmt:format(...), vim.log.levels.DEBUG)
|
||||||
end
|
end
|
||||||
|
|
||||||
---Split a string on newlines, dropping the trailing empty element that an
|
|
||||||
---input ending in `\n` produces. Convenient for slicing subprocess stdout
|
|
||||||
---into a list of lines without a phantom blank at the end.
|
|
||||||
---@param content string
|
---@param content string
|
||||||
---@return string[]
|
---@return string[]
|
||||||
function M.split_lines(content)
|
function M.split_lines(content)
|
||||||
@@ -164,9 +107,9 @@ function M.debounce(fn, delay)
|
|||||||
local fired_gen = 0
|
local fired_gen = 0
|
||||||
|
|
||||||
local cb_main = vim.schedule_wrap(function()
|
local cb_main = vim.schedule_wrap(function()
|
||||||
-- Identity check: the libuv fire may have been superseded by a
|
-- Identity check: the libuv fire may have been superseded by
|
||||||
-- re-arm or a cancel between the timer firing and this scheduled
|
-- a re-arm or a cancel between the timer firing and this
|
||||||
-- callback running.
|
-- scheduled callback running.
|
||||||
if fired_gen ~= gen or args == nil then
|
if fired_gen ~= gen or args == nil then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -217,17 +160,9 @@ end
|
|||||||
---@class ow.Git.ExecOpts
|
---@class ow.Git.ExecOpts
|
||||||
---@field cwd string?
|
---@field cwd string?
|
||||||
---@field stdin string?
|
---@field stdin string?
|
||||||
---@field silent boolean? suppress the auto-log on non-zero exit
|
---@field silent boolean?
|
||||||
---@field on_done fun(stdout: string?)? if set, run async and deliver stdout (or nil on failure) here on the main loop instead of returning sync
|
---@field on_done fun(stdout: string?)?
|
||||||
|
|
||||||
---Run a system command. Default is sync: returns stdout on success or
|
|
||||||
---nil on failure (logging stderr unless `opts.silent`). When
|
|
||||||
---`opts.on_done` is set, runs async via `vim.schedule_wrap` and
|
|
||||||
---delivers the same stdout-or-nil value to that callback instead.
|
|
||||||
---
|
|
||||||
---Async mode returns nil immediately. Callers that need access to the
|
|
||||||
---raw stderr / exit code in the failure path should opt out of this
|
|
||||||
---helper and use `vim.system` directly.
|
|
||||||
---@param cmd string[]
|
---@param cmd string[]
|
||||||
---@param opts ow.Git.ExecOpts?
|
---@param opts ow.Git.ExecOpts?
|
||||||
---@return string?
|
---@return string?
|
||||||
|
|||||||
Reference in New Issue
Block a user