refactor: break up git.lua

This commit is contained in:
Lewis Russell 2024-07-06 12:46:09 +01:00 committed by Lewis Russell
parent aa12bb9cd2
commit 220446c8c8
5 changed files with 315 additions and 307 deletions

View file

@ -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)

View file

@ -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<string,any>]])
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',

View file

@ -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 = '<not.committed.yet>',

72
lua/gitsigns/git/cmd.lua Normal file
View file

@ -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

207
lua/gitsigns/git/repo.lua Normal file
View file

@ -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<string,any>]])
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