Skip to content

Commit 1020de3

Browse files
authored
fix(files): fixes #78, correctly handle .gitignore (#83)
* fix(files): fixes #78, correctly handle .gitignore Plenary's scandir only checks for .gitignore files in the directories it scans. Since directories are only scanned when opened any .gitignore in any parent directory isn't applied. Implements parsing the output of `git status --ignored=matching`.
1 parent f4b5a5b commit 1020de3

File tree

8 files changed

+194
-117
lines changed

8 files changed

+194
-117
lines changed

lua/neo-tree/git/init.lua

+177
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
local Path = require("plenary.path")
2+
local utils = require("neo-tree.utils")
3+
4+
local os_sep = Path.path.sep
5+
6+
local M = {}
7+
8+
local function execute_command(cmd)
9+
local result = vim.fn.systemlist(cmd)
10+
11+
-- An empty result is ok
12+
if vim.v.shell_error ~= 0 or (#result > 0 and vim.startswith(result[1], "fatal:")) then
13+
return false, {}
14+
else
15+
return true, result
16+
end
17+
end
18+
19+
local function windowize_path(path)
20+
return path:gsub("/", "\\")
21+
end
22+
23+
M.get_repository_root = function(path)
24+
local cmd = "git rev-parse --show-toplevel"
25+
if utils.truthy(path) then
26+
cmd = "git -C " .. path .. " rev-parse --show-toplevel"
27+
end
28+
local ok, git_root = execute_command(cmd)
29+
if not ok then
30+
return nil
31+
end
32+
git_root = git_root[1]
33+
34+
if utils.is_windows then
35+
git_root = windowize_path(git_root)
36+
end
37+
38+
return git_root
39+
end
40+
41+
local function get_simple_git_status_code(status)
42+
-- Prioritze M then A over all others
43+
if status:match("U") or status == "AA" or status == "DD" then
44+
return "U"
45+
elseif status:match("M") then
46+
return "M"
47+
elseif status:match("[ACR]") then
48+
return "A"
49+
elseif status:match("!$") then
50+
return "!"
51+
elseif status:match("?$") then
52+
return "?"
53+
else
54+
local len = #status
55+
while len > 0 do
56+
local char = status:sub(len, len)
57+
if char ~= " " then
58+
return char
59+
end
60+
len = len - 1
61+
end
62+
return status
63+
end
64+
end
65+
66+
local function get_priority_git_status_code(status, other_status)
67+
if not status then
68+
return other_status
69+
elseif not other_status then
70+
return status
71+
elseif status == "U" or other_status == "U" then
72+
return "U"
73+
elseif status == "?" or other_status == "?" then
74+
return "?"
75+
elseif status == "M" or other_status == "M" then
76+
return "M"
77+
elseif status == "A" or other_status == "A" then
78+
return "A"
79+
else
80+
return status
81+
end
82+
end
83+
84+
---Parse "git status" output for the current working directory.
85+
---@return table table Table with the path as key and the status as value.
86+
M.status = function(exclude_directories)
87+
local git_root = M.get_repository_root()
88+
if not git_root then
89+
return {}
90+
end
91+
local ok, result = execute_command("git status --porcelain=v1")
92+
if not ok then
93+
return {}
94+
end
95+
96+
local git_status = {}
97+
for _, line in ipairs(result) do
98+
local status = line:sub(1, 2)
99+
local relative_path = line:sub(4)
100+
local arrow_pos = relative_path:find(" -> ")
101+
if arrow_pos ~= nil then
102+
relative_path = line:sub(arrow_pos + 5)
103+
end
104+
-- remove any " due to whitespace in the path
105+
relative_path = relative_path:gsub('^"', ""):gsub('$"', "")
106+
if utils.is_windows == true then
107+
relative_path = windowize_path(relative_path)
108+
end
109+
local absolute_path = string.format("%s%s%s", git_root, os_sep, relative_path)
110+
git_status[absolute_path] = status
111+
112+
if not exclude_directories then
113+
-- Now bubble this status up to the parent directories
114+
local file_status = get_simple_git_status_code(status)
115+
local parents = Path:new(absolute_path):parents()
116+
for i = #parents, 1, -1 do
117+
local path = parents[i]
118+
local path_status = git_status[path]
119+
git_status[path] = get_priority_git_status_code(path_status, file_status)
120+
end
121+
end
122+
end
123+
124+
return git_status, git_root
125+
end
126+
127+
M.load_ignored = function(path)
128+
local git_root = M.get_repository_root(path)
129+
if not git_root then
130+
return {}
131+
end
132+
local ok, result = execute_command(
133+
"git --no-optional-locks status --porcelain=v1 --ignored=matching --untracked-files=normal"
134+
)
135+
if not ok then
136+
return {}
137+
end
138+
139+
local ignored = {}
140+
for _, v in ipairs(result) do
141+
-- git ignore format:
142+
-- !! path/to/file
143+
-- !! path/to/path/
144+
-- with paths relative to the repository root
145+
if v:sub(1, 2) == "!!" then
146+
local entry = v:sub(4)
147+
-- remove any " due to whitespace in the path
148+
entry = entry:gsub('^"', ""):gsub('$"', "")
149+
if utils.is_windows then
150+
entry = windowize_path(entry)
151+
end
152+
-- use the absolute path
153+
table.insert(ignored, string.format("%s%s%s", git_root, os_sep, entry))
154+
end
155+
end
156+
157+
return ignored
158+
end
159+
160+
M.is_ignored = function(ignored, path, _type)
161+
path = _type == "directory" and (path .. os_sep) or path
162+
for _, v in ipairs(ignored) do
163+
if v:sub(-1) == os_sep then
164+
-- directory ignore
165+
if vim.startswith(path, v) then
166+
return true
167+
end
168+
else
169+
-- file ignore
170+
if path == v then
171+
return true
172+
end
173+
end
174+
end
175+
end
176+
177+
return M

lua/neo-tree/sources/buffers/init.lua

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ local renderer = require("neo-tree.ui.renderer")
77
local items = require("neo-tree.sources.buffers.lib.items")
88
local events = require("neo-tree.events")
99
local manager = require("neo-tree.sources.manager")
10+
local git = require("neo-tree.git")
1011

1112
local M = { name = "buffers" }
1213

@@ -75,7 +76,7 @@ M.setup = function(config, global_config)
7576
handler = function(state)
7677
local this_state = get_state()
7778
if state == this_state then
78-
state.git_status_lookup = utils.get_git_status()
79+
state.git_status_lookup = git.status()
7980
end
8081
end,
8182
})

