From 3cb0f8431f56996a4af2924d78a98a09b6add095 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Mon, 1 Apr 2024 16:01:36 +0100 Subject: [PATCH] fix(version): handle version checks more gracefully - Avoid emitting errors when the version check fails. - Put version checking into separate module. - Pull in upstream changes for vim.system. Fixes: #948 Closes: #960 --- lua/gitsigns.lua | 2 +- lua/gitsigns/async.lua | 3 +- lua/gitsigns/debug/log.lua | 10 +++- lua/gitsigns/git.lua | 88 +++------------------------- lua/gitsigns/git/version.lua | 101 +++++++++++++++++++++++++++++++++ lua/gitsigns/system/compat.lua | 20 ++++--- 6 files changed, 133 insertions(+), 91 deletions(-) create mode 100644 lua/gitsigns/git/version.lua diff --git a/lua/gitsigns.lua b/lua/gitsigns.lua index 7f33239c4..9f281ad1d 100644 --- a/lua/gitsigns.lua +++ b/lua/gitsigns.lua @@ -180,7 +180,7 @@ M.setup = async.void(function(cfg) if config._test_mode then require('gitsigns.attach')._setup() - require('gitsigns.git')._set_version(config._git_version) + require('gitsigns.git.version').check() end if config.auto_attach then diff --git a/lua/gitsigns/async.lua b/lua/gitsigns/async.lua index d8dea7541..3826e8131 100644 --- a/lua/gitsigns/async.lua +++ b/lua/gitsigns/async.lua @@ -147,7 +147,8 @@ end --- @param argc number: The number of arguments of func. Must be included. --- @return function: Returns an async function function M.wrap(func, argc) - assert(argc) + assert(type(func) == 'function') + assert(type(argc) == 'number') return function(...) if not M.running() then return func(...) diff --git a/lua/gitsigns/debug/log.lua b/lua/gitsigns/debug/log.lua index d5659ee32..16b8733f1 100644 --- a/lua/gitsigns/debug/log.lua +++ b/lua/gitsigns/debug/log.lua @@ -114,7 +114,7 @@ local function eprint(msg, level) if info then msg = string.format('(ERROR) %s(%d): %s', info.short_src, info.currentline, msg) end - M.messages[#M.messages + 1] = msg + M.messages[#M.messages + 1] = debug.traceback(msg) if M.debug_mode then error(msg, 3) end @@ -128,4 +128,12 @@ function M.eprintf(fmt, ...) eprint(fmt:format(...), 1) end +function M.assert(cond, msg) + if not cond then + eprint(msg, 1) + end + + return not cond +end + return M diff --git a/lua/gitsigns/git.lua b/lua/gitsigns/git.lua index f5829a3bc..3710c5bf4 100644 --- a/lua/gitsigns/git.lua +++ b/lua/gitsigns/git.lua @@ -8,18 +8,17 @@ local system = require('gitsigns.system').system local gs_config = require('gitsigns.config') local config = gs_config.config -local uv = vim.loop -local startswith = vim.startswith +local uv = vim.uv or vim.loop -local dprint = require('gitsigns.debug.log').dprint -local dprintf = require('gitsigns.debug.log').dprintf -local eprint = require('gitsigns.debug.log').eprint -local err = require('gitsigns.message').error +local dprint = log.dprint +local dprintf = log.dprintf local error_once = require('gitsigns.message').error_once +local check_version = require('gitsigns.git.version').check + local M = {} ---- @type fun(cmd: string[], opts?: SystemOpts): vim.SystemCompleted +--- @type fun(cmd: string[], opts?: vim.SystemOpts): vim.SystemCompleted local asystem = async.wrap(system, 3) --- @param file string @@ -59,73 +58,7 @@ M.Obj = Obj local Repo = {} M.Repo = Repo ---- @class (exact) Gitsigns.Version ---- @field major integer ---- @field minor integer ---- @field patch integer - ---- @param version string ---- @return Gitsigns.Version -local function parse_version(version) - assert(version:match('%d+%.%d+%.%w+'), 'Invalid git version: ' .. version) - local ret = {} - local parts = vim.split(version, '%.') - ret.major = assert(tonumber(parts[1])) - ret.minor = assert(tonumber(parts[2])) - - if parts[3] == 'GIT' then - ret.patch = 0 - else - local patch_ver = vim.split(parts[3], '-') - ret.patch = assert(tonumber(patch_ver[1])) - end - - return ret -end - ---- Usage: check_version{2,3} ---- @param version {[1]: integer, [2]:integer, [3]:integer} ---- @return boolean -local function check_version(version) - if not M.version then - return false - end - if M.version.major < version[1] then - return false - end - if version[2] and M.version.minor < version[2] then - return false - end - if version[3] and M.version.patch < version[3] then - return false - end - return true -end - ---- @async ---- @param version string -function M._set_version(version) - if version ~= 'auto' then - M.version = parse_version(version) - return - end - - --- @type vim.SystemCompleted - local obj = asystem({ 'git', '--version' }) - - local line = vim.split(obj.stdout or '', '\n', { plain = true })[1] - if not line then - err("Unable to detect git version as 'git --version' failed to return anything") - eprint(obj.stderr) - return - end - assert(type(line) == 'string', 'Unexpected output: ' .. line) - assert(startswith(line, 'git version'), 'Unexpected output: ' .. line) - local parts = vim.split(line, '%s+') - M.version = parse_version(parts[3]) -end - ---- @class Gitsigns.Git.JobSpec : SystemOpts +--- @class Gitsigns.Git.JobSpec : vim.SystemOpts --- @field command? string --- @field ignore_error? boolean @@ -133,10 +66,6 @@ end --- @param spec? Gitsigns.Git.JobSpec --- @return string[] stdout, string? stderr local git_command = async.create(function(args, spec) - if not M.version then - M._set_version(config._git_version) - end - spec = spec or {} local cmd = { @@ -275,8 +204,7 @@ function M.get_repo_info(path, cmd, gitdir, toplevel) local has_abs_gd = check_version({ 2, 13 }) local git_dir_opt = has_abs_gd and '--absolute-git-dir' or '--git-dir' - -- Wait for internal scheduler to settle before running command - -- https://github.com/lewis6991/gitsigns.nvim/pull/215 + -- Wait for internal scheduler to settle before running command (#215) scheduler() local args = {} diff --git a/lua/gitsigns/git/version.lua b/lua/gitsigns/git/version.lua new file mode 100644 index 000000000..5360f815a --- /dev/null +++ b/lua/gitsigns/git/version.lua @@ -0,0 +1,101 @@ +local async = require('gitsigns.async') +local gs_config = require('gitsigns.config') + +local log = require('gitsigns.debug.log') +local err = require('gitsigns.message').error +local system = require('gitsigns.system').system + +local M = {} + +--- @type fun(cmd: string[], opts?: vim.SystemOpts): vim.SystemCompleted +local asystem = async.wrap(system, 3) + +--- @class (exact) Gitsigns.Version +--- @field major integer +--- @field minor integer +--- @field patch integer + +--- @param version string +--- @return Gitsigns.Version +local function parse_version(version) + assert(version:match('%d+%.%d+%.%w+'), 'Invalid git version: ' .. version) + local ret = {} + local parts = vim.split(version, '%.') + ret.major = assert(tonumber(parts[1])) + ret.minor = assert(tonumber(parts[2])) + + if parts[3] == 'GIT' then + ret.patch = 0 + else + local patch_ver = vim.split(parts[3], '-') + ret.patch = assert(tonumber(patch_ver[1])) + end + + return ret +end + +local function set_version() + local version = gs_config.config._git_version + if version ~= 'auto' then + local ok, ret = pcall(parse_version, version) + if ok then + M.version = ret + else + err(ret --[[@as string]]) + end + return + end + + --- @type vim.SystemCompleted + local obj = asystem({ 'git', '--version' }) + async.scheduler() + + local line = vim.split(obj.stdout or '', '\n')[1] + if not line then + err("Unable to detect git version as 'git --version' failed to return anything") + log.eprint(obj.stderr) + return + end + + -- Sometime 'git --version' returns an empty string (#948) + if log.assert(type(line) == 'string', 'Unexpected output: ' .. line) then + return + end + + if log.assert(vim.startswith(line, 'git version'), 'Unexpected output: ' .. line) then + return + end + + local parts = vim.split(line, '%s+') + M.version = parse_version(parts[3]) +end + +--- Usage: check_version{2,3} +--- @param version {[1]: integer, [2]:integer, [3]:integer}? +--- @return boolean +function M.check(version) + if not M.version then + set_version() + end + + if not M.version then + return false + end + + if not version then + return false + end + + if M.version.major < version[1] then + return false + end + if version[2] and M.version.minor < version[2] then + return false + end + if version[3] and M.version.patch < version[3] then + return false + end + return true +end + +return M diff --git a/lua/gitsigns/system/compat.lua b/lua/gitsigns/system/compat.lua index 411818b97..ce716680b 100644 --- a/lua/gitsigns/system/compat.lua +++ b/lua/gitsigns/system/compat.lua @@ -25,7 +25,7 @@ local function close_handles(state) close_handle(state.timer) end ---- @class Pckr.SystemObj : vim.SystemObj +--- @class Gitsigns.SystemObj : vim.SystemObj --- @field private _state vim.SystemState local SystemObj = {} @@ -59,14 +59,14 @@ function SystemObj:wait(timeout) local done = vim.wait(timeout or state.timeout or MAX_TIMEOUT, function() return state.result ~= nil - end) + end, nil, true) if not done then -- Send sigkill since this cannot be caught self:_timeout(SIG.KILL) vim.wait(timeout or state.timeout or MAX_TIMEOUT, function() return state.result ~= nil - end) + end, nil, true) end return state.result @@ -140,9 +140,13 @@ local function setup_input(input) return assert(uv.new_pipe(false)), towrite end -local environ = vim.fn.environ() -environ['NVIM'] = vim.v.servername -environ['NVIM_LISTEN_ADDRESS'] = nil +--- @return table +local function base_env() + local env = vim.fn.environ() --- @type table + env['NVIM'] = vim.v.servername + env['NVIM_LISTEN_ADDRESS'] = nil + return env +end --- uv.spawn will completely overwrite the environment --- when we just want to modify the existing one, so @@ -156,7 +160,7 @@ local function setup_env(env, clear_env) end --- @type table - env = vim.tbl_extend('force', environ, env or {}) + env = vim.tbl_extend('force', base_env(), env or {}) local renv = {} --- @type string[] for k, v in pairs(env) do @@ -261,7 +265,7 @@ end --- Run a system command --- --- @param cmd string[] ---- @param opts? SystemOpts +--- @param opts? vim.SystemOpts --- @param on_exit? fun(out: vim.SystemCompleted) --- @return vim.SystemObj local function system(cmd, opts, on_exit)