refactor(git): introduce Revision class, normalize naming, slim docs
This commit is contained in:
+41
-79
@@ -1,15 +1,9 @@
|
||||
local Revision = require("git.revision")
|
||||
local repo = require("git.repo")
|
||||
local util = require("git.util")
|
||||
|
||||
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 enabled boolean
|
||||
function M.set_diff(win, enabled)
|
||||
@@ -18,16 +12,10 @@ function M.set_diff(win, enabled)
|
||||
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 right integer
|
||||
---@param vertical boolean
|
||||
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)
|
||||
vim.cmd.normal({ "m'", bang = true })
|
||||
vim.api.nvim_set_current_buf(right)
|
||||
@@ -38,13 +26,6 @@ function M.open(left, right, vertical)
|
||||
})
|
||||
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 right_win integer
|
||||
---@param pair ow.Git.DiffPair
|
||||
@@ -65,8 +46,6 @@ function M.update_pair(left_win, right_win, pair)
|
||||
vim.cmd.syncbind()
|
||||
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_b integer
|
||||
---@param a_left boolean
|
||||
@@ -79,97 +58,82 @@ local function place_pair(buf_a, buf_b, a_left, vertical)
|
||||
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 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())
|
||||
---@param buf integer
|
||||
---@param rev ow.Git.Revision
|
||||
local function uri_split(opts, buf, rev)
|
||||
local worktree = vim.b[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
|
||||
if not rev.path then
|
||||
util.warning("git URI has no path, cannot diff against worktree")
|
||||
return
|
||||
end
|
||||
-- Lazy-required to break the load-time cycle with `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(
|
||||
{ "git", "cat-file", "-p", opts.revspec },
|
||||
{ "git", "cat-file", "-p", opts.rev },
|
||||
{ cwd = worktree, silent = true }
|
||||
)
|
||||
if not content then
|
||||
util.warning("invalid revspec: %s", opts.revspec)
|
||||
util.warning("invalid rev: %s", opts.rev)
|
||||
return
|
||||
end
|
||||
place_pair(
|
||||
cur_buf,
|
||||
object.buf_for(worktree, opts.revspec, content),
|
||||
buf,
|
||||
object.buf_for(worktree, Revision.parse(opts.rev), content),
|
||||
false,
|
||||
opts.vertical
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
if opts.revspec == "" then
|
||||
local worktree_path = vim.fs.joinpath(worktree, cur.path)
|
||||
if not opts.rev then
|
||||
local worktree_path = vim.fs.joinpath(worktree, rev.path)
|
||||
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
|
||||
end
|
||||
local worktree_buf = vim.fn.bufadd(worktree_path)
|
||||
vim.fn.bufload(worktree_buf)
|
||||
place_pair(cur_buf, worktree_buf, true, opts.vertical)
|
||||
place_pair(buf, worktree_buf, true, opts.vertical)
|
||||
return
|
||||
end
|
||||
|
||||
if cur.stage == 1 then
|
||||
util.warning("gD on merge base is ambiguous, use :Gdiffsplit <ref>")
|
||||
if rev.stage == 1 then
|
||||
util.warning("gD on merge base is ambiguous, use :Gdiffsplit <rev>")
|
||||
return
|
||||
end
|
||||
|
||||
local mapping = {
|
||||
[2] = { ":3:" .. cur.path, true },
|
||||
[3] = { ":2:" .. cur.path, false },
|
||||
[0] = { "HEAD:" .. cur.path, false },
|
||||
[2] = { Revision.new({ stage = 3, path = rev.path }), true },
|
||||
[3] = { Revision.new({ stage = 2, path = rev.path }), false },
|
||||
[0] = { Revision.new({ base = "HEAD", path = rev.path }), false },
|
||||
}
|
||||
local m = mapping[cur.stage] or { ":0:" .. cur.path, true }
|
||||
local other_revspec, cur_left = m[1], m[2]
|
||||
local m = mapping[rev.stage]
|
||||
or { Revision.new({ stage = 0, path = rev.path }), true }
|
||||
local other_rev, left = m[1], m[2]
|
||||
local content = util.exec(
|
||||
{ "git", "cat-file", "-p", other_revspec },
|
||||
{ "git", "cat-file", "-p", other_rev:format() },
|
||||
{ cwd = worktree, silent = true }
|
||||
)
|
||||
if not content then
|
||||
util.warning("invalid revspec: %s", other_revspec)
|
||||
util.warning("invalid rev: %s", other_rev:format())
|
||||
return
|
||||
end
|
||||
place_pair(
|
||||
cur_buf,
|
||||
object.buf_for(worktree, other_revspec, content),
|
||||
cur_left,
|
||||
buf,
|
||||
object.buf_for(worktree, other_rev, content),
|
||||
left,
|
||||
opts.vertical
|
||||
)
|
||||
end
|
||||
|
||||
---@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
|
||||
|
||||
---@param opts ow.Git.SplitOpts
|
||||
@@ -177,9 +141,9 @@ function M.split(opts)
|
||||
local cur_buf = vim.api.nvim_get_current_buf()
|
||||
local cur_path = vim.api.nvim_buf_get_name(cur_buf)
|
||||
|
||||
local cur_revspec = util.parse_uri(cur_path)
|
||||
if cur_revspec then
|
||||
return uri_split(opts, cur_buf, cur_revspec)
|
||||
local cur_rev = Revision.from_uri(cur_path)
|
||||
if cur_rev then
|
||||
return uri_split(opts, cur_buf, cur_rev)
|
||||
end
|
||||
|
||||
if cur_path == "" then
|
||||
@@ -201,25 +165,23 @@ function M.split(opts)
|
||||
return
|
||||
end
|
||||
|
||||
local revspec
|
||||
if opts.revspec == "" then
|
||||
revspec = ":0:" .. rel
|
||||
elseif opts.revspec:find(":", 1, true) then
|
||||
revspec = opts.revspec
|
||||
local rev
|
||||
if not opts.rev then
|
||||
rev = Revision.new({ stage = 0, path = rel })
|
||||
elseif opts.rev:find(":", 1, true) then
|
||||
rev = Revision.parse(opts.rev)
|
||||
else
|
||||
revspec = opts.revspec .. ":" .. rel
|
||||
rev = Revision.new({ base = opts.rev, path = rel })
|
||||
end
|
||||
local content = util.exec(
|
||||
{ "git", "cat-file", "-p", revspec },
|
||||
{ "git", "cat-file", "-p", rev:format() },
|
||||
{ cwd = worktree, silent = true }
|
||||
)
|
||||
if not content then
|
||||
util.warning("invalid revspec: %s", revspec)
|
||||
util.warning("invalid rev: %s", rev:format())
|
||||
return
|
||||
end
|
||||
local object = require("git.object")
|
||||
local buf = object.buf_for(worktree, revspec, content)
|
||||
-- Revspec snapshot is older than the worktree, so it goes left.
|
||||
local buf = require("git.object").buf_for(worktree, rev, content)
|
||||
place_pair(buf, cur_buf, true, opts.vertical)
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user