fix(git): Fix the gitignore implementation. (#335)

This commit is contained in:
Sindre T. Strøm
2021-04-22 21:32:10 +02:00
committed by GitHub
parent 027e523431
commit c2d71046c6
4 changed files with 16 additions and 109 deletions

View File

@@ -1,21 +1,22 @@
local luv = vim.loop
local utils = require'nvim-tree.utils' local utils = require'nvim-tree.utils'
local config = require'nvim-tree.config'
local M = {} local M = {}
local roots = {} local roots = {}
local fstat_cache = {}
---A map from git roots to a list of ignored paths
local gitignore_map = {}
local not_git = 'not a git repo' local not_git = 'not a git repo'
local is_win = vim.api.nvim_call_function("has", {"win32"}) == 1 local is_win = vim.api.nvim_call_function("has", {"win32"}) == 1
local function update_root_status(root) local function update_root_status(root)
local untracked = ' -u' local untracked = ' -u'
if vim.fn.trim(vim.fn.system('git config --type=bool status.showUntrackedFiles')) == 'false' then if vim.fn.trim(vim.fn.system('git -C \'' .. root .. '\' config --type=bool status.showUntrackedFiles')) == 'false' then
untracked = '' untracked = ''
end end
local status = vim.fn.systemlist('cd "'..root..'" && git status --porcelain=v1'..untracked) local status = vim.fn.systemlist('git -C \'' .. root .. '\' status --porcelain=v1 --ignored=matching'..untracked)
roots[root] = {} roots[root] = {}
gitignore_map[root] = {}
for _, v in pairs(status) do for _, v in pairs(status) do
local head = v:sub(0, 2) local head = v:sub(0, 2)
@@ -30,21 +31,11 @@ local function update_root_status(root)
end end
roots[root][body] = head roots[root][body] = head
end
end
---Returns a list of all ignored files and directories in the given git directory. if head == "!!" then
---@param git_root string|nil gitignore_map[root][utils.path_remove_trailing(utils.path_join({root, body}))] = true
---@return table end
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 end
return result
end end
function M.reload_roots() function M.reload_roots()
@@ -82,7 +73,6 @@ local function create_root(cwd)
end end
update_root_status(git_root:sub(0, -2)) update_root_status(git_root:sub(0, -2))
M.update_gitignore_map()
return true return true
end end
@@ -124,13 +114,10 @@ function M.update_status(entries, cwd, parent_node)
if not parent_node then parent_node = {} end if not parent_node then parent_node = {} end
local matching_cwd = utils.path_to_matching_str( utils.path_add_trailing(git_root) ) local matching_cwd = utils.path_to_matching_str( utils.path_add_trailing(git_root) )
local num_ignored = 0
for _, node in pairs(entries) do for _, node in pairs(entries) do
if parent_node.git_status == "ignored" or M.should_gitignore(node.absolute_path) then if parent_node.git_status == "!!" then
node.git_status = "ignored" node.git_status = "!!"
num_ignored = num_ignored + 1
else else
local relpath = node.absolute_path:gsub(matching_cwd, '') local relpath = node.absolute_path:gsub(matching_cwd, '')
if node.entries ~= nil then if node.entries ~= nil then
@@ -144,7 +131,7 @@ function M.update_status(entries, cwd, parent_node)
elseif node.entries ~= nil then elseif node.entries ~= nil then
local matcher = '^'..utils.path_to_matching_str(relpath) local matcher = '^'..utils.path_to_matching_str(relpath)
for key, entry_status in pairs(git_status) do for key, entry_status in pairs(git_status) do
if key:match(matcher) then if entry_status ~= "!!" and key:match(matcher) then
node.git_status = entry_status node.git_status = entry_status
break break
end end
@@ -154,15 +141,8 @@ function M.update_status(entries, cwd, parent_node)
end end
end end
end end
if num_ignored > 0 and num_ignored == #entries then
parent_node.git_status = "ignored"
end
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. ---Check if the given path is ignored by git.
---@param path string Absolute path ---@param path string Absolute path
---@return boolean ---@return boolean
@@ -175,71 +155,4 @@ function M.should_gitignore(path)
return false return false
end 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 return M

View File

@@ -27,7 +27,6 @@ M.Tree = {
function M.init(with_open, with_reload) function M.init(with_open, with_reload)
M.Tree.cwd = luv.cwd() M.Tree.cwd = luv.cwd()
git.git_root(M.Tree.cwd) git.git_root(M.Tree.cwd)
git.update_gitignore_map_sync()
populate(M.Tree.entries, M.Tree.cwd) populate(M.Tree.entries, M.Tree.cwd)
local stat = luv.fs_stat(M.Tree.cwd) local stat = luv.fs_stat(M.Tree.cwd)
@@ -118,7 +117,6 @@ function M.unroll_dir(node)
renderer.draw(M.Tree, true) renderer.draw(M.Tree, true)
else else
git.git_root(node.absolute_path) git.git_root(node.absolute_path)
git.update_gitignore_map_sync()
populate(node.entries, node.link_to or node.absolute_path, node) populate(node.entries, node.link_to or node.absolute_path, node)
renderer.draw(M.Tree, true) renderer.draw(M.Tree, true)
@@ -129,15 +127,12 @@ function M.unroll_dir(node)
end end
end end
local function refresh_git(node, update_gitignore) local function refresh_git(node)
if not node then node = M.Tree end if not node then node = M.Tree end
if update_gitignore == nil or update_gitignore == true then
git.update_gitignore_map_sync()
end
git.update_status(node.entries, node.absolute_path or node.cwd, node) git.update_status(node.entries, node.absolute_path or node.cwd, node)
for _, entry in pairs(node.entries) do for _, entry in pairs(node.entries) do
if entry.entries and #entry.entries > 0 then if entry.entries and #entry.entries > 0 then
refresh_git(entry, false) refresh_git(entry)
end end
end end
end end

View File

@@ -249,7 +249,6 @@ function M.refresh_entries(entries, cwd, parent_node)
local n = e.fn(cwd, name) local n = e.fn(cwd, name)
if e.check(n.link_to, n.absolute_path) then if e.check(n.link_to, n.absolute_path) then
new_nodes_added = true new_nodes_added = true
git.invalidate_gitignore_map(n.absolute_path)
idx = 1 idx = 1
if prev then if prev then
idx = entries_idx[prev] + 1 idx = entries_idx[prev] + 1

View File

@@ -115,8 +115,8 @@ if vim.g.nvim_tree_git_hl == 1 then
}, },
[" A"] = { { hl = "none" } }, [" A"] = { { hl = "none" } },
["RM"] = { { hl = "NvimTreeFileRenamed" } }, ["RM"] = { { hl = "NvimTreeFileRenamed" } },
["!!"] = { { hl = "NvimTreeGitIgnored" } },
dirty = { { hl = "NvimTreeFileDirty" } }, dirty = { { hl = "NvimTreeFileDirty" } },
ignored = { { hl = "NvimTreeGitIgnored" } },
} }
get_git_hl = function(node) get_git_hl = function(node)
local git_status = node.git_status local git_status = node.git_status
@@ -164,8 +164,8 @@ if icon_state.show_git_icon then
["UU"] = { { icon = icon_state.icons.git_icons.unmerged, hl = "NvimTreeGitMerge" } }, ["UU"] = { { icon = icon_state.icons.git_icons.unmerged, hl = "NvimTreeGitMerge" } },
[" D"] = { { icon = icon_state.icons.git_icons.deleted, hl = "NvimTreeGitDeleted" } }, [" D"] = { { icon = icon_state.icons.git_icons.deleted, hl = "NvimTreeGitDeleted" } },
["D "] = { { icon = icon_state.icons.git_icons.deleted, hl = "NvimTreeGitDeleted" } }, ["D "] = { { icon = icon_state.icons.git_icons.deleted, hl = "NvimTreeGitDeleted" } },
["!!"] = { { icon = icon_state.icons.git_icons.ignored, hl = "NvimTreeGitIgnored" } },
dirty = { { icon = icon_state.icons.git_icons.unstaged, hl = "NvimTreeGitDirty" } }, dirty = { { icon = icon_state.icons.git_icons.unstaged, hl = "NvimTreeGitDirty" } },
ignored = { { icon = icon_state.icons.git_icons.ignored, hl = "NvimTreeGitIgnored" } },
} }
get_git_icons = function(node, line, depth, icon_len) get_git_icons = function(node, line, depth, icon_len)