refactor(git): convert blocking subprocess calls to async

This commit is contained in:
2026-04-27 16:02:14 +02:00
parent 00eae8dbb9
commit 6a86a75ed5
8 changed files with 338 additions and 253 deletions
+35 -28
View File
@@ -1,3 +1,4 @@
local git = require("git")
local log = require("log") local log = require("log")
local repo = require("git.repo") local repo = require("git.repo")
local util = require("util") local util = require("util")
@@ -39,11 +40,7 @@ local function prefetch_cmds()
vim.system( vim.system(
{ "git", "--list-cmds=main,others,alias" }, { "git", "--list-cmds=main,others,alias" },
{ text = true }, { text = true },
function(result) vim.schedule_wrap(populate_cached_cmds)
vim.schedule(function()
populate_cached_cmds(result)
end)
end
) )
end end
@@ -75,33 +72,41 @@ end
---@param args string[] ---@param args string[]
---@param conf ow.Git.SplitHandler ---@param conf ow.Git.SplitHandler
local function run_in_split(worktree, args, conf) local function run_in_split(worktree, args, conf)
vim.cmd("new") local buf = git.new_scratch()
local buf = vim.api.nvim_get_current_buf()
vim.bo[buf].buftype = "nofile"
vim.bo[buf].bufhidden = "hide"
vim.bo[buf].swapfile = false
vim.bo[buf].modifiable = false
vim.b[buf].git_worktree = worktree vim.b[buf].git_worktree = worktree
if conf.needs_ref then if conf.needs_ref then
local user_ref = first_positional(args, 2) or "HEAD" local user_ref = first_positional(args, 2) or "HEAD"
local sha = repo.rev_parse(worktree, user_ref, true) local function apply_name(label)
if sha then if not vim.api.nvim_buf_is_valid(buf) then
vim.b[buf].git_ref = sha return
vim.b[buf].git_parent_ref = end
repo.rev_parse(worktree, user_ref .. "^", true) pcall(vim.api.nvim_buf_set_name, buf, "git://" .. label .. "/")
vim.bo[buf].filetype = conf.ft
end end
pcall( repo.rev_parse(worktree, user_ref, true, function(sha)
vim.api.nvim_buf_set_name, if not sha then
buf, apply_name(user_ref)
"git://" .. (sha or user_ref) .. "/" return
) end
repo.rev_parse(worktree, user_ref .. "^", true, function(parent)
if not vim.api.nvim_buf_is_valid(buf) then
return
end
vim.b[buf].git_ref = sha
vim.b[buf].git_parent_ref = parent
apply_name(sha)
end)
end)
else
vim.bo[buf].filetype = conf.ft
end end
vim.bo[buf].filetype = conf.ft
local cmd = { "git" } local cmd = { "git" }
vim.list_extend(cmd, args) vim.list_extend(cmd, args)
vim.system(cmd, { cwd = worktree, text = true }, function(obj) vim.system(
vim.schedule(function() cmd,
{ cwd = worktree, text = true },
vim.schedule_wrap(function(obj)
if not vim.api.nvim_buf_is_valid(buf) then if not vim.api.nvim_buf_is_valid(buf) then
return return
end end
@@ -124,7 +129,7 @@ local function run_in_split(worktree, args, conf)
) )
end end
end) end)
end) )
end end
---@param worktree string ---@param worktree string
@@ -132,8 +137,10 @@ end
local function run_to_messages(worktree, args) local function run_to_messages(worktree, args)
local cmd = { "git" } local cmd = { "git" }
vim.list_extend(cmd, args) vim.list_extend(cmd, args)
vim.system(cmd, { cwd = worktree, text = true }, function(obj) vim.system(
vim.schedule(function() cmd,
{ cwd = worktree, text = true },
vim.schedule_wrap(function(obj)
local out = vim.trim(obj.stdout or "") local out = vim.trim(obj.stdout or "")
local err = vim.trim(obj.stderr or "") local err = vim.trim(obj.stderr or "")
local chunks = {} local chunks = {}
@@ -156,7 +163,7 @@ local function run_to_messages(worktree, args)
vim.api.nvim_echo(chunks, true, {}) vim.api.nvim_echo(chunks, true, {})
end end
end) end)
end) )
end end
---@param args string[] ---@param args string[]
+20 -14
View File
@@ -67,20 +67,26 @@ function M.commit(opts)
if amend then if amend then
table.insert(cmd, "--amend") table.insert(cmd, "--amend")
end end
local result = vim.system(cmd, { cwd = worktree, text = true }) vim.system(
:wait() cmd,
if result.code ~= 0 then { cwd = worktree, text = true },
log.error( vim.schedule_wrap(function(result)
"git commit failed: %s", if result.code ~= 0 then
vim.trim(result.stderr or "") log.error(
) "git commit failed: %s",
return vim.trim(result.stderr or "")
end )
local out = vim.trim(result.stdout or "") return
if out ~= "" then end
log.info("%s", out) local out = vim.trim(result.stdout or "")
end if out ~= "" then
vim.api.nvim_buf_delete(buf, { force = true }) log.info("%s", out)
end
if vim.api.nvim_buf_is_valid(buf) then
vim.api.nvim_buf_delete(buf, { force = true })
end
end)
)
end, end,
}) })
end end
+48 -31
View File
@@ -59,47 +59,62 @@ local function attach_index_writer(buf, worktree, path)
}) })
end end
---Run `git show <revspec>` asynchronously and call `callback(lines?)` on the
---main loop. `lines` is nil on failure (the error is logged).
---@param worktree string ---@param worktree string
---@param revspec string anything `git show` accepts (e.g. `HEAD:foo`, `:foo`, blob SHA) ---@param revspec string anything `git show` accepts (e.g. `HEAD:foo`, `:foo`, blob SHA)
---@return string[] ---@param callback fun(lines: string[]?)
local function read_show(worktree, revspec) local function read_show_async(worktree, revspec, callback)
local result = vim.system( vim.system(
{ "git", "show", revspec }, { "git", "show", revspec },
{ cwd = worktree, text = true } { cwd = worktree, text = true },
vim.schedule_wrap(function(result)
if result.code ~= 0 then
log.error(
"git show %s failed: %s",
revspec,
vim.trim(result.stderr or "")
)
callback(nil)
return
end
callback(util.split_lines(result.stdout or ""))
end)
) )
:wait()
if result.code ~= 0 then
log.error(
"git show %s failed: %s",
revspec,
vim.trim(result.stderr or "")
)
return {}
end
return util.split_lines(result.stdout or "")
end end
---Internal builder: run `git show <revspec>`, drop the result into a fresh ---Internal builder: create a scratch buffer immediately and asynchronously
---scratch buffer, and (when `is_index` is true) wire up the BufWriteCmd ---fill it with the content of `git show <revspec>`. Returning the buffer
---that writes back to the git index for `index_path`. ---synchronously lets callers wire up windows / `:diffthis` right away; the
---diff updates when the content arrives. The buffer starts non-modifiable
---and the index `BufWriteCmd` is only attached after a successful load, so
---a premature `:w` can't blow away the index entry with empty content.
---@param worktree string ---@param worktree string
---@param revspec string ---@param revspec string
---@param is_index boolean ---@param is_index boolean
---@param index_path string? required when is_index is true ---@param index_path string? required when is_index is true
---@return integer ---@return integer
local function build_show_buf(worktree, revspec, is_index, index_path) local function build_show_buf(worktree, revspec, is_index, index_path)
local lines = read_show(worktree, revspec)
local buf = vim.api.nvim_create_buf(false, true) local buf = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) vim.bo[buf].buftype = "nofile"
vim.bo[buf].buftype = is_index and "acwrite" or "nofile"
vim.bo[buf].bufhidden = "wipe" vim.bo[buf].bufhidden = "wipe"
vim.bo[buf].swapfile = false vim.bo[buf].swapfile = false
if is_index then vim.bo[buf].modifiable = false
attach_index_writer(buf, worktree, assert(index_path))
else
vim.bo[buf].modifiable = false
end
vim.bo[buf].modified = false vim.bo[buf].modified = false
read_show_async(worktree, revspec, function(lines)
if not vim.api.nvim_buf_is_valid(buf) then
return
end
vim.bo[buf].modifiable = true
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines or {})
if is_index then
vim.bo[buf].buftype = "acwrite"
attach_index_writer(buf, worktree, assert(index_path))
else
vim.bo[buf].modifiable = false
end
vim.bo[buf].modified = false
end)
return buf return buf
end end
@@ -192,12 +207,14 @@ function M.split(opts)
local label = is_index and "index" or opts.ref local label = is_index and "index" or opts.ref
M.set_buf_name_and_filetype(other, "git://" .. label .. "/" .. rel) M.set_buf_name_and_filetype(other, "git://" .. label .. "/" .. rel)
local split_cmd = opts.vertical and "leftabove vertical sbuffer " local cur_win = vim.api.nvim_get_current_win()
or "leftabove sbuffer " local other_win = vim.api.nvim_open_win(other, true, {
vim.cmd(split_cmd .. other) split = opts.vertical and "left" or "above",
vim.cmd("diffthis") win = cur_win,
vim.cmd("wincmd p") })
vim.cmd("diffthis") vim.wo[other_win].diff = true
vim.api.nvim_set_current_win(cur_win)
vim.wo[cur_win].diff = true
end end
return M return M
+27
View File
@@ -25,6 +25,33 @@ function M.head(path)
return repo.head(path) return repo.head(path)
end end
---@class ow.Git.NewScratchOpts
---@field name string?
---@field bufhidden ("hide"|"wipe")? defaults to "hide"
---@field split ("above"|"below"|"left"|"right")? defaults to splitbelow-aware horizontal
---Open a split with a fresh non-modifiable scratch buffer. Default split
---direction is horizontal, honouring `splitbelow`. Caller flips
---`modifiable`, fills the buffer, and sets `filetype` once content lands.
---@param opts ow.Git.NewScratchOpts?
---@return integer buf
---@return integer win
function M.new_scratch(opts)
opts = opts or {}
local buf = vim.api.nvim_create_buf(false, true)
vim.bo[buf].buftype = "nofile"
vim.bo[buf].bufhidden = opts.bufhidden or "hide"
vim.bo[buf].swapfile = false
vim.bo[buf].modifiable = false
vim.bo[buf].modified = false
if opts.name then
pcall(vim.api.nvim_buf_set_name, buf, opts.name)
end
local split = opts.split or (vim.o.splitbelow and "below" or "above")
local win = vim.api.nvim_open_win(buf, true, { split = split })
return buf, win
end
function M.setup() function M.setup()
for name, link in pairs(HIGHLIGHTS) do for name, link in pairs(HIGHLIGHTS) do
vim.api.nvim_set_hl(0, name, { link = link, default = true }) vim.api.nvim_set_hl(0, name, { link = link, default = true })
+30 -25
View File
@@ -1,3 +1,4 @@
local git = require("git")
local log = require("log") local log = require("log")
local repo = require("git.repo") local repo = require("git.repo")
local util = require("util") local util = require("util")
@@ -13,32 +14,36 @@ function M.show()
return return
end end
local result = vim.system({ local buf = git.new_scratch()
"git",
"log",
"--graph",
"--all",
"--decorate",
"--date=short",
"--format=format:" .. LOG_FORMAT,
}, { cwd = worktree, text = true }):wait()
if result.code ~= 0 then
log.error("git log failed: %s", result.stderr or "")
return
end
local lines = util.split_lines(result.stdout or "")
vim.cmd("new")
local buf = vim.api.nvim_get_current_buf()
vim.bo[buf].buftype = "nofile"
vim.bo[buf].bufhidden = "hide"
vim.bo[buf].swapfile = false
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
vim.bo[buf].modifiable = false
vim.bo[buf].modified = false
vim.b[buf].git_worktree = worktree vim.b[buf].git_worktree = worktree
vim.bo[buf].filetype = "gitlog"
vim.system(
{
"git",
"log",
"--graph",
"--all",
"--decorate",
"--date=short",
"--format=format:" .. LOG_FORMAT,
},
{ cwd = worktree, text = true },
vim.schedule_wrap(function(result)
if not vim.api.nvim_buf_is_valid(buf) then
return
end
if result.code ~= 0 then
log.error("git log failed: %s", vim.trim(result.stderr or ""))
return
end
local lines = util.split_lines(result.stdout or "")
vim.bo[buf].modifiable = true
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
vim.bo[buf].modifiable = false
vim.bo[buf].modified = false
vim.bo[buf].filetype = "gitlog"
end)
)
end end
return M return M
+50 -50
View File
@@ -1,7 +1,9 @@
local log = require("log") local log = require("log")
local util = require("util") local util = require("util")
local UNMERGED = { local M = {}
M.UNMERGED = {
DD = true, DD = true,
AU = true, AU = true,
UD = true, UD = true,
@@ -14,7 +16,7 @@ local UNMERGED = {
---@param code string porcelain v1 XY code ---@param code string porcelain v1 XY code
---@return string? char ---@return string? char
---@return string? hl_group ---@return string? hl_group
local function indicator(code) function M.indicator(code)
if code == "" then if code == "" then
return nil return nil
end end
@@ -24,7 +26,7 @@ local function indicator(code)
if code == "!!" then if code == "!!" then
return "!", "GitIgnored" return "!", "GitIgnored"
end end
if UNMERGED[code] then if M.UNMERGED[code] then
return "U", "GitUnmerged" return "U", "GitUnmerged"
end end
local x, y = code:sub(1, 1), code:sub(2, 2) local x, y = code:sub(1, 1), code:sub(2, 2)
@@ -43,7 +45,7 @@ end
---@param code string ---@param code string
---@return string? ---@return string?
local function format(code) local function format(code)
local char, hl = indicator(code) local char, hl = M.indicator(code)
if not char then if not char then
return nil return nil
end end
@@ -53,7 +55,7 @@ end
---@param path string ---@param path string
---@return string? gitdir ---@return string? gitdir
---@return string? worktree ---@return string? worktree
local function resolve(path) function M.resolve(path)
local found = vim.fs.find(".git", { upward = true, path = path })[1] local found = vim.fs.find(".git", { upward = true, path = path })[1]
if not found then if not found then
return nil return nil
@@ -88,12 +90,12 @@ end
---both when not inside a git repo. ---both when not inside a git repo.
---@return string? gitdir ---@return string? gitdir
---@return string? worktree ---@return string? worktree
local function resolve_cwd() function M.resolve_cwd()
local path = vim.api.nvim_buf_get_name(0) local path = vim.api.nvim_buf_get_name(0)
if path == "" then if path == "" then
path = vim.fn.getcwd() path = vim.fn.getcwd()
end end
return resolve(path) return M.resolve(path)
end end
---@class ow.Git.Repo ---@class ow.Git.Repo
@@ -152,15 +154,17 @@ local function do_refresh(repo)
-- path lines, so it ignores `##` lines below. Running with `--branch` -- path lines, so it ignores `##` lines below. Running with `--branch`
-- lets the sidebar reuse this single subprocess via the GitRefresh -- lets the sidebar reuse this single subprocess via the GitRefresh
-- data payload instead of spawning its own. -- data payload instead of spawning its own.
vim.system({ vim.system(
"git", {
"-c", "git",
"core.quotePath=false", "-c",
"status", "core.quotePath=false",
"--porcelain=v1", "status",
"--branch", "--porcelain=v1",
}, { cwd = repo.worktree, text = true }, function(obj) "--branch",
vim.schedule(function() },
{ cwd = repo.worktree, text = true },
vim.schedule_wrap(function(obj)
local statuses = {} local statuses = {}
if obj.code == 0 then if obj.code == 0 then
for line in (obj.stdout or ""):gmatch("[^\r\n]+") do for line in (obj.stdout or ""):gmatch("[^\r\n]+") do
@@ -210,7 +214,7 @@ local function do_refresh(repo)
}, },
}) })
end) end)
end) )
end end
---@param gitdir string ---@param gitdir string
@@ -244,7 +248,7 @@ local function register(buf)
if path == "" then if path == "" then
return nil return nil
end end
local gitdir, worktree = resolve(path) local gitdir, worktree = M.resolve(path)
if not gitdir or not worktree then if not gitdir or not worktree then
return nil return nil
end end
@@ -259,7 +263,7 @@ local function register(buf)
end end
---@param buf integer ---@param buf integer
local function unregister(buf) function M.unregister(buf)
local repo = repo_by_buf[buf] local repo = repo_by_buf[buf]
if not repo then if not repo then
return return
@@ -273,7 +277,7 @@ local function unregister(buf)
end end
---@param buf integer ---@param buf integer
local function refresh_buf(buf) function M.refresh_buf(buf)
if not vim.api.nvim_buf_is_valid(buf) or vim.bo[buf].buftype ~= "" then if not vim.api.nvim_buf_is_valid(buf) or vim.bo[buf].buftype ~= "" then
return return
end end
@@ -285,7 +289,7 @@ local function refresh_buf(buf)
repo:refresh() repo:refresh()
end end
local function stop_all() function M.stop_all()
for _, repo in pairs(repo_by_gitdir) do for _, repo in pairs(repo_by_gitdir) do
repo:stop_watcher() repo:stop_watcher()
end end
@@ -293,8 +297,8 @@ end
---@param path string ---@param path string
---@return string? ---@return string?
local function head(path) function M.head(path)
local gitdir = resolve(path) local gitdir = M.resolve(path)
if not gitdir then if not gitdir then
return nil return nil
end end
@@ -318,39 +322,35 @@ local function head(path)
return nil return nil
end end
---Resolve a git revision to its object SHA. Returns nil if the ref can't be ---Resolve a git revision to its object SHA. Calls `callback(sha?)` on the
---resolved (root-commit's `^`, blob's `^`, malformed ref, etc.). When `short` ---main loop with nil if the ref can't be parsed (root-commit's `^`, blob's
---is true, the result is abbreviated via `core.abbrev` (auto-extended by git ---`^`, malformed ref, etc.). When `short` is true, the result is abbreviated
---to keep the prefix unique in the current repo). ---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 ref string
---@param short? boolean ---@param short boolean
---@return string? ---@param callback fun(sha: string?)
local function rev_parse(worktree, ref, short) function M.rev_parse(worktree, ref, short, callback)
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, ref)
local result = vim.system(cmd, { cwd = worktree, text = true }):wait() vim.system(
if result.code ~= 0 then cmd,
return nil { cwd = worktree, text = true },
end vim.schedule_wrap(function(result)
local sha = vim.trim(result.stdout or "") local sha
if sha == "" then if result.code == 0 then
return nil local trimmed = vim.trim(result.stdout or "")
end if trimmed ~= "" then
return sha sha = trimmed
end
end
callback(sha)
end)
)
end end
return { return M
UNMERGED = UNMERGED,
head = head,
indicator = indicator,
refresh_buf = refresh_buf,
resolve = resolve,
resolve_cwd = resolve_cwd,
rev_parse = rev_parse,
stop_all = stop_all,
unregister = unregister,
}
+66 -43
View File
@@ -1,4 +1,5 @@
local diff = require("git.diff") local diff = require("git.diff")
local git = require("git")
local log = require("log") local log = require("log")
local repo = require("git.repo") local repo = require("git.repo")
local util = require("util") local util = require("util")
@@ -111,8 +112,8 @@ end
---@param ref string ---@param ref string
local function show_blob(worktree, blob, path, ref) local function show_blob(worktree, blob, path, ref)
local buf = blob_buf(worktree, blob, path, ref) local buf = blob_buf(worktree, blob, path, ref)
vim.cmd("normal! m'") vim.cmd.normal({ "m'", bang = true })
vim.cmd("buffer " .. buf) vim.api.nvim_set_current_buf(buf)
end end
---@param ctx ow.Git.ShowContext ---@param ctx ow.Git.ShowContext
@@ -127,53 +128,75 @@ local function show_diff(ctx, section)
blob_buf(ctx.worktree, section.pre_blob, section.pre_path, parent) blob_buf(ctx.worktree, section.pre_blob, section.pre_path, parent)
local right = local right =
blob_buf(ctx.worktree, section.post_blob, section.post_path, ctx.ref) blob_buf(ctx.worktree, section.post_blob, section.post_path, ctx.ref)
vim.cmd("normal! m'") vim.cmd.normal({ "m'", bang = true })
vim.cmd("buffer " .. left) local left_win = vim.api.nvim_get_current_win()
vim.cmd("diffthis") vim.api.nvim_set_current_buf(left)
vim.cmd("rightbelow vertical sbuffer " .. right) vim.wo[left_win].diff = true
vim.cmd("diffthis") local right_win =
vim.cmd("wincmd p") vim.api.nvim_open_win(right, true, { split = "right", win = left_win })
vim.wo[right_win].diff = true
vim.api.nvim_set_current_win(left_win)
end end
---@param worktree string ---@param worktree string
---@param ref string ---@param ref string
function M.open_commit(worktree, ref) function M.open_commit(worktree, ref)
local sha = repo.rev_parse(worktree, ref, true) or ref repo.rev_parse(worktree, ref, true, function(resolved)
local name = "git://" .. sha .. "/" local sha = resolved or ref
-- Reuse a previously-opened buffer for the same commit; commit SHAs local name = "git://" .. sha .. "/"
-- are immutable so the content is stable. -- Reuse a previously-opened buffer for the same commit; commit SHAs
local existing = vim.fn.bufnr(name) -- are immutable so the content is stable.
if existing ~= -1 and vim.api.nvim_buf_is_loaded(existing) then local existing = vim.fn.bufnr(name)
vim.cmd("normal! m'") if existing ~= -1 and vim.api.nvim_buf_is_loaded(existing) then
vim.cmd("buffer " .. existing) vim.api.nvim_open_win(existing, true, {
return split = vim.o.splitbelow and "below" or "above",
end })
return
end
local result = vim.system( local buf, win = git.new_scratch({ name = name })
{ "git", "show", ref }, vim.b[buf].git_worktree = worktree
{ cwd = worktree, text = true } vim.b[buf].git_ref = sha
)
:wait() vim.system(
if result.code ~= 0 then { "git", "show", ref },
log.error("git show %s failed: %s", ref, result.stderr or "") { cwd = worktree, text = true },
return vim.schedule_wrap(function(result)
end if result.code ~= 0 then
local lines = util.split_lines(result.stdout or "") log.error(
local parent = repo.rev_parse(worktree, ref .. "^", true) "git show %s failed: %s",
local buf = vim.api.nvim_create_buf(false, true) ref,
vim.bo[buf].buftype = "nofile" vim.trim(result.stderr or "")
vim.bo[buf].bufhidden = "hide" )
vim.bo[buf].swapfile = false -- Tear down the empty placeholder window+buffer so a
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) -- retry runs a fresh fetch instead of hitting the
vim.bo[buf].modifiable = false -- cached-buffer branch and reopening an empty pane.
vim.bo[buf].modified = false if vim.api.nvim_win_is_valid(win) then
pcall(vim.api.nvim_buf_set_name, buf, name) pcall(vim.api.nvim_win_close, win, true)
vim.b[buf].git_worktree = worktree end
vim.b[buf].git_ref = sha if vim.api.nvim_buf_is_valid(buf) then
vim.b[buf].git_parent_ref = parent pcall(vim.api.nvim_buf_delete, buf, { force = true })
vim.bo[buf].filetype = "git" end
vim.cmd("normal! m'") return
vim.cmd("buffer " .. buf) end
if not vim.api.nvim_buf_is_valid(buf) then
return
end
local lines = util.split_lines(result.stdout or "")
repo.rev_parse(worktree, ref .. "^", true, function(parent)
if not vim.api.nvim_buf_is_valid(buf) then
return
end
vim.b[buf].git_parent_ref = parent
vim.bo[buf].modifiable = true
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
vim.bo[buf].modifiable = false
vim.bo[buf].modified = false
vim.bo[buf].filetype = "git"
end)
end)
)
end)
end end
---@return boolean dispatched true if the cursor was on an actionable line ---@return boolean dispatched true if the cursor was on an actionable line
+62 -62
View File
@@ -1,4 +1,5 @@
local diff = require("git.diff") local diff = require("git.diff")
local git = require("git")
local log = require("log") local log = require("log")
local repo = require("git.repo") local repo = require("git.repo")
@@ -240,13 +241,15 @@ local function enrich_with_log(worktree, branch, groups, callback)
end end
local pending = #fetches local pending = #fetches
for _, f in ipairs(fetches) do for _, f in ipairs(fetches) do
vim.system({ vim.system(
"git", {
"log", "git",
"--format=%h %s", "log",
f.range, "--format=%h %s",
}, { cwd = worktree, text = true }, function(log_obj) f.range,
vim.schedule(function() },
{ cwd = worktree, text = true },
vim.schedule_wrap(function(log_obj)
if log_obj.code == 0 then if log_obj.code == 0 then
for line in (log_obj.stdout or ""):gmatch("[^\r\n]+") do for line in (log_obj.stdout or ""):gmatch("[^\r\n]+") do
local sha, subject = line:match("^(%S+)%s+(.+)$") local sha, subject = line:match("^(%S+)%s+(.+)$")
@@ -270,7 +273,7 @@ local function enrich_with_log(worktree, branch, groups, callback)
callback(branch, groups) callback(branch, groups)
end end
end) end)
end) )
end end
end end
@@ -287,15 +290,17 @@ local function fetch_status(worktree, prefetched_stdout, callback)
enrich_with_log(worktree, branch, groups, callback) enrich_with_log(worktree, branch, groups, callback)
return return
end end
vim.system({ vim.system(
"git", {
"-c", "git",
"core.quotePath=false", "-c",
"status", "core.quotePath=false",
"--porcelain=v1", "status",
"--branch", "--porcelain=v1",
}, { cwd = worktree, text = true }, function(obj) "--branch",
vim.schedule(function() },
{ cwd = worktree, text = true },
vim.schedule_wrap(function(obj)
if obj.code ~= 0 then if obj.code ~= 0 then
log.error("git status failed: %s", vim.trim(obj.stderr or "")) log.error("git status failed: %s", vim.trim(obj.stderr or ""))
local branch = { ahead = 0, behind = 0 } local branch = { ahead = 0, behind = 0 }
@@ -313,7 +318,7 @@ local function fetch_status(worktree, prefetched_stdout, callback)
local branch, groups = parse_porcelain(obj.stdout or "") local branch, groups = parse_porcelain(obj.stdout or "")
enrich_with_log(worktree, branch, groups, callback) enrich_with_log(worktree, branch, groups, callback)
end) end)
end) )
end end
---@param bufnr integer ---@param bufnr integer
@@ -598,9 +603,7 @@ end
---@param win integer ---@param win integer
---@param enabled boolean ---@param enabled boolean
local function set_diff(win, enabled) local function set_diff(win, enabled)
vim.api.nvim_win_call(win, function() vim.wo[win].diff = enabled
vim.cmd(enabled and "diffthis" or "diffoff")
end)
if enabled then if enabled then
vim.wo[win].foldenable = true vim.wo[win].foldenable = true
vim.wo[win].foldlevel = 0 vim.wo[win].foldlevel = 0
@@ -642,6 +645,21 @@ 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` and put its buffer into the new window. Matches the
---semantics of `:set_current_win(target); :rightbelow/leftabove vertical
---split` (the new window inherits the target's buffer; caller swaps it
---afterwards via `nvim_win_set_buf`).
---@param target_win integer
---@param dir "left"|"right"
---@return integer
local function vsplit_at(target_win, dir)
return vim.api.nvim_open_win(
vim.api.nvim_win_get_buf(target_win),
true,
{ split = dir, win = target_win }
)
end
---@param s ow.Git.StatusState ---@param s ow.Git.StatusState
---@param entry ow.Git.StatusEntry ---@param entry ow.Git.StatusEntry
---@param focus_left boolean ---@param focus_left boolean
@@ -673,23 +691,17 @@ local function show_diff(s, entry, focus_left)
end end
if left_win and not right_win then if left_win and not right_win then
vim.api.nvim_set_current_win(left_win) right_win = vsplit_at(left_win, "right")
vim.cmd("rightbelow vertical split")
right_win = vim.api.nvim_get_current_win()
reset_diff_win(right_win) reset_diff_win(right_win)
elseif right_win and not left_win then elseif right_win and not left_win then
vim.api.nvim_set_current_win(right_win) left_win = vsplit_at(right_win, "left")
vim.cmd("leftabove vertical split")
left_win = vim.api.nvim_get_current_win()
reset_diff_win(left_win) reset_diff_win(left_win)
elseif not (left_win or right_win) then elseif not (left_win or right_win) then
local default_main = find_default_main_win(sidebar_win) local default_main = find_default_main_win(sidebar_win)
if default_main then if default_main then
right_win = default_main right_win = default_main
reset_diff_win(right_win) reset_diff_win(right_win)
vim.api.nvim_set_current_win(default_main) left_win = vsplit_at(right_win, "left")
vim.cmd("leftabove vertical split")
left_win = vim.api.nvim_get_current_win()
reset_diff_win(left_win) reset_diff_win(left_win)
else else
-- No reusable default-empty window. Open the diff pair by -- No reusable default-empty window. Open the diff pair by
@@ -697,12 +709,9 @@ local function show_diff(s, entry, focus_left)
-- when there are other windows to absorb the split; if the -- when there are other windows to absorb the split; if the
-- sidebar is the only window in the tab, the split has to take -- sidebar is the only window in the tab, the split has to take
-- from the sidebar itself, so restore the width explicitly. -- from the sidebar itself, so restore the width explicitly.
vim.api.nvim_set_current_win(sidebar_win) right_win = vsplit_at(sidebar_win, "right")
vim.cmd("rightbelow vertical split")
right_win = vim.api.nvim_get_current_win()
reset_diff_win(right_win) reset_diff_win(right_win)
vim.cmd("leftabove vertical split") left_win = vsplit_at(right_win, "left")
left_win = vim.api.nvim_get_current_win()
reset_diff_win(left_win) reset_diff_win(left_win)
vim.api.nvim_win_set_width(sidebar_win, SIDEBAR_WIDTH) vim.api.nvim_win_set_width(sidebar_win, SIDEBAR_WIDTH)
end end
@@ -756,13 +765,11 @@ local function action_stage()
vim.system( vim.system(
{ "git", "add", "--", entry.path }, { "git", "add", "--", entry.path },
{ cwd = s.worktree }, { cwd = s.worktree },
function(obj) vim.schedule_wrap(function(obj)
if obj.code ~= 0 then if obj.code ~= 0 then
vim.schedule(function() log.error("git add failed: %s", vim.trim(obj.stderr or ""))
log.error("git add failed: %s", vim.trim(obj.stderr or ""))
end)
end end
end end)
) )
end end
@@ -780,16 +787,18 @@ local function action_unstage()
table.insert(cmd, entry.orig) table.insert(cmd, entry.orig)
end end
table.insert(cmd, entry.path) table.insert(cmd, entry.path)
vim.system(cmd, { cwd = s.worktree }, function(obj) vim.system(
if obj.code ~= 0 then cmd,
vim.schedule(function() { cwd = s.worktree },
vim.schedule_wrap(function(obj)
if obj.code ~= 0 then
log.error( log.error(
"git restore --staged failed: %s", "git restore --staged failed: %s",
vim.trim(obj.stderr or "") vim.trim(obj.stderr or "")
) )
end) end
end end)
end) )
end end
local function action_discard() local function action_discard()
@@ -828,16 +837,14 @@ local function action_discard()
vim.system( vim.system(
{ "git", "checkout", "--", entry.path }, { "git", "checkout", "--", entry.path },
{ cwd = s.worktree }, { cwd = s.worktree },
function(obj) vim.schedule_wrap(function(obj)
if obj.code ~= 0 then if obj.code ~= 0 then
vim.schedule(function() log.error(
log.error( "git checkout failed: %s",
"git checkout failed: %s", vim.trim(obj.stderr or "")
vim.trim(obj.stderr or "") )
)
end)
end end
end end)
) )
end end
else else
@@ -875,15 +882,8 @@ local function open(worktree)
end end
local previous_win = vim.api.nvim_get_current_win() local previous_win = vim.api.nvim_get_current_win()
vim.cmd("leftabove vertical new") local bufnr, win = git.new_scratch({ split = "left", bufhidden = "wipe" })
local win = vim.api.nvim_get_current_win()
local bufnr = vim.api.nvim_get_current_buf()
vim.bo[bufnr].buftype = "nofile"
vim.bo[bufnr].bufhidden = "wipe"
vim.bo[bufnr].swapfile = false
vim.bo[bufnr].filetype = "gitstatus" vim.bo[bufnr].filetype = "gitstatus"
vim.bo[bufnr].modifiable = false
vim.wo[win].number = false vim.wo[win].number = false
vim.wo[win].relativenumber = false vim.wo[win].relativenumber = false