refactor(git): introduce Revision class, normalize naming, slim docs
This commit is contained in:
+13
-47
@@ -1,3 +1,4 @@
|
||||
local Revision = require("git.revision")
|
||||
local diff = require("git.diff")
|
||||
local object = require("git.object")
|
||||
local repo = require("git.repo")
|
||||
@@ -19,8 +20,8 @@ local SIDEBAR_WIDTH = 50
|
||||
---@field section string
|
||||
---@field path string
|
||||
---@field orig string?
|
||||
---@field x string porcelain v1 column 1 (always set, may be a literal space)
|
||||
---@field y string porcelain v1 column 2 (always set, may be a literal space)
|
||||
---@field x string
|
||||
---@field y string
|
||||
|
||||
---@class ow.Git.CommitEntry
|
||||
---@field section string
|
||||
@@ -34,7 +35,7 @@ local SIDEBAR_WIDTH = 50
|
||||
---@field worktree string
|
||||
---@field lines table<integer, ow.Git.SidebarEntry>
|
||||
---@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_right_win integer?
|
||||
---@field user_aucmd integer?
|
||||
@@ -47,7 +48,6 @@ local state = {}
|
||||
local group = vim.api.nvim_create_augroup("ow.git.sidebar", { clear = false })
|
||||
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? bufnr
|
||||
local function find_sidebar()
|
||||
@@ -59,8 +59,6 @@ local function find_sidebar()
|
||||
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
|
||||
---@return integer?
|
||||
local function sidebar_win_for(s)
|
||||
@@ -90,7 +88,7 @@ end
|
||||
---@param entry ow.Git.SidebarEntry
|
||||
---@return string? line
|
||||
---@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)
|
||||
if entry.sha then
|
||||
return string.format(" %s %s", entry.sha, entry.subject or ""),
|
||||
@@ -116,7 +114,7 @@ end
|
||||
---@field ahead integer
|
||||
---@field behind integer
|
||||
|
||||
---@param line string '## branch.line' from porcelain v1
|
||||
---@param line string
|
||||
---@return ow.Git.BranchInfo
|
||||
local function parse_branch_line(line)
|
||||
local info = { ahead = 0, behind = 0 }
|
||||
@@ -142,9 +140,6 @@ local function parse_branch_line(line)
|
||||
return info
|
||||
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
|
||||
---@return ow.Git.BranchInfo, table<string, ow.Git.SidebarEntry[]>
|
||||
local function parse_porcelain(stdout)
|
||||
@@ -165,9 +160,6 @@ local function parse_porcelain(stdout)
|
||||
local y = line:sub(2, 2)
|
||||
local rest = line:sub(4)
|
||||
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
|
||||
local arrow = rest:find(" -> ", 1, true)
|
||||
if arrow then
|
||||
@@ -213,9 +205,6 @@ local function parse_porcelain(stdout)
|
||||
return branch, groups
|
||||
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 branch ow.Git.BranchInfo
|
||||
---@param groups table<string, ow.Git.SidebarEntry[]>
|
||||
@@ -233,8 +222,6 @@ local function enrich_with_log(worktree, branch, groups)
|
||||
{ section = "Unpulled", range = "HEAD..@{upstream}" }
|
||||
)
|
||||
end
|
||||
-- Submit both subprocesses before waiting so they run concurrently
|
||||
-- rather than sequentially. Total time = max, not sum.
|
||||
local pending = {}
|
||||
for _, f in ipairs(fetches) do
|
||||
table.insert(pending, {
|
||||
@@ -271,10 +258,6 @@ local function enrich_with_log(worktree, branch, groups)
|
||||
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 prefetched_stdout string?
|
||||
---@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
|
||||
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 groups table<string, ow.Git.SidebarEntry[]>
|
||||
---@return string
|
||||
@@ -408,7 +388,7 @@ local function fingerprint(branch, groups)
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@param prefetched_stdout string? porcelain output from a piggybacked GitRefresh
|
||||
---@param prefetched_stdout string?
|
||||
local function refresh(bufnr, prefetched_stdout)
|
||||
local s = state[bufnr]
|
||||
if not s then
|
||||
@@ -430,9 +410,6 @@ local function refresh(bufnr, prefetched_stdout)
|
||||
if not vim.api.nvim_buf_is_valid(bufnr) then
|
||||
return
|
||||
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
|
||||
local fp = fingerprint(branch, groups)
|
||||
if fp == s.last_render_key then
|
||||
@@ -482,9 +459,10 @@ end
|
||||
---@param path string
|
||||
---@return ow.Git.DiffSide
|
||||
local function head_pane(worktree, path)
|
||||
local rev = Revision.new({ base = "HEAD", path = path })
|
||||
return {
|
||||
buf = object.buf_for(worktree, "HEAD:" .. path),
|
||||
name = util.uri("HEAD:" .. path),
|
||||
buf = object.buf_for(worktree, rev),
|
||||
name = rev:uri(),
|
||||
}
|
||||
end
|
||||
|
||||
@@ -501,9 +479,10 @@ end
|
||||
---@param entry ow.Git.FileEntry
|
||||
---@return ow.Git.DiffSide
|
||||
local function index_pane(s, entry)
|
||||
local rev = Revision.new({ stage = 0, path = entry.path })
|
||||
return {
|
||||
buf = object.buf_for(s.worktree, ":0:" .. entry.path),
|
||||
name = util.uri(":0:" .. entry.path),
|
||||
buf = object.buf_for(s.worktree, rev),
|
||||
name = rev:uri(),
|
||||
}
|
||||
end
|
||||
|
||||
@@ -515,7 +494,6 @@ local function older_pane(s, entry)
|
||||
if entry.x == "A" then
|
||||
return nil
|
||||
end
|
||||
-- HEAD holds the pre-rename path
|
||||
return head_pane(s.worktree, entry.orig or entry.path)
|
||||
end
|
||||
if entry.section == "Unstaged" then
|
||||
@@ -555,10 +533,6 @@ local function reset_diff_win(win)
|
||||
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
|
||||
---@return integer?
|
||||
local function invocation_win_for(s)
|
||||
@@ -613,8 +587,6 @@ local function entry_key(entry)
|
||||
return entry.section .. "|" .. entry.path .. "|" .. (entry.orig or "")
|
||||
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 dir "left"|"right"
|
||||
---@return integer
|
||||
@@ -628,8 +600,6 @@ local function vsplit_at(target_win, dir)
|
||||
return win
|
||||
end
|
||||
|
||||
---Make sure `right_win` exists, repurposing the invocation window or
|
||||
---splitting the sidebar. Returns the right window.
|
||||
---@param s ow.Git.SidebarState
|
||||
---@param sidebar_win integer
|
||||
---@param right_win integer?
|
||||
@@ -642,7 +612,6 @@ local function ensure_right_win(s, sidebar_win, right_win)
|
||||
if target then
|
||||
right_win = target
|
||||
else
|
||||
-- Sidebar-only case: split steals from sidebar, restore width.
|
||||
right_win = vsplit_at(sidebar_win, "right")
|
||||
vim.api.nvim_win_set_width(sidebar_win, SIDEBAR_WIDTH)
|
||||
end
|
||||
@@ -804,9 +773,6 @@ local function action_discard()
|
||||
|
||||
local prompt, action
|
||||
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) == "/"
|
||||
prompt = string.format(
|
||||
"Delete untracked %s %s?",
|
||||
|
||||
Reference in New Issue
Block a user