Meh I'll figure out submodules later
This commit is contained in:
parent
4ca9d44a90
commit
8cb281f436
352 changed files with 66107 additions and 0 deletions
|
|
@ -0,0 +1,183 @@
|
|||
local Job = require("plenary.job")
|
||||
local uv = vim.uv or vim.loop
|
||||
|
||||
local utils = require("neo-tree.utils")
|
||||
local log = require("neo-tree.log")
|
||||
local git_utils = require("neo-tree.git.utils")
|
||||
|
||||
local M = {}
|
||||
local sep = utils.path_separator
|
||||
|
||||
---@param ignored string[]
|
||||
---@param path string
|
||||
---@param _type neotree.Filetype
|
||||
M.is_ignored = function(ignored, path, _type)
|
||||
if _type == "directory" and not utils.is_windows then
|
||||
path = path .. sep
|
||||
end
|
||||
|
||||
return vim.tbl_contains(ignored, path)
|
||||
end
|
||||
|
||||
local git_root_cache = {
|
||||
known_roots = {},
|
||||
dir_lookup = {},
|
||||
}
|
||||
local get_root_for_item = function(item)
|
||||
local dir = item.type == "directory" and item.path or item.parent_path
|
||||
if type(git_root_cache.dir_lookup[dir]) ~= "nil" then
|
||||
return git_root_cache.dir_lookup[dir]
|
||||
end
|
||||
--for _, root in ipairs(git_root_cache.known_roots) do
|
||||
-- if vim.startswith(dir, root) then
|
||||
-- git_root_cache.dir_lookup[dir] = root
|
||||
-- return root
|
||||
-- end
|
||||
--end
|
||||
local root = git_utils.get_repository_root(dir)
|
||||
if root then
|
||||
git_root_cache.dir_lookup[dir] = root
|
||||
table.insert(git_root_cache.known_roots, root)
|
||||
else
|
||||
git_root_cache.dir_lookup[dir] = false
|
||||
end
|
||||
return root
|
||||
end
|
||||
|
||||
---@param state neotree.State
|
||||
---@param items neotree.FileItem[]
|
||||
M.mark_ignored = function(state, items, callback)
|
||||
local folders = {}
|
||||
log.trace("================================================================================")
|
||||
log.trace("IGNORED: mark_ignore BEGIN...")
|
||||
|
||||
for _, item in ipairs(items) do
|
||||
local folder = utils.split_path(item.path)
|
||||
if folder then
|
||||
if not folders[folder] then
|
||||
folders[folder] = {}
|
||||
end
|
||||
table.insert(folders[folder], item.path)
|
||||
end
|
||||
end
|
||||
|
||||
local function process_result(result)
|
||||
if utils.is_windows then
|
||||
--on Windows, git seems to return quotes and double backslash "path\\directory"
|
||||
result = vim.tbl_map(function(item)
|
||||
item = item:gsub("\\\\", "\\")
|
||||
return item
|
||||
end, result)
|
||||
else
|
||||
--check-ignore does not indicate directories the same as 'status' so we need to
|
||||
--add the trailing slash to the path manually if not on Windows.
|
||||
log.trace("IGNORED: Checking types of", #result, "items to see which ones are directories")
|
||||
for i, item in ipairs(result) do
|
||||
local stat = uv.fs_stat(item)
|
||||
if stat and stat.type == "directory" then
|
||||
result[i] = item .. sep
|
||||
end
|
||||
end
|
||||
end
|
||||
result = vim.tbl_map(function(item)
|
||||
-- remove leading and trailing " from git output
|
||||
item = item:gsub('^"', ""):gsub('"$', "")
|
||||
-- convert octal encoded lines to utf-8
|
||||
item = git_utils.octal_to_utf8(item)
|
||||
return item
|
||||
end, result)
|
||||
return result
|
||||
end
|
||||
|
||||
local function finalize(all_results)
|
||||
local show_gitignored = state.filtered_items and state.filtered_items.hide_gitignored == false
|
||||
log.trace("IGNORED: Comparing results to mark items as ignored:", show_gitignored)
|
||||
local ignored, not_ignored = 0, 0
|
||||
for _, item in ipairs(items) do
|
||||
if M.is_ignored(all_results, item.path, item.type) then
|
||||
item.filtered_by = item.filtered_by or {}
|
||||
item.filtered_by.gitignored = true
|
||||
item.filtered_by.show_gitignored = show_gitignored
|
||||
ignored = ignored + 1
|
||||
else
|
||||
not_ignored = not_ignored + 1
|
||||
end
|
||||
end
|
||||
log.trace("IGNORED: mark_ignored is complete, ignored:", ignored, ", not ignored:", not_ignored)
|
||||
log.trace("================================================================================")
|
||||
end
|
||||
|
||||
local all_results = {}
|
||||
if type(callback) == "function" then
|
||||
local jobs = {}
|
||||
local running_jobs = 0
|
||||
local job_count = 0
|
||||
local completed_jobs = 0
|
||||
|
||||
-- This is called when a job completes, and starts the next job if there are any left
|
||||
-- or calls the callback if all jobs are complete.
|
||||
-- It is also called once at the start to start the first 50 jobs.
|
||||
--
|
||||
-- This is done to avoid running too many jobs at once, which can cause a crash from
|
||||
-- having too many open files.
|
||||
local run_more_jobs = function()
|
||||
while #jobs > 0 and running_jobs < 50 and job_count > completed_jobs do
|
||||
local next_job = table.remove(jobs, #jobs)
|
||||
next_job:start()
|
||||
running_jobs = running_jobs + 1
|
||||
end
|
||||
|
||||
if completed_jobs == job_count then
|
||||
finalize(all_results)
|
||||
callback(all_results)
|
||||
end
|
||||
end
|
||||
|
||||
for folder, folder_items in pairs(folders) do
|
||||
local args = { "-C", folder, "check-ignore", "--stdin" }
|
||||
---@diagnostic disable-next-line: missing-fields
|
||||
local job = Job:new({
|
||||
command = "git",
|
||||
args = args,
|
||||
enabled_recording = true,
|
||||
writer = folder_items,
|
||||
on_start = function()
|
||||
log.trace("IGNORED: Running async git with args: ", args)
|
||||
end,
|
||||
on_exit = function(self, code, _)
|
||||
local result
|
||||
if code ~= 0 then
|
||||
log.debug("Failed to load ignored files for", folder, ":", self:stderr_result())
|
||||
result = {}
|
||||
else
|
||||
result = self:result()
|
||||
end
|
||||
vim.list_extend(all_results, process_result(result))
|
||||
|
||||
running_jobs = running_jobs - 1
|
||||
completed_jobs = completed_jobs + 1
|
||||
run_more_jobs()
|
||||
end,
|
||||
})
|
||||
table.insert(jobs, job)
|
||||
job_count = job_count + 1
|
||||
end
|
||||
|
||||
run_more_jobs()
|
||||
else
|
||||
for folder, folder_items in pairs(folders) do
|
||||
local cmd = { "git", "-C", folder, "check-ignore", unpack(folder_items) }
|
||||
log.trace("IGNORED: Running cmd: ", cmd)
|
||||
local result = vim.fn.systemlist(cmd)
|
||||
if vim.v.shell_error == 128 then
|
||||
log.debug("Failed to load ignored files for", state.path, ":", result)
|
||||
result = {}
|
||||
end
|
||||
vim.list_extend(all_results, process_result(result))
|
||||
end
|
||||
finalize(all_results)
|
||||
return all_results
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
local status = require("neo-tree.git.status")
|
||||
local ignored = require("neo-tree.git.ignored")
|
||||
local git_utils = require("neo-tree.git.utils")
|
||||
|
||||
local M = {
|
||||
get_repository_root = git_utils.get_repository_root,
|
||||
is_ignored = ignored.is_ignored,
|
||||
mark_ignored = ignored.mark_ignored,
|
||||
status = status.status,
|
||||
status_async = status.status_async,
|
||||
}
|
||||
|
||||
return M
|
||||
|
|
@ -0,0 +1,350 @@
|
|||
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
|
||||
|
||||
---@class (exact) neotree.git.Context
|
||||
---@field git_status neotree.git.Status
|
||||
---@field git_root string
|
||||
---@field exclude_directories boolean
|
||||
---@field lines_parsed integer
|
||||
|
||||
---@alias neotree.git.Status table<string, string>
|
||||
|
||||
---@param context neotree.git.Context
|
||||
local parse_git_status_line = function(context, line)
|
||||
context.lines_parsed = context.lines_parsed + 1
|
||||
if type(line) ~= "string" then
|
||||
return
|
||||
end
|
||||
if #line < 3 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 neotree.git.Status, string? git_status the neotree.Git.Status of the given root
|
||||
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
|
||||
---@type neotree.git.Context
|
||||
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()
|
||||
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()
|
||||
---@diagnostic disable-next-line: missing-fields
|
||||
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 = 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,
|
||||
})
|
||||
|
||||
---@diagnostic disable-next-line: missing-fields
|
||||
local unstaged_job = Job:new({
|
||||
command = "git",
|
||||
args = { "-C", git_root, "diff", "--name-status" },
|
||||
enable_recording = false,
|
||||
maximium_results = context.max_lines,
|
||||
on_stdout = 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,
|
||||
})
|
||||
|
||||
---@diagnostic disable-next-line: missing-fields
|
||||
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 = 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,
|
||||
})
|
||||
|
||||
---@diagnostic disable-next-line: missing-fields
|
||||
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
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
local Job = require("plenary.job")
|
||||
|
||||
local utils = require("neo-tree.utils")
|
||||
local log = require("neo-tree.log")
|
||||
|
||||
local M = {}
|
||||
|
||||
M.get_repository_root = function(path, callback)
|
||||
local args = { "rev-parse", "--show-toplevel" }
|
||||
if utils.truthy(path) then
|
||||
args = { "-C", path, "rev-parse", "--show-toplevel" }
|
||||
end
|
||||
if type(callback) == "function" then
|
||||
---@diagnostic disable-next-line: missing-fields
|
||||
Job:new({
|
||||
command = "git",
|
||||
args = args,
|
||||
enabled_recording = true,
|
||||
on_exit = function(self, code, _)
|
||||
if code ~= 0 then
|
||||
log.trace("GIT ROOT ERROR ", self:stderr_result())
|
||||
callback(nil)
|
||||
return
|
||||
end
|
||||
local git_root = self:result()[1]
|
||||
|
||||
if utils.is_windows then
|
||||
git_root = utils.windowize_path(git_root)
|
||||
end
|
||||
|
||||
log.trace("GIT ROOT for '", path, "' is '", git_root, "'")
|
||||
callback(git_root)
|
||||
end,
|
||||
}):start()
|
||||
else
|
||||
local ok, git_output = utils.execute_command({ "git", unpack(args) })
|
||||
if not ok then
|
||||
log.trace("GIT ROOT ERROR ", git_output)
|
||||
return nil
|
||||
end
|
||||
local git_root = git_output[1]
|
||||
|
||||
if utils.is_windows then
|
||||
git_root = utils.windowize_path(git_root)
|
||||
end
|
||||
|
||||
log.trace("GIT ROOT for '", path, "' is '", git_root, "'")
|
||||
return git_root
|
||||
end
|
||||
end
|
||||
|
||||
local convert_octal_char = function(octal)
|
||||
return string.char(tonumber(octal, 8))
|
||||
end
|
||||
|
||||
M.octal_to_utf8 = function(text)
|
||||
-- git uses octal encoding for utf-8 filepaths, convert octal back to utf-8
|
||||
local success, converted = pcall(string.gsub, text, "\\([0-7][0-7][0-7])", convert_octal_char)
|
||||
if success then
|
||||
return converted
|
||||
else
|
||||
return text
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
Loading…
Add table
Add a link
Reference in a new issue