Skip to content

Commit 2b43d10

Browse files
authored
feat(git): add async option for git status check, fixes #147 (#151)
1 parent c0c39e7 commit 2b43d10

File tree

10 files changed

+379
-205
lines changed

10 files changed

+379
-205
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
uses: JohnnyMorganz/[email protected]
1818
with:
1919
token: ${{ secrets.GITHUB_TOKEN }}
20-
args: --check lua/
20+
args: --glob '*.lua' --glob '!defaults.lua' --check lua
2121

2222
plenary-tests:
2323
runs-on: ubuntu-20.04

lua/neo-tree.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ local define_events = function()
6666
events.define_autocmd_event(events.VIM_WIN_ENTER, { "WinEnter" }, 0)
6767
events.define_autocmd_event(events.VIM_DIR_CHANGED, { "DirChanged" }, 200)
6868
events.define_autocmd_event(events.VIM_TAB_CLOSED, { "TabClosed" })
69-
69+
events.define_event(events.GIT_STATUS_CHANGED, { debounce_frequency = 0 })
7070
events_setup = true
7171
end
7272

lua/neo-tree/events/init.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ local M = {
1515
FILE_RENAMED = "file_renamed",
1616
FS_EVENT = "fs_event",
1717
GIT_EVENT = "git_event",
18+
GIT_STATUS_CHANGED = "git_status_changed",
1819
VIM_BUFFER_ADDED = "vim_buffer_added",
1920
VIM_BUFFER_CHANGED = "vim_buffer_changed",
2021
VIM_BUFFER_DELETED = "vim_buffer_deleted",

lua/neo-tree/git/ignored.lua

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
local utils = require("neo-tree.utils")
2+
local log = require("neo-tree.log")
3+
local git_utils = require("neo-tree.git.utils")
4+
5+
local M = {}
6+
7+
M.load_ignored_per_directory = function(path)
8+
if type(path) ~= "string" then
9+
log.error("load_ignored_per_directory: path must be a string")
10+
return {}
11+
end
12+
path = utils.path_join(path, "*")
13+
local cmd = 'git check-ignore "' .. path .. '"'
14+
local result = vim.fn.systemlist(cmd)
15+
if vim.v.shell_error == 128 then
16+
if utils.truthy(result) and vim.startswith(result[1], "fatal: not a git repository") then
17+
return {}
18+
end
19+
log.error("Failed to load ignored files for ", path, ": ", result)
20+
return {}
21+
end
22+
return result
23+
end
24+
25+
M.load_ignored = function(path)
26+
local git_root = git_utils.get_repository_root(path)
27+
if not git_root then
28+
return {}
29+
end
30+
local ok, result = utils.execute_command(
31+
"git --no-optional-locks status --porcelain=v1 --ignored=matching --untracked-files=normal"
32+
)
33+
if not ok then
34+
return {}
35+
end
36+
37+
local ignored = {}
38+
for _, v in ipairs(result) do
39+
-- git ignore format:
40+
-- !! path/to/file
41+
-- !! path/to/path/
42+
-- with paths relative to the repository root
43+
if v:sub(1, 2) == "!!" then
44+
local entry = v:sub(4)
45+
-- remove any " due to whitespace in the path
46+
entry = entry:gsub('^"', ""):gsub('$"', "")
47+
if utils.is_windows then
48+
entry = utils.windowize_path(entry)
49+
end
50+
-- use the absolute path
51+
table.insert(ignored, utils.path_join(git_root, entry))
52+
end
53+
end
54+
55+
return ignored
56+
end
57+
58+
M.is_ignored = function(ignored, path, _type)
59+
path = _type == "directory" and (path .. utils.path_separator) or path
60+
for _, v in ipairs(ignored) do
61+
if v:sub(-1) == utils.path_separator then
62+
-- directory ignore
63+
if vim.startswith(path, v) then
64+
return true
65+
end
66+
else
67+
-- file ignore
68+
if path == v then
69+
return true
70+
end
71+
end
72+
end
73+
end
74+
75+
return M

lua/neo-tree/git/init.lua

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

19614
return M

0 commit comments

Comments
 (0)