refactor(git): rework module around clearer Status and Repo split
This commit is contained in:
+8
-8
@@ -5,11 +5,11 @@ local util = require("git.util")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class ow.Git.SplitHandler
|
||||
---@class ow.Git.Cmd.SplitHandler
|
||||
---@field ft string
|
||||
---@field needs_rev boolean?
|
||||
|
||||
---@type table<string, ow.Git.SplitHandler>
|
||||
---@type table<string, ow.Git.Cmd.SplitHandler>
|
||||
local SPLIT_HANDLERS = {
|
||||
log = { ft = "git" },
|
||||
diff = { ft = "diff" },
|
||||
@@ -74,7 +74,7 @@ end
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
---@param args string[]
|
||||
---@param conf ow.Git.SplitHandler
|
||||
---@param conf ow.Git.Cmd.SplitHandler
|
||||
local function run_in_split(r, args, conf)
|
||||
local cmd = { "git" }
|
||||
vim.list_extend(cmd, args)
|
||||
@@ -86,7 +86,7 @@ local function run_in_split(r, args, conf)
|
||||
end
|
||||
local name = "[git " .. table.concat(args, " ") .. "]"
|
||||
local buf = place_split(name)
|
||||
repo.attach(buf, r)
|
||||
repo.bind(buf, r)
|
||||
object.attach_dispatch(buf)
|
||||
local state = r:state(buf) --[[@as -nil]]
|
||||
state.sha = nil
|
||||
@@ -178,7 +178,7 @@ end
|
||||
|
||||
---@param args string[]
|
||||
function M.run(args)
|
||||
local r = repo.find()
|
||||
local r = repo.resolve()
|
||||
if not r then
|
||||
util.warning("not in a git repository")
|
||||
return
|
||||
@@ -193,7 +193,7 @@ function M.run(args)
|
||||
if sub == "show" then
|
||||
local arg = first_positional(args, 2)
|
||||
if arg and arg:find(":", 1, true) then
|
||||
object.open_object(r, arg)
|
||||
object.open(r, arg)
|
||||
return
|
||||
end
|
||||
run_in_split(r, args, { ft = "git", needs_rev = true })
|
||||
@@ -204,7 +204,7 @@ function M.run(args)
|
||||
if vim.list_contains(args, "-p") then
|
||||
local rev = first_positional(args, 2)
|
||||
if rev then
|
||||
object.open_object(r, rev)
|
||||
object.open(r, rev)
|
||||
return
|
||||
end
|
||||
end
|
||||
@@ -223,7 +223,7 @@ end
|
||||
---@param arg_lead string
|
||||
---@return string[]
|
||||
function M.complete_rev(arg_lead)
|
||||
local r = repo.find()
|
||||
local r = repo.resolve()
|
||||
if not r then
|
||||
return {}
|
||||
end
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ local M = {}
|
||||
---@param opts { amend: boolean? }?
|
||||
function M.commit(opts)
|
||||
local amend = opts and opts.amend or false
|
||||
local r = repo.find()
|
||||
local r = repo.resolve()
|
||||
if not r then
|
||||
util.warning("not in a git repository")
|
||||
return
|
||||
|
||||
+13
-5
@@ -4,6 +4,14 @@ local util = require("git.util")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class ow.Git.Diff.Side
|
||||
---@field buf integer
|
||||
---@field name string?
|
||||
|
||||
---@class ow.Git.Diff.Pair
|
||||
---@field left ow.Git.Diff.Side
|
||||
---@field right ow.Git.Diff.Side
|
||||
|
||||
---@param win integer
|
||||
---@param enabled boolean
|
||||
function M.set_diff(win, enabled)
|
||||
@@ -28,7 +36,7 @@ end
|
||||
|
||||
---@param left_win integer
|
||||
---@param right_win integer
|
||||
---@param pair ow.Git.DiffPair
|
||||
---@param pair ow.Git.Diff.Pair
|
||||
function M.update_pair(left_win, right_win, pair)
|
||||
M.set_diff(left_win, false)
|
||||
M.set_diff(right_win, false)
|
||||
@@ -58,11 +66,11 @@ local function place_pair(buf_a, buf_b, a_left, vertical)
|
||||
end
|
||||
end
|
||||
|
||||
---@param opts ow.Git.SplitOpts
|
||||
---@param opts ow.Git.Diff.SplitOpts
|
||||
---@param buf integer
|
||||
---@param rev ow.Git.Revision
|
||||
local function uri_split(opts, buf, rev)
|
||||
local r = repo.find(buf)
|
||||
local r = repo.resolve(buf)
|
||||
if not r then
|
||||
util.warning("git URI buffer has no worktree")
|
||||
return
|
||||
@@ -127,11 +135,11 @@ local function uri_split(opts, buf, rev)
|
||||
place_pair(buf, object.buf_for(r, other_rev, content), left, opts.vertical)
|
||||
end
|
||||
|
||||
---@class ow.Git.SplitOpts
|
||||
---@class ow.Git.Diff.SplitOpts
|
||||
---@field rev string?
|
||||
---@field vertical boolean
|
||||
|
||||
---@param opts ow.Git.SplitOpts
|
||||
---@param opts ow.Git.Diff.SplitOpts
|
||||
function M.split(opts)
|
||||
local cur_buf = vim.api.nvim_get_current_buf()
|
||||
local cur_path = vim.api.nvim_buf_get_name(cur_buf)
|
||||
|
||||
+35
-12
@@ -20,25 +20,28 @@ function M.init()
|
||||
|
||||
local group = vim.api.nvim_create_augroup("ow.git", { clear = true })
|
||||
|
||||
vim.api.nvim_create_autocmd(
|
||||
{ "BufReadPost", "BufNewFile", "BufWritePost", "FileChangedShellPost" },
|
||||
{
|
||||
group = group,
|
||||
callback = function(args)
|
||||
require("git.repo").refresh(args.buf)
|
||||
end,
|
||||
}
|
||||
)
|
||||
vim.api.nvim_create_autocmd({ "BufReadPost", "BufNewFile" }, {
|
||||
group = group,
|
||||
callback = function(args)
|
||||
require("git.repo").track(args.buf)
|
||||
end,
|
||||
})
|
||||
vim.api.nvim_create_autocmd({ "BufWritePost", "FileChangedShellPost" }, {
|
||||
group = group,
|
||||
callback = function(args)
|
||||
require("git.repo").refresh(args.buf)
|
||||
end,
|
||||
})
|
||||
vim.api.nvim_create_autocmd({ "BufDelete", "BufWipeout" }, {
|
||||
group = group,
|
||||
callback = function(args)
|
||||
require("git.repo").unregister(args.buf)
|
||||
require("git.repo").unbind(args.buf)
|
||||
end,
|
||||
})
|
||||
vim.api.nvim_create_autocmd("FocusGained", {
|
||||
group = group,
|
||||
callback = function(args)
|
||||
require("git.repo").refresh(args.buf)
|
||||
callback = function()
|
||||
require("git.repo").refresh_all()
|
||||
end,
|
||||
})
|
||||
vim.api.nvim_create_autocmd("VimLeavePre", {
|
||||
@@ -48,6 +51,22 @@ function M.init()
|
||||
end,
|
||||
})
|
||||
|
||||
vim.api.nvim_create_autocmd({ "VimEnter", "DirChanged", "TabEnter" }, {
|
||||
group = group,
|
||||
callback = function()
|
||||
require("git.repo").update_cwd_repo()
|
||||
end,
|
||||
})
|
||||
vim.api.nvim_create_autocmd("TabClosed", {
|
||||
group = group,
|
||||
callback = function(args)
|
||||
local tab = tonumber(args.file) --[[@as integer?]]
|
||||
if tab then
|
||||
require("git.repo").release_tab(tab)
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
vim.api.nvim_create_autocmd("BufReadCmd", {
|
||||
pattern = "git://*",
|
||||
group = group,
|
||||
@@ -118,6 +137,10 @@ function M.init()
|
||||
return require("git.cmd").complete(...)
|
||||
end,
|
||||
})
|
||||
|
||||
vim.api.nvim_create_user_command("Grefresh", function()
|
||||
require("git.repo").refresh_all()
|
||||
end, { desc = "Refresh git status for all repos" })
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
+10
-8
@@ -12,7 +12,7 @@ local cr = vim.api.nvim_replace_termcodes("<CR>", true, false, true)
|
||||
---@param buf integer
|
||||
local function attach_dispatch(buf)
|
||||
vim.keymap.set("n", "<CR>", function()
|
||||
local r = repo.find(buf)
|
||||
local r = repo.resolve(buf)
|
||||
-- Anchor past the leading graph chars (matches the leading sha
|
||||
-- column, not any hex word that happens to appear later in the
|
||||
-- subject).
|
||||
@@ -22,7 +22,7 @@ local function attach_dispatch(buf)
|
||||
:match("^[*|/\\_ ]*(%x%x%x%x%x%x%x+)")
|
||||
if sha then
|
||||
---@cast r -nil
|
||||
require("git.object").open_object(r, sha, { split = false })
|
||||
require("git.object").open(r, sha, { split = false })
|
||||
else
|
||||
vim.api.nvim_feedkeys(cr, "n", false)
|
||||
end
|
||||
@@ -50,7 +50,7 @@ end
|
||||
|
||||
---@param buf integer
|
||||
local function populate(buf)
|
||||
local r = repo.find(buf)
|
||||
local r = repo.resolve(buf)
|
||||
local state = r and r:state(buf)
|
||||
if not r or not state then
|
||||
return
|
||||
@@ -100,7 +100,7 @@ function M.read_uri(buf)
|
||||
if not r then
|
||||
return
|
||||
end
|
||||
repo.attach(buf, r)
|
||||
repo.bind(buf, r)
|
||||
|
||||
vim.bo[buf].swapfile = false
|
||||
vim.bo[buf].bufhidden = "hide"
|
||||
@@ -113,7 +113,7 @@ function M.read_uri(buf)
|
||||
populate(buf)
|
||||
end
|
||||
|
||||
---@class ow.Git.LogOpts
|
||||
---@class ow.Git.Log.OpenOpts
|
||||
---@field max_count integer?
|
||||
|
||||
---@type table<string, fun(s: string): any>
|
||||
@@ -121,17 +121,17 @@ M.opt_parsers = {
|
||||
max_count = tonumber,
|
||||
}
|
||||
|
||||
---@param opts ow.Git.LogOpts?
|
||||
---@param opts ow.Git.Log.OpenOpts?
|
||||
function M.open(opts)
|
||||
opts = opts or {}
|
||||
local r = repo.find()
|
||||
local r = repo.resolve()
|
||||
if not r then
|
||||
util.warning("not in a git repository")
|
||||
return
|
||||
end
|
||||
|
||||
local buf = vim.fn.bufadd(M.URI_PREFIX .. r.worktree)
|
||||
repo.attach(buf, r)
|
||||
repo.bind(buf, r)
|
||||
local state = r:state(buf) --[[@as -nil]]
|
||||
state.log_max_count = opts.max_count
|
||||
local was_loaded = vim.api.nvim_buf_is_loaded(buf)
|
||||
@@ -183,4 +183,6 @@ function M.complete_glog(arg_lead)
|
||||
return matches
|
||||
end
|
||||
|
||||
repo.on_uri_refresh(M.URI_PREFIX, M.read_uri)
|
||||
|
||||
return M
|
||||
|
||||
+11
-9
@@ -145,7 +145,7 @@ end
|
||||
---@return integer
|
||||
function M.buf_for(r, rev, content)
|
||||
local buf = vim.fn.bufadd(M.format_uri(rev))
|
||||
repo.attach(buf, r)
|
||||
repo.bind(buf, r)
|
||||
if content then
|
||||
local state = r:state(buf) --[[@as -nil]]
|
||||
state.pending_content = content
|
||||
@@ -163,12 +163,12 @@ function M.read_uri(buf)
|
||||
end
|
||||
local rev_str = rev:format()
|
||||
|
||||
local r = repo.find(buf)
|
||||
local r = repo.resolve(buf)
|
||||
if not r then
|
||||
util.error("git BufReadCmd %s: cannot resolve worktree", name)
|
||||
return
|
||||
end
|
||||
repo.attach(buf, r)
|
||||
repo.bind(buf, r)
|
||||
local state = r:state(buf) --[[@as -nil]]
|
||||
|
||||
vim.bo[buf].swapfile = false
|
||||
@@ -275,7 +275,7 @@ local function load_blob(r, blob, path, sha)
|
||||
vim.api.nvim_set_current_buf(buf)
|
||||
end
|
||||
|
||||
---@param s ow.Git.BufState
|
||||
---@param s ow.Git.Repo.BufState
|
||||
---@param section ow.Git.DiffSection
|
||||
local function open_section(s, section)
|
||||
if not section.blob_a or not section.blob_b then
|
||||
@@ -300,13 +300,13 @@ local function open_section(s, section)
|
||||
vim.api.nvim_set_current_buf(buf)
|
||||
end
|
||||
|
||||
---@class ow.Git.OpenObjectOpts
|
||||
---@class ow.Git.Object.OpenOpts
|
||||
---@field split (false|"above"|"below"|"left"|"right")?
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
---@param rev string
|
||||
---@param opts ow.Git.OpenObjectOpts?
|
||||
function M.open_object(r, rev, opts)
|
||||
---@param opts ow.Git.Object.OpenOpts?
|
||||
function M.open(r, rev, opts)
|
||||
local parsed = Revision.parse(rev)
|
||||
if parsed.base then
|
||||
local sha = r:rev_parse(parsed.base, true)
|
||||
@@ -341,7 +341,7 @@ function M.open_under_cursor()
|
||||
or line:match("^tree (%x+)$")
|
||||
or line:match("^object (%x+)$")
|
||||
if sha then
|
||||
M.open_object(r, sha, { split = false })
|
||||
M.open(r, sha, { split = false })
|
||||
return true
|
||||
end
|
||||
|
||||
@@ -351,7 +351,7 @@ function M.open_under_cursor()
|
||||
local nav_rev = entry_type == "blob"
|
||||
and Revision.new({ base = s.sha, path = entry_name }):format()
|
||||
or entry_sha
|
||||
M.open_object(r, nav_rev, { split = false })
|
||||
M.open(r, nav_rev, { split = false })
|
||||
return true
|
||||
end
|
||||
|
||||
@@ -384,4 +384,6 @@ function M.open_under_cursor()
|
||||
return false
|
||||
end
|
||||
|
||||
repo.on_uri_refresh(M.URI_PREFIX, M.read_uri)
|
||||
|
||||
return M
|
||||
|
||||
+239
-296
@@ -1,7 +1,9 @@
|
||||
local status = require("git.status")
|
||||
local util = require("git.util")
|
||||
|
||||
---@param buf integer?
|
||||
local M = {}
|
||||
|
||||
---@param buf? integer
|
||||
---@return integer
|
||||
local function expand_buf(buf)
|
||||
if not buf or buf == 0 then
|
||||
@@ -10,7 +12,7 @@ local function expand_buf(buf)
|
||||
return buf
|
||||
end
|
||||
|
||||
---@class ow.Git.BufState
|
||||
---@class ow.Git.Repo.BufState
|
||||
---@field repo ow.Git.Repo
|
||||
---@field sha string?
|
||||
---@field parent_sha string?
|
||||
@@ -19,110 +21,58 @@ end
|
||||
---@field log_max_count integer?
|
||||
---@field pending_content string?
|
||||
|
||||
---@class ow.Git.StatusEntry
|
||||
---@field section "Untracked"|"Unstaged"|"Staged"|"Unmerged"
|
||||
---@field x string
|
||||
---@field y string
|
||||
---@field path string
|
||||
---@field orig string?
|
||||
---@alias ow.Git.Repo.Event "refresh"
|
||||
|
||||
---@class ow.Git.BranchInfo
|
||||
---@field head string?
|
||||
---@field upstream string?
|
||||
---@field ahead integer
|
||||
---@field behind integer
|
||||
|
||||
---@class ow.Git.PorcelainGroups
|
||||
---@field Untracked ow.Git.StatusEntry[]
|
||||
---@field Unstaged ow.Git.StatusEntry[]
|
||||
---@field Staged ow.Git.StatusEntry[]
|
||||
---@field Unmerged ow.Git.StatusEntry[]
|
||||
local global = util.Emitter.new()
|
||||
|
||||
---@class ow.Git.Repo
|
||||
---@field gitdir string
|
||||
---@field worktree string
|
||||
---@field buffers table<integer, ow.Git.BufState>
|
||||
---@field watcher? uv.uv_fs_event_t
|
||||
---@field refresh fun(self: ow.Git.Repo)
|
||||
---@field refresh_handle ow.Git.Util.DebounceHandle
|
||||
---@field private refresh_listeners (fun(r: ow.Git.Repo, porcelain_stdout: string?))[]
|
||||
---@field buffers table<integer, ow.Git.Repo.BufState>
|
||||
---@field tabs table<integer, true>
|
||||
---@field status ow.Git.Status
|
||||
---@field private _events ow.Git.Util.Emitter<ow.Git.Repo.Event>
|
||||
---@field private _watcher? uv.uv_fs_event_t
|
||||
---@field private _schedule_refresh fun(self: ow.Git.Repo)
|
||||
---@field private _refresh_handle ow.Git.Util.DebounceHandle
|
||||
local Repo = {}
|
||||
Repo.__index = Repo
|
||||
|
||||
---@param line string
|
||||
---@return string x, string y, string path, string? orig
|
||||
local function parse_porcelain_line(line)
|
||||
local x = line:sub(1, 1)
|
||||
local y = line:sub(2, 2)
|
||||
local rest = line:sub(4)
|
||||
local orig
|
||||
if x == "R" or x == "C" or y == "R" or y == "C" then
|
||||
local arrow = rest:find(" -> ", 1, true)
|
||||
if arrow then
|
||||
orig = rest:sub(1, arrow - 1)
|
||||
rest = rest:sub(arrow + 4)
|
||||
end
|
||||
end
|
||||
return x, y, rest, orig
|
||||
end
|
||||
local STATUS_CMD = {
|
||||
"git",
|
||||
"--no-optional-locks",
|
||||
"-c",
|
||||
"core.quotePath=false",
|
||||
"status",
|
||||
"--porcelain=v1",
|
||||
"--branch",
|
||||
"--ignored",
|
||||
}
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
local function do_refresh(r)
|
||||
---@private
|
||||
function Repo:_fetch_status()
|
||||
vim.system(
|
||||
{
|
||||
"git",
|
||||
"-c",
|
||||
"core.quotePath=false",
|
||||
"status",
|
||||
"--porcelain=v1",
|
||||
"--branch",
|
||||
},
|
||||
{ cwd = r.worktree, text = true },
|
||||
STATUS_CMD,
|
||||
{ cwd = self.worktree, text = true },
|
||||
vim.schedule_wrap(function(obj)
|
||||
local statuses = {}
|
||||
if obj.code == 0 then
|
||||
for line in (obj.stdout or ""):gmatch("[^\r\n]+") do
|
||||
if line:sub(1, 2) ~= "##" then
|
||||
local x, y, path = parse_porcelain_line(line)
|
||||
statuses[vim.fs.joinpath(r.worktree, path)] =
|
||||
status.format(x .. y)
|
||||
end
|
||||
end
|
||||
else
|
||||
if obj.code ~= 0 then
|
||||
util.warning(
|
||||
"git status failed: %s",
|
||||
vim.trim(obj.stderr or "")
|
||||
)
|
||||
return
|
||||
end
|
||||
local dirty = false
|
||||
for buf in pairs(r.buffers) do
|
||||
if not vim.api.nvim_buf_is_valid(buf) then
|
||||
r.buffers[buf] = nil
|
||||
else
|
||||
local name = vim.api.nvim_buf_get_name(buf)
|
||||
local object = require("git.object")
|
||||
local log = require("git.log")
|
||||
if name:sub(1, #object.URI_PREFIX) == object.URI_PREFIX then
|
||||
object.read_uri(buf)
|
||||
elseif name:sub(1, #log.URI_PREFIX) == log.URI_PREFIX then
|
||||
log.read_uri(buf)
|
||||
else
|
||||
local s = statuses[vim.fn.resolve(name)]
|
||||
if vim.b[buf].git_status ~= s then
|
||||
vim.b[buf].git_status = s
|
||||
dirty = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if dirty then
|
||||
vim.cmd.redrawstatus({ bang = true })
|
||||
end
|
||||
r:notify_refresh(obj.code == 0 and obj.stdout or nil)
|
||||
self.status = status.parse(obj.stdout or "")
|
||||
self._events:emit("refresh", self.status)
|
||||
global:emit("refresh", self, self.status)
|
||||
end)
|
||||
)
|
||||
end
|
||||
|
||||
function Repo:refresh()
|
||||
self:_schedule_refresh()
|
||||
end
|
||||
|
||||
---@param gitdir string
|
||||
---@param worktree string
|
||||
---@return ow.Git.Repo
|
||||
@@ -131,10 +81,14 @@ function Repo.new(gitdir, worktree)
|
||||
gitdir = gitdir,
|
||||
worktree = worktree,
|
||||
buffers = {},
|
||||
refresh_listeners = {},
|
||||
tabs = {},
|
||||
status = status.parse(""),
|
||||
_events = util.Emitter.new(),
|
||||
}, Repo)
|
||||
self.refresh, self.refresh_handle = util.debounce(do_refresh, 50)
|
||||
self._schedule_refresh, self._refresh_handle =
|
||||
util.debounce(Repo._fetch_status, 50)
|
||||
self:start_watcher()
|
||||
self:refresh()
|
||||
return self
|
||||
end
|
||||
|
||||
@@ -148,7 +102,7 @@ function Repo:start_watcher()
|
||||
)
|
||||
return
|
||||
end
|
||||
local ok, err = watcher:start(
|
||||
local ok, start_err = watcher:start(
|
||||
self.gitdir,
|
||||
{ recursive = true },
|
||||
function(err_, filename)
|
||||
@@ -163,67 +117,35 @@ function Repo:start_watcher()
|
||||
end
|
||||
)
|
||||
if not ok then
|
||||
util.warning("failed to watch %s: %s", self.gitdir, tostring(err))
|
||||
util.warning("failed to watch %s: %s", self.gitdir, tostring(start_err))
|
||||
watcher:close()
|
||||
return
|
||||
end
|
||||
self.watcher = watcher
|
||||
self._watcher = watcher
|
||||
end
|
||||
|
||||
function Repo:stop_watcher()
|
||||
if self.watcher then
|
||||
self.watcher:stop()
|
||||
self.watcher:close()
|
||||
self.watcher = nil
|
||||
function Repo:close()
|
||||
if self._watcher then
|
||||
self._watcher:stop()
|
||||
self._watcher:close()
|
||||
self._watcher = nil
|
||||
end
|
||||
self.refresh_handle.close()
|
||||
self._refresh_handle.close()
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
function Repo:add_buffer(buf)
|
||||
buf = expand_buf(buf)
|
||||
if not self.buffers[buf] then
|
||||
self.buffers[buf] = { repo = self }
|
||||
end
|
||||
---@param event ow.Git.Repo.Event
|
||||
---@param fn fun(...)
|
||||
---@return fun() unsubscribe
|
||||
function Repo:on(event, fn)
|
||||
return self._events:on(event, fn)
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
function Repo:remove_buffer(buf)
|
||||
self.buffers[expand_buf(buf)] = nil
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
---@return ow.Git.BufState?
|
||||
---@param buf? integer
|
||||
---@return ow.Git.Repo.BufState?
|
||||
function Repo:state(buf)
|
||||
return self.buffers[expand_buf(buf)]
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
function Repo:has_buffers()
|
||||
return next(self.buffers) ~= nil
|
||||
end
|
||||
|
||||
---@param fn fun(r: ow.Git.Repo, porcelain_stdout: string?)
|
||||
---@return fun() unsubscribe
|
||||
function Repo:on_refresh(fn)
|
||||
table.insert(self.refresh_listeners, fn)
|
||||
return function()
|
||||
for i, f in ipairs(self.refresh_listeners) do
|
||||
if f == fn then
|
||||
table.remove(self.refresh_listeners, i)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param porcelain_stdout string?
|
||||
function Repo:notify_refresh(porcelain_stdout)
|
||||
for _, fn in ipairs(self.refresh_listeners) do
|
||||
fn(self, porcelain_stdout)
|
||||
end
|
||||
end
|
||||
|
||||
---@return string?
|
||||
function Repo:head()
|
||||
local f = io.open(vim.fs.joinpath(self.gitdir, "HEAD"), "r")
|
||||
@@ -278,27 +200,117 @@ function Repo:rev_parse(rev, short)
|
||||
return trimmed ~= "" and trimmed or nil
|
||||
end
|
||||
|
||||
local M = {}
|
||||
---@type table<string, ow.Git.Repo> keyed by worktree
|
||||
local repos = {}
|
||||
|
||||
---@type table<string, ow.Git.Repo>
|
||||
local repo_by_gitdir = {}
|
||||
---@param event ow.Git.Repo.Event
|
||||
---@param fn fun(...)
|
||||
---@return fun() unsubscribe
|
||||
function M.on(event, fn)
|
||||
return global:on(event, fn)
|
||||
end
|
||||
|
||||
---@type table<integer, ow.Git.Repo>
|
||||
local repo_by_buf = {}
|
||||
---@param prefix string
|
||||
---@param fn fun(buf: integer)
|
||||
---@return fun() unsubscribe
|
||||
function M.on_uri_refresh(prefix, fn)
|
||||
return M.on("refresh", function(r)
|
||||
for buf in pairs(r.buffers) do
|
||||
if vim.api.nvim_buf_is_valid(buf) then
|
||||
local name = vim.api.nvim_buf_get_name(buf)
|
||||
if name:sub(1, #prefix) == prefix then
|
||||
fn(buf)
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
local function release(r)
|
||||
if repos[r.worktree] ~= r then
|
||||
return
|
||||
end
|
||||
if next(r.buffers) ~= nil or next(r.tabs) ~= nil then
|
||||
return
|
||||
end
|
||||
r:close()
|
||||
repos[r.worktree] = nil
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
---@return ow.Git.Repo?
|
||||
local function find_by_buf(buf)
|
||||
for _, r in pairs(repos) do
|
||||
if r.buffers[buf] then
|
||||
return r
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
---@param path string
|
||||
---@return ow.Git.Repo?
|
||||
function M.resolve(path)
|
||||
path = vim.fn.resolve(path)
|
||||
local function find_by_path(path)
|
||||
if path == "" then
|
||||
return nil
|
||||
end
|
||||
if repos[path] then
|
||||
return repos[path]
|
||||
end
|
||||
for wt, r in pairs(repos) do
|
||||
if path:sub(1, #wt + 1) == wt .. "/" then
|
||||
return r
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
---@return string
|
||||
local function path_for_buf(buf)
|
||||
local path = vim.api.nvim_buf_get_name(buf)
|
||||
if path == "" or path:match("^%a+://") then
|
||||
return vim.fn.getcwd()
|
||||
end
|
||||
return vim.fn.resolve(path)
|
||||
end
|
||||
|
||||
---@param arg? integer | string bufnr (default current) or worktree path
|
||||
---@return ow.Git.Repo?
|
||||
function M.find(arg)
|
||||
if type(arg) == "string" then
|
||||
return find_by_path(arg)
|
||||
end
|
||||
local buf = expand_buf(arg)
|
||||
return find_by_buf(buf) or find_by_path(path_for_buf(buf))
|
||||
end
|
||||
|
||||
---@param arg? integer | string bufnr (default current) or worktree path
|
||||
---@return ow.Git.Repo?
|
||||
function M.resolve(arg)
|
||||
local existing = M.find(arg)
|
||||
if existing then
|
||||
return existing
|
||||
end
|
||||
local path
|
||||
if type(arg) == "string" then
|
||||
path = vim.fn.resolve(arg)
|
||||
else
|
||||
path = path_for_buf(expand_buf(arg))
|
||||
end
|
||||
local found = vim.fs.find(".git", { upward = true, path = path })[1]
|
||||
if not found then
|
||||
return nil
|
||||
end
|
||||
local worktree = vim.fs.dirname(found)
|
||||
if repos[worktree] then
|
||||
return repos[worktree]
|
||||
end
|
||||
local stat = vim.uv.fs_stat(found)
|
||||
if not stat then
|
||||
return nil
|
||||
end
|
||||
local worktree = vim.fs.dirname(found)
|
||||
local gitdir
|
||||
if stat.type == "directory" then
|
||||
gitdir = found
|
||||
@@ -314,192 +326,123 @@ function M.resolve(path)
|
||||
util.warning(".git file at %s has no `gitdir:` line", found)
|
||||
return nil
|
||||
end
|
||||
if rel:match("^/") then
|
||||
gitdir = rel
|
||||
else
|
||||
gitdir = vim.fs.joinpath(worktree, rel)
|
||||
end
|
||||
gitdir = vim.fs.normalize(gitdir)
|
||||
end
|
||||
local r = repo_by_gitdir[gitdir]
|
||||
if not r then
|
||||
r = Repo.new(gitdir, worktree)
|
||||
repo_by_gitdir[gitdir] = r
|
||||
gitdir = vim.fs.normalize(
|
||||
rel:match("^/") and rel or vim.fs.joinpath(worktree, rel)
|
||||
)
|
||||
end
|
||||
local r = Repo.new(gitdir, worktree)
|
||||
repos[worktree] = r
|
||||
return r
|
||||
end
|
||||
|
||||
---@param buf integer?
|
||||
---@return ow.Git.Repo?
|
||||
function M.find(buf)
|
||||
buf = expand_buf(buf)
|
||||
local existing = repo_by_buf[buf]
|
||||
if existing then
|
||||
return existing
|
||||
end
|
||||
local path = vim.api.nvim_buf_get_name(buf)
|
||||
if path == "" or path:match("^%a+://") then
|
||||
path = vim.fn.getcwd()
|
||||
end
|
||||
return M.resolve(path)
|
||||
end
|
||||
|
||||
---@param buf integer?
|
||||
---@return ow.Git.BufState?
|
||||
---@param buf? integer
|
||||
---@return ow.Git.Repo.BufState?
|
||||
function M.state(buf)
|
||||
buf = expand_buf(buf)
|
||||
local r = repo_by_buf[buf]
|
||||
local r = find_by_buf(buf)
|
||||
return r and r.buffers[buf]
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
---@param buf? integer
|
||||
---@param r ow.Git.Repo
|
||||
function M.attach(buf, r)
|
||||
function M.bind(buf, r)
|
||||
buf = expand_buf(buf)
|
||||
if repo_by_buf[buf] == r then
|
||||
local prev = find_by_buf(buf)
|
||||
if prev == r then
|
||||
return
|
||||
end
|
||||
if repo_by_buf[buf] then
|
||||
repo_by_buf[buf]:remove_buffer(buf)
|
||||
if prev then
|
||||
prev.buffers[buf] = nil
|
||||
release(prev)
|
||||
end
|
||||
r:add_buffer(buf)
|
||||
repo_by_buf[buf] = r
|
||||
r.buffers[buf] = { repo = r }
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
---@return ow.Git.Repo?
|
||||
function M.register(buf)
|
||||
---@param buf? integer
|
||||
function M.unbind(buf)
|
||||
buf = expand_buf(buf)
|
||||
local r = M.find(buf)
|
||||
if not r then
|
||||
return nil
|
||||
end
|
||||
if repo_by_buf[buf] ~= r then
|
||||
M.attach(buf, r)
|
||||
end
|
||||
return r
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
function M.unregister(buf)
|
||||
buf = expand_buf(buf)
|
||||
local r = repo_by_buf[buf]
|
||||
local r = find_by_buf(buf)
|
||||
if not r then
|
||||
return
|
||||
end
|
||||
repo_by_buf[buf] = nil
|
||||
r:remove_buffer(buf)
|
||||
if not r:has_buffers() then
|
||||
r:stop_watcher()
|
||||
repo_by_gitdir[r.gitdir] = nil
|
||||
end
|
||||
r.buffers[buf] = nil
|
||||
release(r)
|
||||
end
|
||||
|
||||
---@param buf integer
|
||||
function M.refresh(buf)
|
||||
buf = expand_buf(buf)
|
||||
---@return boolean
|
||||
local function is_worktree_buf(buf)
|
||||
if not vim.api.nvim_buf_is_valid(buf) or vim.bo[buf].buftype ~= "" then
|
||||
return
|
||||
return false
|
||||
end
|
||||
local path = vim.api.nvim_buf_get_name(buf)
|
||||
if path == "" or path:match("^%a+://") then
|
||||
return path ~= "" and not path:match("^%a+://")
|
||||
end
|
||||
|
||||
---@param buf? integer
|
||||
function M.track(buf)
|
||||
buf = expand_buf(buf)
|
||||
if not is_worktree_buf(buf) then
|
||||
return
|
||||
end
|
||||
local r = M.register(buf)
|
||||
if not r then
|
||||
vim.b[buf].git_status = nil
|
||||
local r = M.resolve(buf)
|
||||
if r and not r.buffers[buf] then
|
||||
M.bind(buf, r)
|
||||
end
|
||||
end
|
||||
|
||||
---@param buf? integer
|
||||
function M.refresh(buf)
|
||||
local r = find_by_buf(expand_buf(buf))
|
||||
if r then
|
||||
r:refresh()
|
||||
end
|
||||
end
|
||||
|
||||
function M.refresh_all()
|
||||
for _, r in pairs(repos) do
|
||||
r:refresh()
|
||||
end
|
||||
end
|
||||
|
||||
function M.update_cwd_repo()
|
||||
local tab = vim.api.nvim_get_current_tabpage()
|
||||
local new = M.resolve(vim.fn.getcwd())
|
||||
local old
|
||||
for _, r in pairs(repos) do
|
||||
if r.tabs[tab] then
|
||||
old = r
|
||||
break
|
||||
end
|
||||
end
|
||||
if new == old then
|
||||
return
|
||||
end
|
||||
r:refresh()
|
||||
if old then
|
||||
old.tabs[tab] = nil
|
||||
release(old)
|
||||
end
|
||||
if new then
|
||||
new.tabs[tab] = true
|
||||
new:refresh()
|
||||
end
|
||||
end
|
||||
|
||||
---@param tab integer
|
||||
function M.release_tab(tab)
|
||||
for _, r in pairs(repos) do
|
||||
if r.tabs[tab] then
|
||||
r.tabs[tab] = nil
|
||||
release(r)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M.stop_all()
|
||||
for _, r in pairs(repo_by_gitdir) do
|
||||
r:stop_watcher()
|
||||
for _, r in pairs(repos) do
|
||||
r:close()
|
||||
end
|
||||
end
|
||||
|
||||
---@param line string
|
||||
---@return ow.Git.BranchInfo
|
||||
function M.parse_branch_line(line)
|
||||
local info = { ahead = 0, behind = 0 }
|
||||
local content = line:sub(4)
|
||||
local arrow = content:find("...", 1, true)
|
||||
if not arrow then
|
||||
info.head = content
|
||||
return info
|
||||
end
|
||||
info.head = content:sub(1, arrow - 1)
|
||||
local rest = content:sub(arrow + 3)
|
||||
local bracket = rest:find(" %[")
|
||||
if not bracket then
|
||||
info.upstream = rest
|
||||
return info
|
||||
end
|
||||
info.upstream = rest:sub(1, bracket - 1)
|
||||
local inside = rest:match("%[([^%]]+)%]")
|
||||
if inside then
|
||||
info.ahead = (tonumber(inside:match("ahead (%d+)")) or 0) --[[@as integer]]
|
||||
info.behind = (tonumber(inside:match("behind (%d+)")) or 0) --[[@as integer]]
|
||||
end
|
||||
return info
|
||||
end
|
||||
|
||||
---@param stdout string
|
||||
---@return ow.Git.BranchInfo, ow.Git.PorcelainGroups
|
||||
function M.parse_porcelain(stdout)
|
||||
local branch = { ahead = 0, behind = 0 }
|
||||
---@type ow.Git.PorcelainGroups
|
||||
local groups = {
|
||||
Untracked = {},
|
||||
Unstaged = {},
|
||||
Staged = {},
|
||||
Unmerged = {},
|
||||
}
|
||||
for line in stdout:gmatch("[^\r\n]+") do
|
||||
if line:sub(1, 2) == "##" then
|
||||
branch = M.parse_branch_line(line)
|
||||
else
|
||||
local x, y, path, orig = parse_porcelain_line(line)
|
||||
if x == "?" and y == "?" then
|
||||
table.insert(groups.Untracked, {
|
||||
section = "Untracked",
|
||||
x = x,
|
||||
y = y,
|
||||
path = path,
|
||||
orig = orig,
|
||||
})
|
||||
elseif status.UNMERGED[x .. y] then
|
||||
table.insert(groups.Unmerged, {
|
||||
section = "Unmerged",
|
||||
x = x,
|
||||
y = y,
|
||||
path = path,
|
||||
orig = orig,
|
||||
})
|
||||
else
|
||||
if x ~= " " then
|
||||
table.insert(groups.Staged, {
|
||||
section = "Staged",
|
||||
x = x,
|
||||
y = y,
|
||||
path = path,
|
||||
orig = orig,
|
||||
})
|
||||
end
|
||||
if y ~= " " then
|
||||
table.insert(groups.Unstaged, {
|
||||
section = "Unstaged",
|
||||
x = x,
|
||||
y = y,
|
||||
path = path,
|
||||
orig = orig,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return branch, groups
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
+157
-31
@@ -1,6 +1,6 @@
|
||||
local M = {}
|
||||
|
||||
M.UNMERGED = {
|
||||
local UNMERGED = {
|
||||
DD = true,
|
||||
AU = true,
|
||||
UD = true,
|
||||
@@ -10,48 +10,174 @@ M.UNMERGED = {
|
||||
UU = true,
|
||||
}
|
||||
|
||||
---@param code string
|
||||
---@return string? char
|
||||
---@return string? hl_group
|
||||
function M.indicator(code)
|
||||
if code == "" then
|
||||
return nil
|
||||
---@alias ow.Git.Status.EntryKind "untracked"|"unstaged"|"staged"|"unmerged"|"ignored"
|
||||
|
||||
---@class ow.Git.Status.Entry
|
||||
---@field path string
|
||||
---@field kind ow.Git.Status.EntryKind
|
||||
---@field char string
|
||||
---@field hl string
|
||||
---@field orig string?
|
||||
|
||||
---@class ow.Git.Status.BranchInfo
|
||||
---@field head string?
|
||||
---@field upstream string?
|
||||
---@field ahead integer
|
||||
---@field behind integer
|
||||
|
||||
---@class ow.Git.Status
|
||||
---@field branch ow.Git.Status.BranchInfo
|
||||
---@field entries table<string, ow.Git.Status.Entry[]>
|
||||
local Status = {}
|
||||
Status.__index = Status
|
||||
|
||||
---@param kind ow.Git.Status.EntryKind
|
||||
---@return ow.Git.Status.Entry[]
|
||||
function Status:by_kind(kind)
|
||||
local out = {}
|
||||
for _, list in pairs(self.entries) do
|
||||
for _, e in ipairs(list) do
|
||||
if e.kind == kind then
|
||||
table.insert(out, e)
|
||||
end
|
||||
end
|
||||
end
|
||||
if code == "??" then
|
||||
return "?", "GitUntracked"
|
||||
end
|
||||
if code == "!!" then
|
||||
return "!", "GitIgnored"
|
||||
end
|
||||
if M.UNMERGED[code] then
|
||||
return "U", "GitUnmerged"
|
||||
end
|
||||
local x, y = code:sub(1, 1), code:sub(2, 2)
|
||||
if x == "R" or y == "R" then
|
||||
return out
|
||||
end
|
||||
|
||||
---@param x string
|
||||
---@return string char, string hl
|
||||
local function staged_attrs(x)
|
||||
if x == "R" then
|
||||
return "R", "GitRenamed"
|
||||
end
|
||||
if y == " " and x ~= " " then
|
||||
return x, "GitStaged"
|
||||
return x, "GitStaged"
|
||||
end
|
||||
|
||||
---@param y string
|
||||
---@return string char, string hl
|
||||
local function unstaged_attrs(y)
|
||||
if y == "R" then
|
||||
return "R", "GitRenamed"
|
||||
end
|
||||
if y == "D" then
|
||||
return "D", "GitDeleted"
|
||||
end
|
||||
return "M", "GitUnstaged"
|
||||
return y, "GitUnstaged"
|
||||
end
|
||||
|
||||
---@param code string
|
||||
---@return string?
|
||||
function M.format(code)
|
||||
local char, hl = M.indicator(code)
|
||||
if not char then
|
||||
return nil
|
||||
---@param line string
|
||||
---@return ow.Git.Status.BranchInfo
|
||||
local function parse_branch_line(line)
|
||||
local info = { ahead = 0, behind = 0 }
|
||||
local content = line:sub(4)
|
||||
local arrow = content:find("...", 1, true)
|
||||
if not arrow then
|
||||
info.head = content
|
||||
return info
|
||||
end
|
||||
return string.format("%%#%s#%s%%*", hl, char)
|
||||
info.head = content:sub(1, arrow - 1)
|
||||
local rest = content:sub(arrow + 3)
|
||||
local bracket = rest:find(" %[")
|
||||
if not bracket then
|
||||
info.upstream = rest
|
||||
return info
|
||||
end
|
||||
info.upstream = rest:sub(1, bracket - 1)
|
||||
local inside = rest:match("%[([^%]]+)%]")
|
||||
if inside then
|
||||
info.ahead = (tonumber(inside:match("ahead (%d+)")) or 0) --[[@as integer]]
|
||||
info.behind = (tonumber(inside:match("behind (%d+)")) or 0) --[[@as integer]]
|
||||
end
|
||||
return info
|
||||
end
|
||||
|
||||
---@return string
|
||||
function M.statusline()
|
||||
return vim.b.git_status or ""
|
||||
---@param line string
|
||||
---@return string x, string y, string path, string? orig
|
||||
local function parse_status_line(line)
|
||||
local x = line:sub(1, 1)
|
||||
local y = line:sub(2, 2)
|
||||
local rest = line:sub(4)
|
||||
local orig
|
||||
if x == "R" or x == "C" or y == "R" or y == "C" then
|
||||
local arrow = rest:find(" -> ", 1, true)
|
||||
if arrow then
|
||||
orig = rest:sub(1, arrow - 1)
|
||||
rest = rest:sub(arrow + 4)
|
||||
end
|
||||
end
|
||||
return x, y, rest, orig
|
||||
end
|
||||
|
||||
---@param entries table<string, ow.Git.Status.Entry[]>
|
||||
---@param entry ow.Git.Status.Entry
|
||||
local function add(entries, entry)
|
||||
local key = entry.path
|
||||
if key:sub(-1) == "/" then
|
||||
key = key:sub(1, -2)
|
||||
end
|
||||
local list = entries[key] or {}
|
||||
table.insert(list, entry)
|
||||
entries[key] = list
|
||||
end
|
||||
|
||||
---@param stdout string
|
||||
---@return ow.Git.Status
|
||||
function M.parse(stdout)
|
||||
local branch = { ahead = 0, behind = 0 }
|
||||
---@type table<string, ow.Git.Status.Entry[]>
|
||||
local entries = {}
|
||||
for line in stdout:gmatch("[^\r\n]+") do
|
||||
if line:sub(1, 2) == "##" then
|
||||
branch = parse_branch_line(line)
|
||||
else
|
||||
local x, y, path, orig = parse_status_line(line)
|
||||
if x == "?" and y == "?" then
|
||||
add(entries, {
|
||||
path = path,
|
||||
kind = "untracked",
|
||||
char = "?",
|
||||
hl = "GitUntracked",
|
||||
})
|
||||
elseif x == "!" and y == "!" then
|
||||
add(entries, {
|
||||
path = path,
|
||||
kind = "ignored",
|
||||
char = "i",
|
||||
hl = "GitIgnored",
|
||||
})
|
||||
elseif UNMERGED[x .. y] then
|
||||
add(entries, {
|
||||
path = path,
|
||||
kind = "unmerged",
|
||||
char = "!",
|
||||
hl = "GitUnmerged",
|
||||
})
|
||||
else
|
||||
if x ~= " " then
|
||||
local char, hl = staged_attrs(x)
|
||||
add(entries, {
|
||||
path = path,
|
||||
kind = "staged",
|
||||
char = char,
|
||||
hl = hl,
|
||||
orig = orig,
|
||||
})
|
||||
end
|
||||
if y ~= " " then
|
||||
local char, hl = unstaged_attrs(y)
|
||||
add(entries, {
|
||||
path = path,
|
||||
kind = "unstaged",
|
||||
char = char,
|
||||
hl = hl,
|
||||
orig = orig,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return setmetatable({ branch = branch, entries = entries }, Status)
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -2,32 +2,18 @@ local Revision = require("git.revision")
|
||||
local diff = require("git.diff")
|
||||
local object = require("git.object")
|
||||
local repo = require("git.repo")
|
||||
local status = require("git.status")
|
||||
local util = require("git.util")
|
||||
|
||||
local M = {}
|
||||
|
||||
local SECTIONS = {
|
||||
"Untracked",
|
||||
"Unstaged",
|
||||
"Staged",
|
||||
"Unmerged",
|
||||
"Unpushed",
|
||||
"Unpulled",
|
||||
}
|
||||
local SIDEBAR_WIDTH = 50
|
||||
---@type ow.Git.Status.EntryKind[]
|
||||
local KINDS = { "untracked", "unstaged", "staged", "unmerged" }
|
||||
local WINDOW_WIDTH = 50
|
||||
|
||||
---@class ow.Git.CommitEntry
|
||||
---@field section "Unpushed"|"Unpulled"
|
||||
---@field sha string
|
||||
---@field subject string?
|
||||
|
||||
---@alias ow.Git.SidebarEntry ow.Git.StatusEntry | ow.Git.CommitEntry
|
||||
|
||||
---@class ow.Git.SidebarState
|
||||
---@class ow.Git.StatusView.State
|
||||
---@field repo ow.Git.Repo
|
||||
---@field lines table<integer, ow.Git.SidebarEntry>
|
||||
---@field sidebar_win integer?
|
||||
---@field lines table<integer, ow.Git.Status.Entry>
|
||||
---@field win integer?
|
||||
---@field invocation_win integer?
|
||||
---@field diff_left_win integer?
|
||||
---@field diff_right_win integer?
|
||||
@@ -35,181 +21,56 @@ local SIDEBAR_WIDTH = 50
|
||||
---@field last_shown_key string?
|
||||
---@field last_render_key string?
|
||||
|
||||
---@type table<integer, ow.Git.SidebarState>
|
||||
---@type table<integer, ow.Git.StatusView.State>
|
||||
local state = {}
|
||||
|
||||
local group = vim.api.nvim_create_augroup("ow.git.sidebar", { clear = false })
|
||||
local ns = vim.api.nvim_create_namespace("ow.git.sidebar")
|
||||
local group =
|
||||
vim.api.nvim_create_augroup("ow.git.status_win", { clear = false })
|
||||
local ns = vim.api.nvim_create_namespace("ow.git.status_win")
|
||||
|
||||
---@return integer? win
|
||||
---@return integer? bufnr
|
||||
local function find_sidebar()
|
||||
local function find_view()
|
||||
for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
|
||||
local buf = vim.api.nvim_win_get_buf(win)
|
||||
if vim.bo[buf].filetype == "gitsidebar" then
|
||||
if vim.bo[buf].filetype == "gitstatus" then
|
||||
return win, buf
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param s ow.Git.SidebarState
|
||||
---@param s ow.Git.StatusView.State
|
||||
---@return integer?
|
||||
local function sidebar_win_for(s)
|
||||
local win = s.sidebar_win
|
||||
local function win_for(s)
|
||||
local win = s.win
|
||||
if win and vim.api.nvim_win_is_valid(win) then
|
||||
return win
|
||||
end
|
||||
win = find_sidebar()
|
||||
s.sidebar_win = win
|
||||
win = find_view()
|
||||
s.win = win
|
||||
return win
|
||||
end
|
||||
|
||||
---@param entry ow.Git.SidebarEntry
|
||||
---@return string?
|
||||
local function entry_code(entry)
|
||||
if entry.section == "Untracked" then
|
||||
return "??"
|
||||
elseif entry.section == "Unmerged" then
|
||||
return entry.x .. entry.y
|
||||
elseif entry.section == "Staged" then
|
||||
return entry.x .. " "
|
||||
elseif entry.section == "Unstaged" then
|
||||
return " " .. entry.y
|
||||
end
|
||||
end
|
||||
|
||||
---@param entry ow.Git.SidebarEntry
|
||||
---@return string? line
|
||||
---@return string? hl_group
|
||||
---@return integer? hl_len
|
||||
---@param entry ow.Git.Status.Entry
|
||||
---@return string line
|
||||
---@return string hl_group
|
||||
---@return integer hl_len
|
||||
local function format_entry(entry)
|
||||
if entry.sha then
|
||||
return string.format(" %s %s", entry.sha, entry.subject or ""),
|
||||
"GitSha",
|
||||
#entry.sha
|
||||
end
|
||||
local code = entry_code(entry)
|
||||
if not code then
|
||||
return nil
|
||||
end
|
||||
local char, hl = status.indicator(code)
|
||||
if not char then
|
||||
return nil
|
||||
end
|
||||
local label = entry.orig and (entry.orig .. " -> " .. entry.path)
|
||||
or entry.path
|
||||
return string.format(" %s %s", char, label), hl, #char
|
||||
return string.format(" %s %s", entry.char, label), entry.hl, #entry.char
|
||||
end
|
||||
|
||||
---@param stdout string
|
||||
---@return ow.Git.BranchInfo, table<string, ow.Git.SidebarEntry[]>
|
||||
local function parse_porcelain(stdout)
|
||||
local branch, groups = repo.parse_porcelain(stdout)
|
||||
---@cast groups table<string, ow.Git.SidebarEntry[]>
|
||||
groups.Unpushed = {}
|
||||
groups.Unpulled = {}
|
||||
return branch, groups
|
||||
end
|
||||
|
||||
---@param worktree string
|
||||
---@param branch ow.Git.BranchInfo
|
||||
---@param groups table<string, ow.Git.SidebarEntry[]>
|
||||
local function enrich_with_log(worktree, branch, groups)
|
||||
local fetches = {}
|
||||
if branch.upstream and branch.ahead > 0 then
|
||||
table.insert(
|
||||
fetches,
|
||||
{ section = "Unpushed", range = "@{upstream}..HEAD" }
|
||||
)
|
||||
end
|
||||
if branch.upstream and branch.behind > 0 then
|
||||
table.insert(
|
||||
fetches,
|
||||
{ section = "Unpulled", range = "HEAD..@{upstream}" }
|
||||
)
|
||||
end
|
||||
local pending = {}
|
||||
for _, f in ipairs(fetches) do
|
||||
table.insert(pending, {
|
||||
f = f,
|
||||
sys = vim.system({
|
||||
"git",
|
||||
"log",
|
||||
"--max-count=200",
|
||||
"--format=%h %s",
|
||||
f.range,
|
||||
}, { cwd = worktree, text = true }),
|
||||
})
|
||||
end
|
||||
for _, p in ipairs(pending) do
|
||||
local result = p.sys:wait()
|
||||
if result.code == 0 then
|
||||
for line in (result.stdout or ""):gmatch("[^\r\n]+") do
|
||||
local sha, subject = line:match("^(%S+)%s+(.+)$")
|
||||
if sha then
|
||||
table.insert(groups[p.f.section], {
|
||||
section = p.f.section,
|
||||
sha = sha,
|
||||
subject = subject,
|
||||
})
|
||||
end
|
||||
end
|
||||
else
|
||||
util.error(
|
||||
"git log %s failed: %s",
|
||||
p.f.range,
|
||||
vim.trim(result.stderr or "")
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param worktree string
|
||||
---@param prefetched_stdout string?
|
||||
---@param callback fun(branch: ow.Git.BranchInfo, groups: table<string, ow.Git.SidebarEntry[]>)
|
||||
local function fetch_status(worktree, prefetched_stdout, callback)
|
||||
if prefetched_stdout then
|
||||
local branch, groups = parse_porcelain(prefetched_stdout)
|
||||
enrich_with_log(worktree, branch, groups)
|
||||
callback(branch, groups)
|
||||
return
|
||||
end
|
||||
vim.system(
|
||||
{
|
||||
"git",
|
||||
"-c",
|
||||
"core.quotePath=false",
|
||||
"status",
|
||||
"--porcelain=v1",
|
||||
"--branch",
|
||||
},
|
||||
{ cwd = worktree, text = true },
|
||||
vim.schedule_wrap(function(obj)
|
||||
if obj.code ~= 0 then
|
||||
util.error("git status failed: %s", vim.trim(obj.stderr or ""))
|
||||
local branch = { ahead = 0, behind = 0 }
|
||||
local groups = {
|
||||
Untracked = {},
|
||||
Unstaged = {},
|
||||
Staged = {},
|
||||
Unmerged = {},
|
||||
Unpushed = {},
|
||||
Unpulled = {},
|
||||
}
|
||||
callback(branch, groups)
|
||||
return
|
||||
end
|
||||
local branch, groups = parse_porcelain(obj.stdout or "")
|
||||
enrich_with_log(worktree, branch, groups)
|
||||
callback(branch, groups)
|
||||
end)
|
||||
)
|
||||
---@param kind ow.Git.Status.EntryKind
|
||||
---@return string
|
||||
local function display_name(kind)
|
||||
return (kind:gsub("^%l", string.upper))
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@param branch ow.Git.BranchInfo
|
||||
---@param groups table<string, ow.Git.SidebarEntry[]>
|
||||
local function render(bufnr, branch, groups)
|
||||
---@param status ow.Git.Status
|
||||
local function render(bufnr, status)
|
||||
local branch = status.branch
|
||||
local lines = { "Head: " .. (branch.head or "?") }
|
||||
if branch.upstream then
|
||||
local push = "Push: " .. branch.upstream
|
||||
@@ -225,24 +86,23 @@ local function render(bufnr, branch, groups)
|
||||
|
||||
local meta = {}
|
||||
local marks = {}
|
||||
for _, section in ipairs(SECTIONS) do
|
||||
local entries = groups[section]
|
||||
if entries and #entries > 0 then
|
||||
table.insert(lines, string.format("%s (%d)", section, #entries))
|
||||
for _, kind in ipairs(KINDS) do
|
||||
local entries = status:by_kind(kind)
|
||||
if #entries > 0 then
|
||||
table.insert(
|
||||
lines,
|
||||
string.format("%s (%d)", display_name(kind), #entries)
|
||||
)
|
||||
for _, entry in ipairs(entries) do
|
||||
local line, hl, hl_len = format_entry(entry)
|
||||
if line then
|
||||
table.insert(lines, line)
|
||||
meta[#lines] = entry
|
||||
if hl and hl_len then
|
||||
table.insert(marks, {
|
||||
row = #lines - 1,
|
||||
col = 2,
|
||||
end_col = 2 + hl_len,
|
||||
hl = hl,
|
||||
})
|
||||
end
|
||||
end
|
||||
table.insert(lines, line)
|
||||
meta[#lines] = entry
|
||||
table.insert(marks, {
|
||||
row = #lines - 1,
|
||||
col = 2,
|
||||
end_col = 2 + hl_len,
|
||||
hl = hl,
|
||||
})
|
||||
end
|
||||
table.insert(lines, "")
|
||||
end
|
||||
@@ -261,89 +121,78 @@ local function render(bufnr, branch, groups)
|
||||
state[bufnr].lines = meta
|
||||
end
|
||||
|
||||
---@param branch ow.Git.BranchInfo
|
||||
---@param groups table<string, ow.Git.SidebarEntry[]>
|
||||
---@param status ow.Git.Status
|
||||
---@return string
|
||||
local function fingerprint(branch, groups)
|
||||
local function fingerprint(status)
|
||||
local branch = status.branch
|
||||
local parts = {
|
||||
branch.head or "",
|
||||
branch.upstream or "",
|
||||
tostring(branch.ahead),
|
||||
tostring(branch.behind),
|
||||
}
|
||||
for _, section in ipairs(SECTIONS) do
|
||||
local entries = groups[section]
|
||||
if entries then
|
||||
for _, e in ipairs(entries) do
|
||||
table.insert(
|
||||
parts,
|
||||
e.section
|
||||
.. ":"
|
||||
.. (e.path or e.sha or "")
|
||||
.. ":"
|
||||
.. (e.orig or "")
|
||||
.. ":"
|
||||
.. (e.x or "")
|
||||
.. ":"
|
||||
.. (e.y or "")
|
||||
)
|
||||
end
|
||||
for _, kind in ipairs(KINDS) do
|
||||
for _, e in ipairs(status:by_kind(kind)) do
|
||||
table.insert(
|
||||
parts,
|
||||
e.kind
|
||||
.. ":"
|
||||
.. e.path
|
||||
.. ":"
|
||||
.. (e.orig or "")
|
||||
.. ":"
|
||||
.. e.char
|
||||
)
|
||||
end
|
||||
end
|
||||
return table.concat(parts, "\0")
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@param prefetched_stdout string?
|
||||
local function refresh(bufnr, prefetched_stdout)
|
||||
local function refresh(bufnr)
|
||||
local s = state[bufnr]
|
||||
if not s then
|
||||
return
|
||||
end
|
||||
|
||||
local saved_path, saved_sha
|
||||
local sidebar_win = sidebar_win_for(s)
|
||||
if sidebar_win then
|
||||
local lnum = vim.api.nvim_win_get_cursor(sidebar_win)[1]
|
||||
local saved_path
|
||||
local status_win = win_for(s)
|
||||
if status_win then
|
||||
local lnum = vim.api.nvim_win_get_cursor(status_win)[1]
|
||||
local entry = s.lines[lnum]
|
||||
if entry then
|
||||
saved_path = entry.path
|
||||
saved_sha = entry.sha
|
||||
end
|
||||
end
|
||||
|
||||
fetch_status(s.repo.worktree, prefetched_stdout, function(branch, groups)
|
||||
if not vim.api.nvim_buf_is_valid(bufnr) then
|
||||
return
|
||||
end
|
||||
s.last_shown_key = nil
|
||||
local fp = fingerprint(branch, groups)
|
||||
if fp == s.last_render_key then
|
||||
return
|
||||
end
|
||||
s.last_render_key = fp
|
||||
render(bufnr, branch, groups)
|
||||
if not saved_path and not saved_sha then
|
||||
return
|
||||
end
|
||||
for lnum, entry in pairs(s.lines) do
|
||||
if
|
||||
(saved_path and entry.path == saved_path)
|
||||
or (saved_sha and entry.sha == saved_sha)
|
||||
then
|
||||
local win = sidebar_win_for(s)
|
||||
if win then
|
||||
pcall(vim.api.nvim_win_set_cursor, win, { lnum, 0 })
|
||||
end
|
||||
break
|
||||
if not vim.api.nvim_buf_is_valid(bufnr) then
|
||||
return
|
||||
end
|
||||
local status = s.repo.status
|
||||
s.last_shown_key = nil
|
||||
local fp = fingerprint(status)
|
||||
if fp == s.last_render_key then
|
||||
return
|
||||
end
|
||||
s.last_render_key = fp
|
||||
render(bufnr, status)
|
||||
if not saved_path then
|
||||
return
|
||||
end
|
||||
for lnum, entry in pairs(s.lines) do
|
||||
if entry.path == saved_path then
|
||||
local win = win_for(s)
|
||||
if win then
|
||||
pcall(vim.api.nvim_win_set_cursor, win, { lnum, 0 })
|
||||
end
|
||||
break
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
---@param bufnr integer
|
||||
---@return ow.Git.SidebarState?
|
||||
---@return ow.Git.SidebarEntry?
|
||||
---@return ow.Git.StatusView.State?
|
||||
---@return ow.Git.Status.Entry?
|
||||
local function current_entry(bufnr)
|
||||
local s = state[bufnr]
|
||||
if not s then
|
||||
@@ -353,17 +202,9 @@ local function current_entry(bufnr)
|
||||
return s, s.lines[lnum]
|
||||
end
|
||||
|
||||
---@class ow.Git.DiffSide
|
||||
---@field buf integer
|
||||
---@field name string?
|
||||
|
||||
---@class ow.Git.DiffPair
|
||||
---@field left ow.Git.DiffSide
|
||||
---@field right ow.Git.DiffSide
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
---@param path string
|
||||
---@return ow.Git.DiffSide
|
||||
---@return ow.Git.Diff.Side
|
||||
local function head_pane(r, path)
|
||||
local rev = Revision.new({ base = "HEAD", path = path })
|
||||
return {
|
||||
@@ -374,16 +215,16 @@ end
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
---@param path string
|
||||
---@return ow.Git.DiffSide
|
||||
---@return ow.Git.Diff.Side
|
||||
local function worktree_pane(r, path)
|
||||
local buf = vim.fn.bufadd(vim.fs.joinpath(r.worktree, path))
|
||||
vim.fn.bufload(buf)
|
||||
return { buf = buf, name = nil }
|
||||
end
|
||||
|
||||
---@param s ow.Git.SidebarState
|
||||
---@param entry ow.Git.StatusEntry
|
||||
---@return ow.Git.DiffSide
|
||||
---@param s ow.Git.StatusView.State
|
||||
---@param entry ow.Git.Status.Entry
|
||||
---@return ow.Git.Diff.Side
|
||||
local function index_pane(s, entry)
|
||||
local rev = Revision.new({ stage = 0, path = entry.path })
|
||||
return {
|
||||
@@ -392,39 +233,39 @@ local function index_pane(s, entry)
|
||||
}
|
||||
end
|
||||
|
||||
---@param s ow.Git.SidebarState
|
||||
---@param entry ow.Git.StatusEntry
|
||||
---@return ow.Git.DiffSide?
|
||||
---@param s ow.Git.StatusView.State
|
||||
---@param entry ow.Git.Status.Entry
|
||||
---@return ow.Git.Diff.Side?
|
||||
local function older_pane(s, entry)
|
||||
if entry.section == "Staged" then
|
||||
if entry.x == "A" then
|
||||
if entry.kind == "staged" then
|
||||
if entry.char == "A" then
|
||||
return nil
|
||||
end
|
||||
return head_pane(s.repo, entry.orig or entry.path)
|
||||
end
|
||||
if entry.section == "Unstaged" then
|
||||
if entry.kind == "unstaged" then
|
||||
return index_pane(s, entry)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
---@param s ow.Git.SidebarState
|
||||
---@param entry ow.Git.StatusEntry
|
||||
---@return ow.Git.DiffSide?
|
||||
---@param s ow.Git.StatusView.State
|
||||
---@param entry ow.Git.Status.Entry
|
||||
---@return ow.Git.Diff.Side?
|
||||
local function newer_pane(s, entry)
|
||||
if entry.section == "Staged" then
|
||||
if entry.x == "D" then
|
||||
if entry.kind == "staged" then
|
||||
if entry.char == "D" then
|
||||
return nil
|
||||
end
|
||||
return index_pane(s, entry)
|
||||
end
|
||||
if entry.section == "Unstaged" then
|
||||
if entry.y == "D" then
|
||||
if entry.kind == "unstaged" then
|
||||
if entry.char == "D" then
|
||||
return nil
|
||||
end
|
||||
return worktree_pane(s.repo, entry.path)
|
||||
end
|
||||
if entry.section == "Untracked" then
|
||||
if entry.kind == "untracked" then
|
||||
return worktree_pane(s.repo, entry.path)
|
||||
end
|
||||
return nil
|
||||
@@ -439,14 +280,14 @@ local function reset_diff_win(win)
|
||||
end)
|
||||
end
|
||||
|
||||
---@param s ow.Git.SidebarState
|
||||
---@param s ow.Git.StatusView.State
|
||||
---@return integer?
|
||||
local function invocation_win_for(s)
|
||||
local win = s.invocation_win
|
||||
if not win or not vim.api.nvim_win_is_valid(win) then
|
||||
return nil
|
||||
end
|
||||
if win == s.sidebar_win then
|
||||
if win == s.win then
|
||||
return nil
|
||||
end
|
||||
if
|
||||
@@ -458,11 +299,11 @@ local function invocation_win_for(s)
|
||||
return win
|
||||
end
|
||||
|
||||
---@param s ow.Git.SidebarState
|
||||
---@param sidebar_win integer
|
||||
---@param s ow.Git.StatusView.State
|
||||
---@param status_win integer
|
||||
---@return integer? left
|
||||
---@return integer? right
|
||||
local function adopt_diff_wins(s, sidebar_win)
|
||||
local function adopt_diff_wins(s, status_win)
|
||||
local left = s.diff_left_win
|
||||
local right = s.diff_right_win
|
||||
if left and not vim.api.nvim_win_is_valid(left) then
|
||||
@@ -475,7 +316,7 @@ local function adopt_diff_wins(s, sidebar_win)
|
||||
return left, right
|
||||
end
|
||||
for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
|
||||
if win ~= sidebar_win then
|
||||
if win ~= status_win then
|
||||
local role = vim.w[win].git_diff_role
|
||||
if role == "left" and not left then
|
||||
left = win
|
||||
@@ -487,10 +328,10 @@ local function adopt_diff_wins(s, sidebar_win)
|
||||
return left, right
|
||||
end
|
||||
|
||||
---@param entry ow.Git.StatusEntry
|
||||
---@param entry ow.Git.Status.Entry
|
||||
---@return string
|
||||
local function entry_key(entry)
|
||||
return entry.section .. "|" .. entry.path .. "|" .. (entry.orig or "")
|
||||
return entry.kind .. "|" .. entry.path .. "|" .. (entry.orig or "")
|
||||
end
|
||||
|
||||
---@param target_win integer
|
||||
@@ -506,11 +347,11 @@ local function vsplit_at(target_win, dir)
|
||||
return win
|
||||
end
|
||||
|
||||
---@param s ow.Git.SidebarState
|
||||
---@param sidebar_win integer
|
||||
---@param s ow.Git.StatusView.State
|
||||
---@param status_win integer
|
||||
---@param right_win integer?
|
||||
---@return integer
|
||||
local function ensure_right_win(s, sidebar_win, right_win)
|
||||
local function ensure_right_win(s, status_win, right_win)
|
||||
if right_win then
|
||||
return right_win
|
||||
end
|
||||
@@ -518,46 +359,38 @@ local function ensure_right_win(s, sidebar_win, right_win)
|
||||
if target then
|
||||
right_win = target
|
||||
else
|
||||
right_win = vsplit_at(sidebar_win, "right")
|
||||
vim.api.nvim_win_set_width(sidebar_win, SIDEBAR_WIDTH)
|
||||
right_win = vsplit_at(status_win, "right")
|
||||
vim.api.nvim_win_set_width(status_win, WINDOW_WIDTH)
|
||||
end
|
||||
reset_diff_win(right_win)
|
||||
return right_win
|
||||
end
|
||||
|
||||
---@param s ow.Git.SidebarState
|
||||
---@param entry ow.Git.SidebarEntry
|
||||
---@param s ow.Git.StatusView.State
|
||||
---@param entry ow.Git.Status.Entry
|
||||
---@param focus_left boolean
|
||||
local function view_entry(s, entry, focus_left)
|
||||
if not entry.path then
|
||||
return
|
||||
end
|
||||
---@cast entry ow.Git.StatusEntry
|
||||
local sidebar_win = sidebar_win_for(s)
|
||||
if not sidebar_win then
|
||||
local status_win = win_for(s)
|
||||
if not status_win then
|
||||
return
|
||||
end
|
||||
|
||||
local left = older_pane(s, entry)
|
||||
local right = newer_pane(s, entry)
|
||||
if not left and not right then
|
||||
util.warning(
|
||||
"no content for %s entry: %s",
|
||||
string.lower(entry.section),
|
||||
entry.path
|
||||
)
|
||||
util.warning("no content for %s entry: %s", entry.kind, entry.path)
|
||||
return
|
||||
end
|
||||
|
||||
local key = entry_key(entry)
|
||||
local left_win, right_win = adopt_diff_wins(s, sidebar_win)
|
||||
local left_win, right_win = adopt_diff_wins(s, status_win)
|
||||
local want_pair = left and right
|
||||
|
||||
if s.last_shown_key == key then
|
||||
local intact = (want_pair and left_win and right_win)
|
||||
or (not want_pair and right_win and not left_win)
|
||||
if intact then
|
||||
local target = focus_left and (left_win or right_win) or sidebar_win
|
||||
local target = focus_left and (left_win or right_win) or status_win
|
||||
vim.api.nvim_set_current_win(target)
|
||||
return
|
||||
end
|
||||
@@ -569,22 +402,22 @@ local function view_entry(s, entry, focus_left)
|
||||
left_win = nil
|
||||
s.diff_left_win = nil
|
||||
end
|
||||
right_win = ensure_right_win(s, sidebar_win, right_win)
|
||||
right_win = ensure_right_win(s, status_win, right_win)
|
||||
s.diff_right_win = right_win
|
||||
vim.w[right_win].git_diff_role = "right"
|
||||
local side = left or right
|
||||
---@cast side ow.Git.DiffSide
|
||||
---@cast side ow.Git.Diff.Side
|
||||
diff.set_diff(right_win, false)
|
||||
vim.api.nvim_win_set_buf(right_win, side.buf)
|
||||
if side.name then
|
||||
util.set_buf_name(side.buf, side.name)
|
||||
end
|
||||
s.last_shown_key = key
|
||||
vim.api.nvim_set_current_win(focus_left and right_win or sidebar_win)
|
||||
vim.api.nvim_set_current_win(focus_left and right_win or status_win)
|
||||
return
|
||||
end
|
||||
---@cast left ow.Git.DiffSide
|
||||
---@cast right ow.Git.DiffSide
|
||||
---@cast left ow.Git.Diff.Side
|
||||
---@cast right ow.Git.Diff.Side
|
||||
|
||||
if left_win and not right_win then
|
||||
right_win = vsplit_at(left_win, "right")
|
||||
@@ -593,7 +426,7 @@ local function view_entry(s, entry, focus_left)
|
||||
left_win = vsplit_at(right_win, "left")
|
||||
reset_diff_win(left_win)
|
||||
elseif not (left_win or right_win) then
|
||||
right_win = ensure_right_win(s, sidebar_win, nil)
|
||||
right_win = ensure_right_win(s, status_win, nil)
|
||||
left_win = vsplit_at(right_win, "left")
|
||||
reset_diff_win(left_win)
|
||||
local combined = vim.api.nvim_win_get_width(left_win)
|
||||
@@ -611,7 +444,7 @@ local function view_entry(s, entry, focus_left)
|
||||
diff.update_pair(left_win, right_win, { left = left, right = right })
|
||||
s.last_shown_key = key
|
||||
|
||||
vim.api.nvim_set_current_win(focus_left and left_win or sidebar_win)
|
||||
vim.api.nvim_set_current_win(focus_left and left_win or status_win)
|
||||
end
|
||||
|
||||
---@param focus_left boolean
|
||||
@@ -625,11 +458,10 @@ end
|
||||
|
||||
local function action_stage()
|
||||
local s, entry = current_entry(vim.api.nvim_get_current_buf())
|
||||
if not s or not entry or not entry.path then
|
||||
if not s or not entry then
|
||||
return
|
||||
end
|
||||
---@cast entry ow.Git.StatusEntry
|
||||
if entry.section == "Staged" then
|
||||
if entry.kind == "staged" then
|
||||
return
|
||||
end
|
||||
vim.system(
|
||||
@@ -645,11 +477,10 @@ end
|
||||
|
||||
local function action_unstage()
|
||||
local s, entry = current_entry(vim.api.nvim_get_current_buf())
|
||||
if not s or not entry or not entry.path then
|
||||
if not s or not entry then
|
||||
return
|
||||
end
|
||||
---@cast entry ow.Git.StatusEntry
|
||||
if entry.section ~= "Staged" then
|
||||
if entry.kind ~= "staged" then
|
||||
return
|
||||
end
|
||||
local cmd = { "git", "restore", "--staged", "--" }
|
||||
@@ -673,17 +504,16 @@ end
|
||||
|
||||
local function action_discard()
|
||||
local s, entry = current_entry(vim.api.nvim_get_current_buf())
|
||||
if not s or not entry or not entry.path then
|
||||
if not s or not entry then
|
||||
return
|
||||
end
|
||||
---@cast entry ow.Git.StatusEntry
|
||||
if entry.section == "Staged" then
|
||||
if entry.kind == "staged" then
|
||||
util.warning("file has staged changes, unstage first with 'u'")
|
||||
return
|
||||
end
|
||||
|
||||
local prompt, action
|
||||
if entry.section == "Untracked" then
|
||||
if entry.kind == "untracked" then
|
||||
local is_dir = entry.path:sub(-1) == "/"
|
||||
prompt = string.format(
|
||||
"Delete untracked %s %s?",
|
||||
@@ -698,7 +528,7 @@ local function action_discard()
|
||||
end
|
||||
refresh(vim.api.nvim_get_current_buf())
|
||||
end
|
||||
elseif entry.section == "Unstaged" then
|
||||
elseif entry.kind == "unstaged" then
|
||||
prompt = string.format("Discard changes to %s?", entry.path)
|
||||
action = function()
|
||||
vim.system(
|
||||
@@ -725,7 +555,7 @@ end
|
||||
|
||||
local function action_help()
|
||||
print(table.concat({
|
||||
"git status sidebar",
|
||||
"git status window",
|
||||
" <Tab> preview diff (keep focus)",
|
||||
" <CR> open diff (focus left pane)",
|
||||
" s stage file",
|
||||
@@ -737,7 +567,7 @@ end
|
||||
|
||||
---@param r ow.Git.Repo
|
||||
local function open(r)
|
||||
local existing = find_sidebar()
|
||||
local existing = find_view()
|
||||
if existing then
|
||||
vim.api.nvim_set_current_win(existing)
|
||||
return
|
||||
@@ -745,7 +575,7 @@ local function open(r)
|
||||
|
||||
local previous_win = vim.api.nvim_get_current_win()
|
||||
local bufnr, win = util.new_scratch({ split = "left" })
|
||||
vim.bo[bufnr].filetype = "gitsidebar"
|
||||
vim.bo[bufnr].filetype = "gitstatus"
|
||||
|
||||
vim.wo[win].number = false
|
||||
vim.wo[win].relativenumber = false
|
||||
@@ -753,12 +583,12 @@ local function open(r)
|
||||
vim.wo[win].signcolumn = "no"
|
||||
vim.wo[win].cursorline = true
|
||||
vim.wo[win].winfixwidth = true
|
||||
vim.api.nvim_win_set_width(win, SIDEBAR_WIDTH)
|
||||
vim.api.nvim_win_set_width(win, WINDOW_WIDTH)
|
||||
|
||||
state[bufnr] = {
|
||||
repo = r,
|
||||
lines = {},
|
||||
sidebar_win = win,
|
||||
win = win,
|
||||
invocation_win = previous_win,
|
||||
}
|
||||
|
||||
@@ -781,8 +611,8 @@ local function open(r)
|
||||
k("X", action_discard, "Discard worktree changes")
|
||||
k("g?", action_help, "Help")
|
||||
|
||||
state[bufnr].unsubscribe = r:on_refresh(function(_, porcelain_stdout)
|
||||
refresh(bufnr, porcelain_stdout)
|
||||
state[bufnr].unsubscribe = r:on("refresh", function()
|
||||
refresh(bufnr)
|
||||
end)
|
||||
vim.api.nvim_create_autocmd({ "BufWipeout", "BufDelete" }, {
|
||||
buffer = bufnr,
|
||||
@@ -804,12 +634,12 @@ local function open(r)
|
||||
end
|
||||
|
||||
function M.toggle()
|
||||
local sidebar_win = find_sidebar()
|
||||
if sidebar_win then
|
||||
vim.api.nvim_win_close(sidebar_win, false)
|
||||
local status_win = find_view()
|
||||
if status_win then
|
||||
vim.api.nvim_win_close(status_win, false)
|
||||
return
|
||||
end
|
||||
local r = repo.find()
|
||||
local r = repo.resolve()
|
||||
if not r then
|
||||
util.warning("not in a git repository")
|
||||
return
|
||||
@@ -0,0 +1,31 @@
|
||||
local repo = require("git.repo")
|
||||
|
||||
local M = {}
|
||||
|
||||
---@return string
|
||||
function M.render()
|
||||
local r = repo.find()
|
||||
if not r then
|
||||
return ""
|
||||
end
|
||||
local name = vim.api.nvim_buf_get_name(0)
|
||||
if name == "" then
|
||||
return ""
|
||||
end
|
||||
local rel = vim.fs.relpath(r.worktree, vim.fn.resolve(name))
|
||||
local list = rel and r.status.entries[rel]
|
||||
if not list then
|
||||
return ""
|
||||
end
|
||||
local parts = {}
|
||||
for _, e in ipairs(list) do
|
||||
table.insert(parts, string.format("%%#%s#%s%%*", e.hl, e.char))
|
||||
end
|
||||
return table.concat(parts, " ")
|
||||
end
|
||||
|
||||
repo.on("refresh", function()
|
||||
vim.cmd.redrawstatus({ bang = true })
|
||||
end)
|
||||
|
||||
return M
|
||||
+42
-6
@@ -1,11 +1,11 @@
|
||||
local M = {}
|
||||
|
||||
---@class ow.Git.ScratchOpts
|
||||
---@class ow.Git.Util.ScratchOpts
|
||||
---@field name string?
|
||||
---@field bufhidden ("hide"|"wipe")?
|
||||
|
||||
---@param buf integer
|
||||
---@param opts ow.Git.ScratchOpts
|
||||
---@param opts ow.Git.Util.ScratchOpts
|
||||
local function setup_scratch(buf, opts)
|
||||
vim.bo[buf].buftype = "nofile"
|
||||
vim.bo[buf].bufhidden = opts.bufhidden or "wipe"
|
||||
@@ -43,10 +43,10 @@ function M.place_buf(buf, split)
|
||||
return win
|
||||
end
|
||||
|
||||
---@class ow.Git.NewScratchOpts : ow.Git.ScratchOpts
|
||||
---@class ow.Git.Util.NewScratchOpts : ow.Git.Util.ScratchOpts
|
||||
---@field split (false|"above"|"below"|"left"|"right")?
|
||||
|
||||
---@param opts ow.Git.NewScratchOpts?
|
||||
---@param opts ow.Git.Util.NewScratchOpts?
|
||||
---@return integer buf
|
||||
---@return integer win
|
||||
function M.new_scratch(opts)
|
||||
@@ -170,14 +170,14 @@ function M.debounce(fn, delay)
|
||||
}
|
||||
end
|
||||
|
||||
---@class ow.Git.ExecOpts
|
||||
---@class ow.Git.Util.ExecOpts
|
||||
---@field cwd string?
|
||||
---@field stdin string?
|
||||
---@field silent boolean?
|
||||
---@field on_done fun(stdout: string?)?
|
||||
|
||||
---@param cmd string[]
|
||||
---@param opts ow.Git.ExecOpts?
|
||||
---@param opts ow.Git.Util.ExecOpts?
|
||||
---@return string?
|
||||
function M.exec(cmd, opts)
|
||||
opts = opts or {}
|
||||
@@ -209,4 +209,40 @@ function M.exec(cmd, opts)
|
||||
return handle(vim.system(cmd, sys_opts):wait())
|
||||
end
|
||||
|
||||
---@class ow.Git.Util.Emitter<T>
|
||||
---@field private _listeners table<T, (fun(...))[]>
|
||||
local Emitter = {}
|
||||
Emitter.__index = Emitter
|
||||
|
||||
---@return ow.Git.Util.Emitter
|
||||
function Emitter.new()
|
||||
return setmetatable({ _listeners = {} }, Emitter)
|
||||
end
|
||||
|
||||
---@param event T
|
||||
---@param fn fun(...)
|
||||
---@return fun() unsubscribe
|
||||
function Emitter:on(event, fn)
|
||||
local list = self._listeners[event] or {}
|
||||
self._listeners[event] = list
|
||||
table.insert(list, fn)
|
||||
return function()
|
||||
for i, f in ipairs(list) do
|
||||
if f == fn then
|
||||
table.remove(list, i)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param event T
|
||||
function Emitter:emit(event, ...)
|
||||
for _, fn in ipairs(self._listeners[event] or {}) do
|
||||
fn(...)
|
||||
end
|
||||
end
|
||||
|
||||
M.Emitter = Emitter
|
||||
|
||||
return M
|
||||
|
||||
Reference in New Issue
Block a user