246 lines
6.3 KiB
Lua
246 lines
6.3 KiB
Lua
local luv = vim.loop
|
|
local utils = require'nvim-tree.utils'
|
|
local config = require'nvim-tree.config'
|
|
local M = {}
|
|
|
|
local roots = {}
|
|
local fstat_cache = {}
|
|
|
|
local not_git = 'not a git repo'
|
|
local is_win = vim.api.nvim_call_function("has", {"win32"}) == 1
|
|
|
|
local function update_root_status(root)
|
|
local untracked = ' -u'
|
|
if vim.fn.trim(vim.fn.system('git config --type=bool status.showUntrackedFiles')) == 'false' then
|
|
untracked = ''
|
|
end
|
|
local status = vim.fn.systemlist('cd "'..root..'" && git status --porcelain=v1'..untracked)
|
|
roots[root] = {}
|
|
|
|
for _, v in pairs(status) do
|
|
local head = v:sub(0, 2)
|
|
local body = v:sub(4, -1)
|
|
if body:match('%->') ~= nil then
|
|
body = body:gsub('^.* %-> ', '')
|
|
end
|
|
|
|
--- Git returns paths with a forward slash wherever you run it, thats why i have to replace it only on windows
|
|
if is_win then
|
|
body = body:gsub("/", "\\")
|
|
end
|
|
|
|
roots[root][body] = head
|
|
end
|
|
end
|
|
|
|
---Returns a list of all ignored files and directories in the given git directory.
|
|
---@param git_root string|nil
|
|
---@return table
|
|
function M.get_gitignored(git_root)
|
|
local result = vim.fn.systemlist(
|
|
"git -C '" .. (git_root or "") .. "' ls-files --others --ignored --exclude-standard --directory"
|
|
)
|
|
if result[1] and result[1]:match("^fatal:") then
|
|
return {}
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
function M.reload_roots()
|
|
for root, status in pairs(roots) do
|
|
if status ~= not_git then
|
|
update_root_status(root)
|
|
end
|
|
end
|
|
end
|
|
|
|
local function get_git_root(path)
|
|
if roots[path] then
|
|
return path, roots[path]
|
|
end
|
|
|
|
for name, status in pairs(roots) do
|
|
if status ~= not_git then
|
|
if path:match(utils.path_to_matching_str(name)) then
|
|
return name, status
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
local function create_root(cwd)
|
|
local git_root = vim.fn.system('cd "'..cwd..'" && git rev-parse --show-toplevel')
|
|
|
|
if not git_root or #git_root == 0 or git_root:match('fatal') then
|
|
roots[cwd] = not_git
|
|
return false
|
|
end
|
|
|
|
if is_win then
|
|
git_root = git_root:gsub("/", "\\")
|
|
end
|
|
|
|
update_root_status(git_root:sub(0, -2))
|
|
M.update_gitignore_map()
|
|
return true
|
|
end
|
|
|
|
---Get the root of the git dir containing the given path or `nil` if it's not a
|
|
---git dir.
|
|
---@param path string
|
|
---@return string|nil
|
|
function M.git_root(path)
|
|
local git_root, git_status = get_git_root(path)
|
|
if not git_root then
|
|
if not create_root(path) then
|
|
return
|
|
end
|
|
git_root, git_status = get_git_root(path)
|
|
end
|
|
|
|
if git_status == not_git then
|
|
return
|
|
end
|
|
|
|
return git_root
|
|
end
|
|
|
|
function M.update_status(entries, cwd, parent_node)
|
|
local git_root, git_status = get_git_root(cwd)
|
|
if not git_root then
|
|
if not create_root(cwd) then
|
|
return
|
|
end
|
|
git_root, git_status = get_git_root(cwd)
|
|
elseif git_status == not_git then
|
|
return
|
|
end
|
|
|
|
if not git_root then
|
|
return
|
|
end
|
|
|
|
if not parent_node then parent_node = {} end
|
|
|
|
local matching_cwd = utils.path_to_matching_str( utils.path_add_trailing(git_root) )
|
|
local num_ignored = 0
|
|
|
|
for _, node in pairs(entries) do
|
|
if parent_node.git_status == "ignored" or M.should_gitignore(node.absolute_path) then
|
|
node.git_status = "ignored"
|
|
num_ignored = num_ignored + 1
|
|
|
|
else
|
|
local relpath = node.absolute_path:gsub(matching_cwd, '')
|
|
if node.entries ~= nil then
|
|
relpath = utils.path_add_trailing(relpath)
|
|
node.git_status = nil
|
|
end
|
|
|
|
local status = git_status[relpath]
|
|
if status then
|
|
node.git_status = status
|
|
elseif node.entries ~= nil then
|
|
local matcher = '^'..utils.path_to_matching_str(relpath)
|
|
for key, entry_status in pairs(git_status) do
|
|
if key:match(matcher) then
|
|
node.git_status = entry_status
|
|
break
|
|
end
|
|
end
|
|
else
|
|
node.git_status = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
if num_ignored > 0 and num_ignored == #entries then
|
|
parent_node.git_status = "ignored"
|
|
end
|
|
end
|
|
|
|
---A map from git roots to a list of ignored paths
|
|
local gitignore_map = {}
|
|
|
|
---Check if the given path is ignored by git.
|
|
---@param path string Absolute path
|
|
---@return boolean
|
|
function M.should_gitignore(path)
|
|
for _, paths in pairs(gitignore_map) do
|
|
if paths[path] == true then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
---Updates the gitignore map if it's needed. Each entry in the map is only
|
|
---updated if changes have been made to the git root's `.gitignore` or
|
|
---`.git/info/exclude` files, or it's been invalidated by the
|
|
---`invalidate_gitignore_map` function.
|
|
function M.update_gitignore_map_sync()
|
|
if not (config.get_icon_state().show_git_icon or vim.g.nvim_tree_git_hl == 1) then
|
|
return
|
|
end
|
|
|
|
local ignore_files = { ".gitignore", utils.path_join({".git", "info", "exclude"}) }
|
|
for git_root, git_status in pairs(roots) do
|
|
if git_status ~= not_git then
|
|
-- The mtime for `.gitignore` and `.git/info/exclude` is cached such that
|
|
-- the list of ignored files is only recreated when one of the said files
|
|
-- are modified.
|
|
for _, s in ipairs(ignore_files) do
|
|
local path = utils.path_join({git_root, s})
|
|
local stat = luv.fs_stat(path)
|
|
if stat and stat.mtime then
|
|
if not (fstat_cache[path]
|
|
and fstat_cache[path].mtime == stat.mtime.sec) then
|
|
|
|
gitignore_map[git_root] = {
|
|
_valid = false
|
|
}
|
|
fstat_cache[path] = {
|
|
mtime = stat.mtime.sec
|
|
}
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
for git_root, paths in pairs(gitignore_map) do
|
|
if not paths._valid then
|
|
gitignore_map[git_root] = {
|
|
_valid = true
|
|
}
|
|
paths = gitignore_map[git_root]
|
|
|
|
for _, s in ipairs(M.get_gitignored(git_root)) do
|
|
if is_win then s = s:gsub("/", "\\") end
|
|
s = utils.path_remove_trailing(s)
|
|
paths[utils.path_join({git_root, s})] = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
---Updates the gitignore map asynchronously if it's needed.
|
|
function M.update_gitignore_map()
|
|
vim.schedule(function()
|
|
M.update_gitignore_map_sync()
|
|
end)
|
|
end
|
|
|
|
---Force the ignore list of this path's git root to be recreated on the next
|
|
---call to `update_gitignore_map`.
|
|
---@param path string Absolute path
|
|
function M.invalidate_gitignore_map(path)
|
|
local git_root = get_git_root(path)
|
|
if git_root and gitignore_map[git_root] then
|
|
gitignore_map[git_root]._valid = false
|
|
end
|
|
end
|
|
|
|
return M
|