From 220446c8c86a280180d852efac60991eaf1a21d4 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Sat, 6 Jul 2024 12:46:09 +0100 Subject: [PATCH] refactor: break up git.lua --- lua/gitsigns.lua | 4 +- lua/gitsigns/git.lua | 310 +------------------------------------ lua/gitsigns/git/blame.lua | 29 ++++ lua/gitsigns/git/cmd.lua | 72 +++++++++ lua/gitsigns/git/repo.lua | 207 +++++++++++++++++++++++++ 5 files changed, 315 insertions(+), 307 deletions(-) create mode 100644 lua/gitsigns/git/cmd.lua create mode 100644 lua/gitsigns/git/repo.lua diff --git a/lua/gitsigns.lua b/lua/gitsigns.lua index 79dcf852e..21c82f7aa 100644 --- a/lua/gitsigns.lua +++ b/lua/gitsigns.lua @@ -29,7 +29,7 @@ local function get_gitdir_and_head() end end - local info = require('gitsigns.git').get_repo_info(cwd) + local info = require('gitsigns.git').Repo.get_info(cwd) return info.gitdir, info.abbrev_head end @@ -89,7 +89,7 @@ local update_cwd_head = async.create(function() 100, async.create(function() local git = require('gitsigns.git') - local new_head = git.get_repo_info(cwd).abbrev_head + local new_head = git.Repo.get_info(cwd).abbrev_head async.scheduler() vim.g.gitsigns_head = new_head end) diff --git a/lua/gitsigns/git.lua b/lua/gitsigns/git.lua index 7c4abbddb..2b6fd6c26 100644 --- a/lua/gitsigns/git.lua +++ b/lua/gitsigns/git.lua @@ -1,18 +1,12 @@ -local async = require('gitsigns.async') local log = require('gitsigns.debug.log') local util = require('gitsigns.util') - -local system = require('gitsigns.system').system -local scheduler = require('gitsigns.async').scheduler - -local uv = vim.uv or vim.loop +local Repo = require('gitsigns.git.repo') local check_version = require('gitsigns.git.version').check local M = {} ---- @type fun(cmd: string[], opts?: vim.SystemOpts): vim.SystemCompleted -local asystem = async.wrap(3, system) +M.Repo = Repo --- @param file string --- @return boolean @@ -41,82 +35,7 @@ local Obj = {} M.Obj = Obj ---- @class Gitsigns.RepoInfo ---- @field gitdir string ---- @field toplevel string ---- @field detached boolean ---- @field abbrev_head string - ---- @class Gitsigns.Repo : Gitsigns.RepoInfo ---- ---- Username configured for the repo. ---- Needed for to determine "You" in current line blame. ---- @field username string -local Repo = {} -M.Repo = Repo - ---- @class Gitsigns.Git.JobSpec : vim.SystemOpts ---- @field ignore_error? boolean - ---- @async ---- @param args string[] ---- @param spec? Gitsigns.Git.JobSpec ---- @return string[] stdout, string? stderr -local function git_command(args, spec) - spec = spec or {} - - local cmd = { - 'git', - '--no-pager', - '--no-optional-locks', - '--literal-pathspecs', - '-c', - 'gc.auto=0', -- Disable auto-packing which emits messages to stderr - unpack(args), - } - - if spec.text == nil then - spec.text = true - end - - -- Fix #895. Only needed for Nvim 0.9 and older - spec.clear_env = true - - --- @type vim.SystemCompleted - local obj = asystem(cmd, spec) - - if not spec.ignore_error and obj.code > 0 then - log.eprintf( - "Received exit code %d when running command\n'%s':\n%s", - obj.code, - table.concat(cmd, ' '), - obj.stderr - ) - end - - local stdout_lines = vim.split(obj.stdout or '', '\n') - - if spec.text then - -- If stdout ends with a newline, then remove the final empty string after - -- the split - if stdout_lines[#stdout_lines] == '' then - stdout_lines[#stdout_lines] = nil - end - end - - if log.verbose then - log.vprintf('%d lines:', #stdout_lines) - for i = 1, math.min(10, #stdout_lines) do - log.vprintf('\t%s', stdout_lines[i]) - end - end - - if obj.stderr == '' then - obj.stderr = nil - end - - return stdout_lines, obj.stderr -end +local git_command = require('gitsigns.git.cmd') --- @async --- @param file_cmp string @@ -142,194 +61,6 @@ function M.diff(file_cmp, file_buf, indent_heuristic, diff_algo) }) end ---- @async ---- @param gitdir? string ---- @param head_str string ---- @param cwd string ---- @return string -local function process_abbrev_head(gitdir, head_str, cwd) - if not gitdir then - return head_str - end - if head_str == 'HEAD' then - local short_sha = git_command({ 'rev-parse', '--short', 'HEAD' }, { - ignore_error = true, - cwd = cwd, - })[1] or '' - if log.debug_mode and short_sha ~= '' then - short_sha = 'HEAD' - end - if - util.path_exists(gitdir .. '/rebase-merge') - or util.path_exists(gitdir .. '/rebase-apply') - then - return short_sha .. '(rebasing)' - end - return short_sha - end - return head_str -end - -local has_cygpath = jit and jit.os == 'Windows' and vim.fn.executable('cygpath') == 1 - ---- @param path? string ---- @return string? -local function normalize_path(path) - if path and has_cygpath and not uv.fs_stat(path) then - -- If on windows and path isn't recognizable as a file, try passing it - -- through cygpath - path = asystem({ 'cygpath', '-aw', path }).stdout - end - return path -end - ---- @async ---- @param cwd string ---- @param gitdir? string ---- @param toplevel? string ---- @return Gitsigns.RepoInfo -function M.get_repo_info(cwd, gitdir, toplevel) - -- Does git rev-parse have --absolute-git-dir, added in 2.13: - -- https://public-inbox.org/git/20170203024829.8071-16-szeder.dev@gmail.com/ - local has_abs_gd = check_version({ 2, 13 }) - - -- Wait for internal scheduler to settle before running command (#215) - scheduler() - - local args = {} - - if gitdir then - vim.list_extend(args, { '--git-dir', gitdir }) - end - - if toplevel then - vim.list_extend(args, { '--work-tree', toplevel }) - end - - vim.list_extend(args, { - 'rev-parse', - '--show-toplevel', - has_abs_gd and '--absolute-git-dir' or '--git-dir', - '--abbrev-ref', - 'HEAD', - }) - - local results = git_command(args, { - ignore_error = true, - cwd = toplevel or cwd, - }) - - local toplevel_r = normalize_path(results[1]) - local gitdir_r = normalize_path(results[2]) - - if gitdir_r and not has_abs_gd then - gitdir_r = assert(uv.fs_realpath(gitdir_r)) - end - - return { - toplevel = toplevel_r, - gitdir = gitdir_r, - abbrev_head = process_abbrev_head(gitdir_r, results[3], cwd), - detached = toplevel_r and gitdir_r ~= toplevel_r .. '/.git', - } -end - --------------------------------------------------------------------------------- --- Git repo object methods --------------------------------------------------------------------------------- - ---- Run git command the with the objects gitdir and toplevel ---- @async ---- @param args string[] ---- @param spec? Gitsigns.Git.JobSpec ---- @return string[] stdout, string? stderr -function Repo:command(args, spec) - spec = spec or {} - spec.cwd = self.toplevel - - local args1 = { '--git-dir', self.gitdir } - - if self.detached then - vim.list_extend(args1, { '--work-tree', self.toplevel }) - end - - vim.list_extend(args1, args) - - return git_command(args1, spec) -end - ---- @return string[] -function Repo:files_changed() - --- @type string[] - local results = self:command({ 'status', '--porcelain', '--ignore-submodules' }) - - local ret = {} --- @type string[] - for _, line in ipairs(results) do - if line:sub(1, 2):match('^.M') then - ret[#ret + 1] = line:sub(4, -1) - end - end - return ret -end - ---- @param encoding string ---- @return boolean -local function iconv_supported(encoding) - -- TODO(lewis6991): needs https://github.com/neovim/neovim/pull/21924 - if vim.startswith(encoding, 'utf-16') then - return false - elseif vim.startswith(encoding, 'utf-32') then - return false - end - return true -end - ---- Get version of file in the index, return array lines ---- @param object string ---- @param encoding? string ---- @return string[] stdout, string? stderr -function Repo:get_show_text(object, encoding) - local stdout, stderr = self:command({ 'show', object }, { text = false, ignore_error = true }) - - if encoding and encoding ~= 'utf-8' and iconv_supported(encoding) then - for i, l in ipairs(stdout) do - stdout[i] = vim.iconv(l, encoding, 'utf-8') - end - end - - return stdout, stderr -end - ---- @async -function Repo:update_abbrev_head() - self.abbrev_head = M.get_repo_info(self.toplevel).abbrev_head -end - ---- @async ---- @param dir string ---- @param gitdir? string ---- @param toplevel? string ---- @return Gitsigns.Repo -function Repo.new(dir, gitdir, toplevel) - local self = setmetatable({}, { __index = Repo }) - - local info = M.get_repo_info(dir, gitdir, toplevel) - for k, v in - pairs(info --[[@as table]]) - do - ---@diagnostic disable-next-line:no-unknown - self[k] = v - end - - self.username = self:command({ 'config', 'user.name' }, { ignore_error = true })[1] - - return self -end - --------------------------------------------------------------------------------- --- Git object methods --------------------------------------------------------------------------------- - --- @param revision? string function Obj:update_revision(revision) self.revision = util.norm_base(revision) @@ -525,35 +256,6 @@ function Obj:unstage_file() autocmd_changed(self.file) end ---- @class Gitsigns.CommitInfo ---- @field author string ---- @field author_mail string ---- @field author_time integer ---- @field author_tz string ---- @field committer string ---- @field committer_mail string ---- @field committer_time integer ---- @field committer_tz string ---- @field summary string ---- @field sha string ---- @field abbrev_sha string ---- @field boundary? true - ---- @class Gitsigns.BlameInfoPublic: Gitsigns.BlameInfo, Gitsigns.CommitInfo ---- @field body? string[] ---- @field hunk_no? integer ---- @field num_hunks? integer ---- @field hunk? string[] ---- @field hunk_head? string - ---- @class Gitsigns.BlameInfo ---- @field orig_lnum integer ---- @field final_lnum integer ---- @field commit Gitsigns.CommitInfo ---- @field filename string ---- @field previous_filename? string ---- @field previous_sha? string - --- @param lines string[] --- @param lnum? integer --- @param revision? string @@ -585,15 +287,13 @@ end --- Stage 'lines' as the entire contents of the file --- @param lines string[] function Obj:stage_lines(lines) - local stdout = self.repo:command({ + local new_object = self.repo:command({ 'hash-object', '-w', '--path', self.relpath, '--stdin', - }, { stdin = lines }) - - local new_object = stdout[1] + }, { stdin = lines })[1] self.repo:command({ 'update-index', diff --git a/lua/gitsigns/git/blame.lua b/lua/gitsigns/git/blame.lua index 5a28e304e..b52c61895 100644 --- a/lua/gitsigns/git/blame.lua +++ b/lua/gitsigns/git/blame.lua @@ -3,6 +3,35 @@ local uv = vim.uv or vim.loop local error_once = require('gitsigns.message').error_once local dprintf = require('gitsigns.debug.log').dprintf +--- @class Gitsigns.CommitInfo +--- @field author string +--- @field author_mail string +--- @field author_time integer +--- @field author_tz string +--- @field committer string +--- @field committer_mail string +--- @field committer_time integer +--- @field committer_tz string +--- @field summary string +--- @field sha string +--- @field abbrev_sha string +--- @field boundary? true + +--- @class Gitsigns.BlameInfoPublic: Gitsigns.BlameInfo, Gitsigns.CommitInfo +--- @field body? string[] +--- @field hunk_no? integer +--- @field num_hunks? integer +--- @field hunk? string[] +--- @field hunk_head? string + +--- @class Gitsigns.BlameInfo +--- @field orig_lnum integer +--- @field final_lnum integer +--- @field commit Gitsigns.CommitInfo +--- @field filename string +--- @field previous_filename? string +--- @field previous_sha? string + local NOT_COMMITTED = { author = 'Not Committed Yet', author_mail = '', diff --git a/lua/gitsigns/git/cmd.lua b/lua/gitsigns/git/cmd.lua new file mode 100644 index 000000000..6ec0b4ae1 --- /dev/null +++ b/lua/gitsigns/git/cmd.lua @@ -0,0 +1,72 @@ +local async = require('gitsigns.async') +local log = require('gitsigns.debug.log') + +local system = require('gitsigns.system').system + +--- @type fun(cmd: string[], opts?: vim.SystemOpts): vim.SystemCompleted +local asystem = async.wrap(3, system) + +--- @class Gitsigns.Git.JobSpec : vim.SystemOpts +--- @field ignore_error? boolean + +--- @async +--- @param args string[] +--- @param spec? Gitsigns.Git.JobSpec +--- @return string[] stdout, string? stderr +local function git_command(args, spec) + spec = spec or {} + + local cmd = { + 'git', + '--no-pager', + '--no-optional-locks', + '--literal-pathspecs', + '-c', + 'gc.auto=0', -- Disable auto-packing which emits messages to stderr + unpack(args), + } + + if spec.text == nil then + spec.text = true + end + + -- Fix #895. Only needed for Nvim 0.9 and older + spec.clear_env = true + + --- @type vim.SystemCompleted + local obj = asystem(cmd, spec) + + if not spec.ignore_error and obj.code > 0 then + log.eprintf( + "Received exit code %d when running command\n'%s':\n%s", + obj.code, + table.concat(cmd, ' '), + obj.stderr + ) + end + + local stdout_lines = vim.split(obj.stdout or '', '\n') + + if spec.text then + -- If stdout ends with a newline, then remove the final empty string after + -- the split + if stdout_lines[#stdout_lines] == '' then + stdout_lines[#stdout_lines] = nil + end + end + + if log.verbose then + log.vprintf('%d lines:', #stdout_lines) + for i = 1, math.min(10, #stdout_lines) do + log.vprintf('\t%s', stdout_lines[i]) + end + end + + if obj.stderr == '' then + obj.stderr = nil + end + + return stdout_lines, obj.stderr +end + +return git_command diff --git a/lua/gitsigns/git/repo.lua b/lua/gitsigns/git/repo.lua new file mode 100644 index 000000000..8feeeb8ac --- /dev/null +++ b/lua/gitsigns/git/repo.lua @@ -0,0 +1,207 @@ +local async = require('gitsigns.async') +local git_command = require('gitsigns.git.cmd') +local log = require('gitsigns.debug.log') +local util = require('gitsigns.util') + +local system = require('gitsigns.system').system +local check_version = require('gitsigns.git.version').check + +--- @type fun(cmd: string[], opts?: vim.SystemOpts): vim.SystemCompleted +local asystem = async.wrap(3, system) + +local uv = vim.uv or vim.loop + +--- @class Gitsigns.RepoInfo +--- @field gitdir string +--- @field toplevel string +--- @field detached boolean +--- @field abbrev_head string + +--- @class Gitsigns.Repo : Gitsigns.RepoInfo +--- +--- Username configured for the repo. +--- Needed for to determine "You" in current line blame. +--- @field username string +local M = {} + +--- Run git command the with the objects gitdir and toplevel +--- @async +--- @param args string[] +--- @param spec? Gitsigns.Git.JobSpec +--- @return string[] stdout, string? stderr +function M:command(args, spec) + spec = spec or {} + spec.cwd = self.toplevel + + local args1 = { '--git-dir', self.gitdir } + + if self.detached then + vim.list_extend(args1, { '--work-tree', self.toplevel }) + end + + vim.list_extend(args1, args) + + return git_command(args1, spec) +end + +--- @return string[] +function M:files_changed() + --- @type string[] + local results = self:command({ 'status', '--porcelain', '--ignore-submodules' }) + + local ret = {} --- @type string[] + for _, line in ipairs(results) do + if line:sub(1, 2):match('^.M') then + ret[#ret + 1] = line:sub(4, -1) + end + end + return ret +end + +--- @param encoding string +--- @return boolean +local function iconv_supported(encoding) + -- TODO(lewis6991): needs https://github.com/neovim/neovim/pull/21924 + if vim.startswith(encoding, 'utf-16') then + return false + elseif vim.startswith(encoding, 'utf-32') then + return false + end + return true +end + +--- Get version of file in the index, return array lines +--- @param object string +--- @param encoding? string +--- @return string[] stdout, string? stderr +function M:get_show_text(object, encoding) + local stdout, stderr = self:command({ 'show', object }, { text = false, ignore_error = true }) + + if encoding and encoding ~= 'utf-8' and iconv_supported(encoding) then + for i, l in ipairs(stdout) do + stdout[i] = vim.iconv(l, encoding, 'utf-8') + end + end + + return stdout, stderr +end + +--- @async +function M:update_abbrev_head() + self.abbrev_head = M.get_info(self.toplevel).abbrev_head +end + +--- @async +--- @param dir string +--- @param gitdir? string +--- @param toplevel? string +--- @return Gitsigns.Repo +function M.new(dir, gitdir, toplevel) + local self = setmetatable({}, { __index = M }) + + local info = M.get_info(dir, gitdir, toplevel) + for k, v in + pairs(info --[[@as table]]) + do + ---@diagnostic disable-next-line:no-unknown + self[k] = v + end + + self.username = self:command({ 'config', 'user.name' }, { ignore_error = true })[1] + + return self +end + +local has_cygpath = jit and jit.os == 'Windows' and vim.fn.executable('cygpath') == 1 + +--- @param path? string +--- @return string? +local function normalize_path(path) + if path and has_cygpath and not uv.fs_stat(path) then + -- If on windows and path isn't recognizable as a file, try passing it + -- through cygpath + path = asystem({ 'cygpath', '-aw', path }).stdout + end + return path +end + +--- @async +--- @param gitdir? string +--- @param head_str string +--- @param cwd string +--- @return string +local function process_abbrev_head(gitdir, head_str, cwd) + if not gitdir then + return head_str + end + if head_str == 'HEAD' then + local short_sha = git_command({ 'rev-parse', '--short', 'HEAD' }, { + ignore_error = true, + cwd = cwd, + })[1] or '' + if log.debug_mode and short_sha ~= '' then + short_sha = 'HEAD' + end + if + util.path_exists(gitdir .. '/rebase-merge') + or util.path_exists(gitdir .. '/rebase-apply') + then + return short_sha .. '(rebasing)' + end + return short_sha + end + return head_str +end + +--- @async +--- @param cwd string +--- @param gitdir? string +--- @param toplevel? string +--- @return Gitsigns.RepoInfo +function M.get_info(cwd, gitdir, toplevel) + -- Does git rev-parse have --absolute-git-dir, added in 2.13: + -- https://public-inbox.org/git/20170203024829.8071-16-szeder.dev@gmail.com/ + local has_abs_gd = check_version({ 2, 13 }) + + -- Wait for internal scheduler to settle before running command (#215) + async.scheduler() + + local args = {} + + if gitdir then + vim.list_extend(args, { '--git-dir', gitdir }) + end + + if toplevel then + vim.list_extend(args, { '--work-tree', toplevel }) + end + + vim.list_extend(args, { + 'rev-parse', + '--show-toplevel', + has_abs_gd and '--absolute-git-dir' or '--git-dir', + '--abbrev-ref', + 'HEAD', + }) + + local results = git_command(args, { + ignore_error = true, + cwd = toplevel or cwd, + }) + + local toplevel_r = normalize_path(results[1]) + local gitdir_r = normalize_path(results[2]) + + if gitdir_r and not has_abs_gd then + gitdir_r = assert(uv.fs_realpath(gitdir_r)) + end + + return { + toplevel = toplevel_r, + gitdir = gitdir_r, + abbrev_head = process_abbrev_head(gitdir_r, results[3], cwd), + detached = toplevel_r and gitdir_r ~= toplevel_r .. '/.git', + } +end + +return M