lua/neo-tree/sources/filesystem/README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ require("neo-tree").setup({
6161
-- This function is called after the file system has been scanned,
6262
-- but before the tree is rendered. You can use this to gather extra
6363
-- data that can be used in the renderers.
64-
local utils = require("neo-tree.utils")
65-
state.git_status_lookup = utils.get_git_status()
64+
local git = require("neo-tree.git")
65+
state.git_status_lookup = git.status()
6666
end,
6767
-- The components section provides custom functions that may be called by
6868
-- the renderers below. Each componment is a function that takes the

lua/neo-tree/sources/filesystem/init.lua

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ local inputs = require("neo-tree.ui.inputs")
99
local events = require("neo-tree.events")
1010
local log = require("neo-tree.log")
1111
local manager = require("neo-tree.sources.manager")
12+
local git = require("neo-tree.git")
1213

1314
local M = { name = "filesystem" }
1415

@@ -225,7 +226,7 @@ M.setup = function(config, global_config)
225226
handler = function(state)
226227
local this_state = get_state()
227228
if state == this_state then
228-
state.git_status_lookup = utils.get_git_status()
229+
state.git_status_lookup = git.status()
229230
end
230231
end,
231232
})

lua/neo-tree/sources/filesystem/lib/fs_scan.lua

+8-4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ local filter_external = require("neo-tree.sources.filesystem.lib.filter_external
77
local file_items = require("neo-tree.sources.common.file-items")
88
local log = require("neo-tree.log")
99
local fs_watch = require("neo-tree.sources.filesystem.lib.fs_watch")
10+
local git = require("neo-tree.git")
1011

1112
local M = {}
1213

@@ -20,14 +21,15 @@ local function do_scan(context, path_to_scan)
2021

2122
scan.scan_dir_async(path_to_scan, {
2223
hidden = filters.show_hidden or false,
23-
respect_gitignore = filters.respect_gitignore or false,
2424
search_pattern = state.search_pattern or nil,
2525
add_dirs = true,
2626
depth = 1,
2727
on_insert = function(path, _type)
28-
local success, _ = pcall(file_items.create_item, context, path, _type)
29-
if not success then
30-
log.error("error creating item for ", path)
28+
if not filters.respect_gitignore or not git.is_ignored(state.git_ignored, path, _type) then
29+
local success, _ = pcall(file_items.create_item, context, path, _type)
30+
if not success then
31+
log.error("error creating item for ", path)
32+
end
3133
end
3234
end,
3335
on_exit = vim.schedule_wrap(function()
@@ -145,6 +147,8 @@ M.get_items_async = function(state, parent_id, path_to_reveal, callback)
145147
end)
146148
context.paths_to_load = utils.unique(context.paths_to_load)
147149
end
150+
151+
state.git_ignored = state.filters.respect_gitignore and git.load_ignored(state.path) or {}
148152
end
149153
do_scan(context, parent_id or state.path)
150154
end

lua/neo-tree/sources/git_status/lib/items.lua

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
local vim = vim
22
local renderer = require("neo-tree.ui.renderer")
3-
local utils = require("neo-tree.utils")
43
local file_items = require("neo-tree.sources.common.file-items")
54
local popups = require("neo-tree.ui.popups")
65
local log = require("neo-tree.log")
6+
local git = require("neo-tree.git")
77

88
local M = {}
99

@@ -14,7 +14,7 @@ M.get_git_status = function(state)
1414
return
1515
end
1616
state.loading = true
17-
local status_lookup, project_root = utils.get_git_status(true)
17+
local status_lookup, project_root = git.status(true)
1818
state.path = project_root or state.path or vim.fn.getcwd()
1919
local context = file_items.create_context(state)
2020
-- Create root folder

lua/neo-tree/ui/highlights.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ M.NORMALNC = "NeoTreeNormalNC"
2121
M.ROOT_NAME = "NeoTreeRootName"
2222
M.TITLE_BAR = "NeoTreeTitleBar"
2323

24-
function dec_to_hex(n)
24+
local function dec_to_hex(n)
2525
local hex = string.format("%06x", n)
2626
if n < 16 then
2727
hex = "0" .. hex

0 commit comments

Comments
 (0)