mirror of
https://github.com/SpaceVim/SpaceVim.git
synced 2025-02-04 05:50:06 +08:00
340 lines
10 KiB
Lua
340 lines
10 KiB
Lua
local utils = require("neo-tree.utils")
|
|
local events = require("neo-tree.events")
|
|
local Job = require("plenary.job")
|
|
local log = require("neo-tree.log")
|
|
local git_utils = require("neo-tree.git.utils")
|
|
|
|
local M = {}
|
|
|
|
local function get_simple_git_status_code(status)
|
|
-- Prioritze M then A over all others
|
|
if status:match("U") or status == "AA" or status == "DD" then
|
|
return "U"
|
|
elseif status:match("M") then
|
|
return "M"
|
|
elseif status:match("[ACR]") then
|
|
return "A"
|
|
elseif status:match("!$") then
|
|
return "!"
|
|
elseif status:match("?$") then
|
|
return "?"
|
|
else
|
|
local len = #status
|
|
while len > 0 do
|
|
local char = status:sub(len, len)
|
|
if char ~= " " then
|
|
return char
|
|
end
|
|
len = len - 1
|
|
end
|
|
return status
|
|
end
|
|
end
|
|
|
|
local function get_priority_git_status_code(status, other_status)
|
|
if not status then
|
|
return other_status
|
|
elseif not other_status then
|
|
return status
|
|
elseif status == "U" or other_status == "U" then
|
|
return "U"
|
|
elseif status == "?" or other_status == "?" then
|
|
return "?"
|
|
elseif status == "M" or other_status == "M" then
|
|
return "M"
|
|
elseif status == "A" or other_status == "A" then
|
|
return "A"
|
|
else
|
|
return status
|
|
end
|
|
end
|
|
|
|
local parse_git_status_line = function(context, line)
|
|
context.lines_parsed = context.lines_parsed + 1
|
|
if type(line) ~= "string" then
|
|
return
|
|
end
|
|
if #line < 4 then
|
|
return
|
|
end
|
|
local git_root = context.git_root
|
|
local git_status = context.git_status
|
|
local exclude_directories = context.exclude_directories
|
|
|
|
local line_parts = vim.split(line, " ")
|
|
if #line_parts < 2 then
|
|
return
|
|
end
|
|
local status = line_parts[1]
|
|
local relative_path = line_parts[2]
|
|
|
|
-- rename output is `R000 from/filename to/filename`
|
|
if status:match("^R") then
|
|
relative_path = line_parts[3]
|
|
end
|
|
|
|
-- remove any " due to whitespace or utf-8 in the path
|
|
relative_path = relative_path:gsub('^"', ""):gsub('"$', "")
|
|
-- convert octal encoded lines to utf-8
|
|
relative_path = git_utils.octal_to_utf8(relative_path)
|
|
|
|
if utils.is_windows == true then
|
|
relative_path = utils.windowize_path(relative_path)
|
|
end
|
|
local absolute_path = utils.path_join(git_root, relative_path)
|
|
-- merge status result if there are results from multiple passes
|
|
local existing_status = git_status[absolute_path]
|
|
if existing_status then
|
|
local merged = ""
|
|
local i = 0
|
|
while i < 2 do
|
|
i = i + 1
|
|
local existing_char = #existing_status >= i and existing_status:sub(i, i) or ""
|
|
local new_char = #status >= i and status:sub(i, i) or ""
|
|
local merged_char = get_priority_git_status_code(existing_char, new_char)
|
|
merged = merged .. merged_char
|
|
end
|
|
status = merged
|
|
end
|
|
git_status[absolute_path] = status
|
|
|
|
if not exclude_directories then
|
|
-- Now bubble this status up to the parent directories
|
|
local parts = utils.split(absolute_path, utils.path_separator)
|
|
table.remove(parts) -- pop the last part so we don't override the file's status
|
|
utils.reduce(parts, "", function(acc, part)
|
|
local path = acc .. utils.path_separator .. part
|
|
if utils.is_windows == true then
|
|
path = path:gsub("^" .. utils.path_separator, "")
|
|
end
|
|
local path_status = git_status[path]
|
|
local file_status = get_simple_git_status_code(status)
|
|
git_status[path] = get_priority_git_status_code(path_status, file_status)
|
|
return path
|
|
end)
|
|
end
|
|
end
|
|
|
|
---Parse "git status" output for the current working directory.
|
|
---@base git ref base
|
|
---@exclude_directories boolean Whether to skip bubling up status to directories
|
|
---@path string Path to run the git status command in, defaults to cwd.
|
|
---@return table table Table with the path as key and the status as value.
|
|
---@return table, string|nil The git root for the specified path.
|
|
M.status = function(base, exclude_directories, path)
|
|
local git_root = git_utils.get_repository_root(path)
|
|
if not utils.truthy(git_root) then
|
|
return {}
|
|
end
|
|
|
|
local C = git_root
|
|
local staged_cmd = { "git", "-C", C, "diff", "--staged", "--name-status", base, "--" }
|
|
local staged_ok, staged_result = utils.execute_command(staged_cmd)
|
|
if not staged_ok then
|
|
return {}
|
|
end
|
|
local unstaged_cmd = { "git", "-C", C, "diff", "--name-status" }
|
|
local unstaged_ok, unstaged_result = utils.execute_command(unstaged_cmd)
|
|
if not unstaged_ok then
|
|
return {}
|
|
end
|
|
local untracked_cmd = { "git", "-C", C, "ls-files", "--exclude-standard", "--others" }
|
|
local untracked_ok, untracked_result = utils.execute_command(untracked_cmd)
|
|
if not untracked_ok then
|
|
return {}
|
|
end
|
|
|
|
local context = {
|
|
git_root = git_root,
|
|
git_status = {},
|
|
exclude_directories = exclude_directories,
|
|
lines_parsed = 0,
|
|
}
|
|
|
|
for _, line in ipairs(staged_result) do
|
|
parse_git_status_line(context, line)
|
|
end
|
|
for _, line in ipairs(unstaged_result) do
|
|
if line then
|
|
line = " " .. line
|
|
end
|
|
parse_git_status_line(context, line)
|
|
end
|
|
for _, line in ipairs(untracked_result) do
|
|
if line then
|
|
line = "? " .. line
|
|
end
|
|
parse_git_status_line(context, line)
|
|
end
|
|
|
|
return context.git_status, git_root
|
|
end
|
|
|
|
local function parse_lines_batch(context, job_complete_callback)
|
|
local i, batch_size = 0, context.batch_size
|
|
|
|
if context.lines_total == nil then
|
|
-- first time through, get the total number of lines
|
|
context.lines_total = math.min(context.max_lines, #context.lines)
|
|
context.lines_parsed = 0
|
|
if context.lines_total == 0 then
|
|
if type(job_complete_callback) == "function" then
|
|
job_complete_callback()
|
|
end
|
|
return
|
|
end
|
|
end
|
|
batch_size = math.min(context.batch_size, context.lines_total - context.lines_parsed)
|
|
|
|
while i < batch_size do
|
|
i = i + 1
|
|
parse_git_status_line(context, context.lines[context.lines_parsed + 1])
|
|
end
|
|
|
|
if context.lines_parsed >= context.lines_total then
|
|
if type(job_complete_callback) == "function" then
|
|
job_complete_callback()
|
|
end
|
|
else
|
|
-- add small delay so other work can happen
|
|
vim.defer_fn(function()
|
|
parse_lines_batch(context, job_complete_callback)
|
|
end, context.batch_delay)
|
|
end
|
|
end
|
|
|
|
M.status_async = function(path, base, opts)
|
|
git_utils.get_repository_root(path, function(git_root)
|
|
if utils.truthy(git_root) then
|
|
log.trace("git.status.status_async called")
|
|
else
|
|
log.trace("status_async: not a git folder: ", path)
|
|
return false
|
|
end
|
|
|
|
local event_id = "git_status_" .. git_root
|
|
local context = {
|
|
git_root = git_root,
|
|
git_status = {},
|
|
exclude_directories = false,
|
|
lines = {},
|
|
lines_parsed = 0,
|
|
batch_size = opts.batch_size or 1000,
|
|
batch_delay = opts.batch_delay or 10,
|
|
max_lines = opts.max_lines or 100000,
|
|
}
|
|
|
|
local should_process = function(err, line, job, err_msg)
|
|
if vim.v.dying > 0 or vim.v.exiting ~= vim.NIL then
|
|
job:shutdown()
|
|
return false
|
|
end
|
|
if err and err > 0 then
|
|
log.error(err_msg, err, line)
|
|
return false
|
|
end
|
|
return true
|
|
end
|
|
|
|
local job_complete_callback = function()
|
|
utils.debounce(event_id, nil, nil, nil, utils.debounce_action.COMPLETE_ASYNC_JOB)
|
|
vim.schedule(function()
|
|
events.fire_event(events.GIT_STATUS_CHANGED, {
|
|
git_root = context.git_root,
|
|
git_status = context.git_status,
|
|
})
|
|
end)
|
|
end
|
|
|
|
local parse_lines = vim.schedule_wrap(function()
|
|
parse_lines_batch(context, job_complete_callback)
|
|
end)
|
|
|
|
utils.debounce(event_id, function()
|
|
local staged_job = Job:new({
|
|
command = "git",
|
|
args = { "-C", git_root, "diff", "--staged", "--name-status", base, "--" },
|
|
enable_recording = false,
|
|
maximium_results = context.max_lines,
|
|
on_stdout = vim.schedule_wrap(function(err, line, job)
|
|
if should_process(err, line, job, "status_async staged error:") then
|
|
table.insert(context.lines, line)
|
|
end
|
|
end),
|
|
on_stderr = function(err, line)
|
|
if err and err > 0 then
|
|
log.error("status_async staged error: ", err, line)
|
|
end
|
|
end,
|
|
})
|
|
|
|
local unstaged_job = Job:new({
|
|
command = "git",
|
|
args = { "-C", git_root, "diff", "--name-status" },
|
|
enable_recording = false,
|
|
maximium_results = context.max_lines,
|
|
on_stdout = vim.schedule_wrap(function(err, line, job)
|
|
if should_process(err, line, job, "status_async unstaged error:") then
|
|
if line then
|
|
line = " " .. line
|
|
end
|
|
table.insert(context.lines, line)
|
|
end
|
|
end),
|
|
on_stderr = function(err, line)
|
|
if err and err > 0 then
|
|
log.error("status_async unstaged error: ", err, line)
|
|
end
|
|
end,
|
|
})
|
|
|
|
local untracked_job = Job:new({
|
|
command = "git",
|
|
args = { "-C", git_root, "ls-files", "--exclude-standard", "--others" },
|
|
enable_recording = false,
|
|
maximium_results = context.max_lines,
|
|
on_stdout = vim.schedule_wrap(function(err, line, job)
|
|
if should_process(err, line, job, "status_async untracked error:") then
|
|
if line then
|
|
line = "? " .. line
|
|
end
|
|
table.insert(context.lines, line)
|
|
end
|
|
end),
|
|
on_stderr = function(err, line)
|
|
if err and err > 0 then
|
|
log.error("status_async untracked error: ", err, line)
|
|
end
|
|
end,
|
|
})
|
|
|
|
Job:new({
|
|
command = "git",
|
|
args = {
|
|
"-C",
|
|
git_root,
|
|
"config",
|
|
"--get",
|
|
"status.showUntrackedFiles",
|
|
},
|
|
enabled_recording = true,
|
|
on_exit = function(self, _, _)
|
|
local result = self:result()
|
|
log.debug("git status.showUntrackedFiles =", result[1])
|
|
if result[1] == "no" then
|
|
unstaged_job:after(parse_lines)
|
|
Job.chain(staged_job, unstaged_job)
|
|
else
|
|
untracked_job:after(parse_lines)
|
|
Job.chain(staged_job, unstaged_job, untracked_job)
|
|
end
|
|
end,
|
|
}):start()
|
|
end, 1000, utils.debounce_strategy.CALL_FIRST_AND_LAST, utils.debounce_action.START_ASYNC_JOB)
|
|
|
|
return true
|
|
end)
|
|
end
|
|
|
|
return M
|