nvim-tree.lua/lua/nvim-tree/git/runner.lua
Alexander Courtis 6a99f5af78
ci: lua language server and Makefile (#2546)
* ci: add lls-check

* ci: add lls-check to ci.yml

* ci: download lua-language-server binary

* ci: download lua-language-server binary

* ci: dummy failure to test

* Revert "ci: dummy failure to test"

This reverts commit 2bc43bad430209e32a5049a16c56710c4f6e2f7b.

* ci: ignore lls-out

* ci: better name

* ci: shellcheck nits

* ci: add luals libs and tidy

* ci: tidy

* ci: add neovim 0.9.4

* ci: add ci neovim 0.9.4 to lib path

* ci: dummy failure to test

* Revert "ci: dummy failure to test"

This reverts commit 45987335d81ec65fecc6636b339671a9a9fcdd97.

* Revert "ci: add ci neovim 0.9.4 to lib path"

This reverts commit 4f397d6ea8bbdf6e808f9dc9db5ecbae291d8cd4.

* Revert "ci: add neovim 0.9.4"

This reverts commit 46fd1b368d27a1892b55381691723db3b30a7527.

* ci: action downloads and installs luals

* ci: remove workspaces from luals

* ci: consistent script naming

* ci: add quality to contributing

* ci: consistent script naming

* ci: add lsp to diagnostics

* ci: temporary find to enumerate home

* ci: add VIMRUNTIME for lls

* ci: temporary find to enumerate home

* ci: temporary find to enumerate home

* ci: remove temporary find to enumerate home

* ci: correct VIMRUNTIME

* ci: add ${3rd}/luv/library

* ci: note VIMRUNTIME override

* ci: add Makefile

* ci: add Makefile

* ci: add Makefile

* ci: add Makefile

* ci: document checks and fixes

* ci: add help check

* ci: add help check

* ci: dummy help failure

* Revert "ci: dummy help failure"

This reverts commit c50cceaa4a.

* ci: document checks and fixes

* ci: document checks and fixes

* ci: matrix nvim version

* ci: matrix nvim version

* Revert "ci: matrix nvim version"

This reverts commit fcef6a11e9.

* Revert "ci: matrix nvim version"

This reverts commit a8cb50d39d.

* ci: matrix nvim version from env

* ci: matrix nvim version from env

* ci: matrix nvim version from env

* ci: matrix nvim version

* ci: matrix nvim version

* ci: matrix per job

* ci: matrix per job

* ci: many lua versions

* ci: move doc to style

* ci: tidy ci and contributing
2024-01-06 13:18:52 +11:00

236 lines
5.8 KiB
Lua

local log = require "nvim-tree.log"
local utils = require "nvim-tree.utils"
local notify = require "nvim-tree.notify"
---@class Runner
local Runner = {}
Runner.__index = Runner
local timeouts = 0
local MAX_TIMEOUTS = 5
---@private
---@param status string
---@param path string|nil
function Runner:_parse_status_output(status, path)
if not path then
return
end
-- replacing slashes if on windows
if vim.fn.has "win32" == 1 then
path = path:gsub("/", "\\")
end
if #status > 0 and #path > 0 then
self.output[utils.path_remove_trailing(utils.path_join { self.toplevel, path })] = status
end
end
---@private
---@param prev_output string
---@param incoming string
---@return string
function Runner:_handle_incoming_data(prev_output, incoming)
if incoming and utils.str_find(incoming, "\n") then
local prev = prev_output .. incoming
local i = 1
local skip_next_line = false
for line in prev:gmatch "[^\n]*\n" do
if skip_next_line then
skip_next_line = false
else
local status = line:sub(1, 2)
local path = line:sub(4, -2)
if utils.str_find(status, "R") then
-- skip next line if it is a rename entry
skip_next_line = true
end
self:_parse_status_output(status, path)
end
i = i + #line
end
return prev:sub(i, -1)
end
if incoming then
return prev_output .. incoming
end
for line in prev_output:gmatch "[^\n]*\n" do
self:_parse_status_output(line)
end
return ""
end
---@param stdout_handle uv.uv_pipe_t
---@param stderr_handle uv.uv_pipe_t
---@return table
function Runner:_getopts(stdout_handle, stderr_handle)
local untracked = self.list_untracked and "-u" or nil
local ignored = (self.list_untracked and self.list_ignored) and "--ignored=matching" or "--ignored=no"
return {
args = { "--no-optional-locks", "status", "--porcelain=v1", "-z", ignored, untracked, self.path },
cwd = self.toplevel,
stdio = { nil, stdout_handle, stderr_handle },
}
end
---@param output string
function Runner:_log_raw_output(output)
if log.enabled "git" and output and type(output) == "string" then
log.raw("git", "%s", output)
log.line("git", "done")
end
end
---@param callback function|nil
function Runner:_run_git_job(callback)
local handle, pid
local stdout = vim.loop.new_pipe(false)
local stderr = vim.loop.new_pipe(false)
local timer = vim.loop.new_timer()
if stdout == nil or stderr == nil or timer == nil then
return
end
local function on_finish(rc)
self.rc = rc or 0
if timer:is_closing() or stdout:is_closing() or stderr:is_closing() or (handle and handle:is_closing()) then
if callback then
callback()
end
return
end
timer:stop()
timer:close()
stdout:read_stop()
stderr:read_stop()
stdout:close()
stderr:close()
-- don't close the handle when killing as it will leave a zombie
if rc == -1 then
pcall(vim.loop.kill, pid, "sigkill")
elseif handle then
handle:close()
end
if callback then
callback()
end
end
local opts = self:_getopts(stdout, stderr)
log.line("git", "running job with timeout %dms", self.timeout)
log.line("git", "git %s", table.concat(utils.array_remove_nils(opts.args), " "))
handle, pid = vim.loop.spawn(
"git",
opts,
vim.schedule_wrap(function(rc)
on_finish(rc)
end)
)
timer:start(
self.timeout,
0,
vim.schedule_wrap(function()
on_finish(-1)
end)
)
local output_leftover = ""
local function manage_stdout(err, data)
if err then
return
end
if data then
data = data:gsub("%z", "\n")
end
self:_log_raw_output(data)
output_leftover = self:_handle_incoming_data(output_leftover, data)
end
local function manage_stderr(_, data)
self:_log_raw_output(data)
end
vim.loop.read_start(stdout, vim.schedule_wrap(manage_stdout))
vim.loop.read_start(stderr, vim.schedule_wrap(manage_stderr))
end
function Runner:_wait()
local function is_done()
return self.rc ~= nil
end
while not vim.wait(30, is_done) do
end
end
---@param opts table
function Runner:_finalise(opts)
if self.rc == -1 then
log.line("git", "job timed out %s %s", opts.toplevel, opts.path)
timeouts = timeouts + 1
if timeouts == MAX_TIMEOUTS then
notify.warn(string.format("%d git jobs have timed out after git.timeout %dms, disabling git integration.", timeouts, opts.timeout))
require("nvim-tree.git").disable_git_integration()
end
elseif self.rc ~= 0 then
log.line("git", "job fail rc %d %s %s", self.rc, opts.toplevel, opts.path)
else
log.line("git", "job success %s %s", opts.toplevel, opts.path)
end
end
--- Runs a git process, which will be killed if it takes more than timeout which defaults to 400ms
---@param opts table
---@param callback function|nil executed passing return when complete
---@return table|nil status by absolute path, nil if callback present
function Runner.run(opts, callback)
local self = setmetatable({
toplevel = opts.toplevel,
path = opts.path,
list_untracked = opts.list_untracked,
list_ignored = opts.list_ignored,
timeout = opts.timeout or 400,
output = {},
rc = nil, -- -1 indicates timeout
}, Runner)
local async = callback ~= nil
local profile = log.profile_start("git %s job %s %s", async and "async" or "sync", opts.toplevel, opts.path)
if async and callback then
-- async, always call back
self:_run_git_job(function()
log.profile_end(profile)
self:_finalise(opts)
callback(self.output)
end)
else
-- sync, maybe call back
self:_run_git_job()
self:_wait()
log.profile_end(profile)
self:_finalise(opts)
if callback then
callback(self.output)
else
return self.output
end
end
end
return Runner