Skip to content

Commit 28c3980

Browse files
authored
fix(#2382): git watcher handles worktrees and submodules, via --absolute-git-dir when it is available (#2389)
* fix(#2382): use --absolute-git-dir when available * fix(#2382): use --absolute-git-dir when available * fix(#2382): rename private git members, destroy git watchers on purge * fix(#2382): consistent naming of toplevel * fix(#2382): more doc and safety * fix(#2382): consistent naming of toplevel * fix(#2382): consistent naming of toplevel
1 parent 0074120 commit 28c3980

File tree

5 files changed

+139
-83
lines changed

5 files changed

+139
-83
lines changed

lua/nvim-tree/actions/reloaders/reloaders.lua

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ local function refresh_nodes(node, projects, unloaded_bufnr)
1212
Iterator.builder({ node })
1313
:applier(function(n)
1414
if n.open and n.nodes then
15-
local project_root = git.get_project_root(n.cwd or n.link_to or n.absolute_path)
16-
explorer_module.reload(n, projects[project_root] or {}, unloaded_bufnr)
15+
local toplevel = git.get_toplevel(n.cwd or n.link_to or n.absolute_path)
16+
explorer_module.reload(n, projects[toplevel] or {}, unloaded_bufnr)
1717
end
1818
end)
1919
:recursor(function(n)
@@ -23,8 +23,8 @@ local function refresh_nodes(node, projects, unloaded_bufnr)
2323
end
2424

2525
function M.reload_node_status(parent_node, projects)
26-
local project_root = git.get_project_root(parent_node.absolute_path)
27-
local status = projects[project_root] or {}
26+
local toplevel = git.get_toplevel(parent_node.absolute_path)
27+
local status = projects[toplevel] or {}
2828
for _, node in ipairs(parent_node.nodes) do
2929
explorer_node.update_git_status(node, explorer_node.is_git_ignored(parent_node), status)
3030
if node.nodes and #node.nodes > 0 then

lua/nvim-tree/explorer/reload.lua

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ local function update_status(nodes_by_path, node_ignored, status)
2222
end
2323

2424
local function reload_and_get_git_project(path, callback)
25-
local project_root = git.get_project_root(path)
25+
local toplevel = git.get_toplevel(path)
2626

27-
git.reload_project(project_root, path, function()
28-
callback(project_root, git.get_project(project_root) or {})
27+
git.reload_project(toplevel, path, function()
28+
callback(toplevel, git.get_project(toplevel) or {})
2929
end)
3030
end
3131

@@ -38,7 +38,7 @@ local function update_parent_statuses(node, project, root)
3838
break
3939
end
4040

41-
root = git.get_project_root(node.parent.absolute_path)
41+
root = git.get_toplevel(node.parent.absolute_path)
4242

4343
-- stop when no more projects
4444
if not root then
@@ -174,10 +174,10 @@ function M.refresh_node(node, callback)
174174

175175
local parent_node = utils.get_parent_of_group(node)
176176

177-
reload_and_get_git_project(node.absolute_path, function(project_root, project)
177+
reload_and_get_git_project(node.absolute_path, function(toplevel, project)
178178
require("nvim-tree.explorer.reload").reload(parent_node, project)
179179

180-
update_parent_statuses(parent_node, project, project_root)
180+
update_parent_statuses(parent_node, project, toplevel)
181181

182182
callback()
183183
end)
@@ -211,11 +211,11 @@ function M.refresh_parent_nodes_for_path(path)
211211

212212
-- refresh in order; this will expand groups as needed
213213
for _, node in ipairs(parent_nodes) do
214-
local project_root = git.get_project_root(node.absolute_path)
215-
local project = git.get_project(project_root) or {}
214+
local toplevel = git.get_toplevel(node.absolute_path)
215+
local project = git.get_project(toplevel) or {}
216216

217217
M.reload(node, project)
218-
update_parent_statuses(node, project, project_root)
218+
update_parent_statuses(node, project, toplevel)
219219
end
220220

221221
log.profile_end(profile)

lua/nvim-tree/git/init.lua

Lines changed: 92 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,15 @@ local explorer_node = require "nvim-tree.explorer.node"
88

99
local M = {
1010
config = {},
11-
projects = {},
12-
cwd_to_project_root = {},
11+
12+
-- all projects keyed by toplevel
13+
_projects_by_toplevel = {},
14+
15+
-- index of paths inside toplevels, false when not inside a project
16+
_toplevels_by_path = {},
17+
18+
-- git dirs by toplevel
19+
_git_dirs_by_toplevel = {},
1320
}
1421

1522
-- Files under .git that should result in a reload when changed.
@@ -22,7 +29,7 @@ local WATCHED_FILES = {
2229
"index", -- staging area
2330
}
2431

25-
local function reload_git_status(project_root, path, project, git_status)
32+
local function reload_git_status(toplevel, path, project, git_status)
2633
if path then
2734
for p in pairs(project.files) do
2835
if p:find(path, 1, true) == 1 then
@@ -34,7 +41,7 @@ local function reload_git_status(project_root, path, project, git_status)
3441
project.files = git_status
3542
end
3643

37-
project.dirs = git_utils.file_status_to_dir_status(project.files, project_root)
44+
project.dirs = git_utils.file_status_to_dir_status(project.files, toplevel)
3845
end
3946

4047
--- Is this path in a known ignored directory?
@@ -56,85 +63,106 @@ local function path_ignored_in_project(path, project)
5663
return false
5764
end
5865

66+
--- Reload all projects
67+
--- @return table projects maybe empty
5968
function M.reload()
6069
if not M.config.git.enable then
6170
return {}
6271
end
6372

64-
for project_root in pairs(M.projects) do
65-
M.reload_project(project_root)
73+
for toplevel in pairs(M._projects_by_toplevel) do
74+
M.reload_project(toplevel)
6675
end
6776

68-
return M.projects
77+
return M._projects_by_toplevel
6978
end
7079

71-
function M.reload_project(project_root, path, callback)
72-
local project = M.projects[project_root]
73-
if not project or not M.config.git.enable then
80+
--- Reload one project. Does nothing when no project or path is ignored
81+
--- @param toplevel string|nil
82+
--- @param path string|nil optional path to update only
83+
--- @param callback function|nil
84+
function M.reload_project(toplevel, path, callback)
85+
local project = M._projects_by_toplevel[toplevel]
86+
if not toplevel or not project or not M.config.git.enable then
7487
if callback then
7588
callback()
7689
end
7790
return
7891
end
7992

80-
if path and (path:find(project_root, 1, true) ~= 1 or path_ignored_in_project(path, project)) then
93+
if path and (path:find(toplevel, 1, true) ~= 1 or path_ignored_in_project(path, project)) then
8194
if callback then
8295
callback()
8396
end
8497
return
8598
end
8699

87100
local opts = {
88-
project_root = project_root,
101+
toplevel = toplevel,
89102
path = path,
90-
list_untracked = git_utils.should_show_untracked(project_root),
103+
list_untracked = git_utils.should_show_untracked(toplevel),
91104
list_ignored = true,
92105
timeout = M.config.git.timeout,
93106
}
94107

95108
if callback then
96109
Runner.run(opts, function(git_status)
97-
reload_git_status(project_root, path, project, git_status)
110+
reload_git_status(toplevel, path, project, git_status)
98111
callback()
99112
end)
100113
else
101114
-- TODO use callback once async/await is available
102115
local git_status = Runner.run(opts)
103-
reload_git_status(project_root, path, project, git_status)
116+
reload_git_status(toplevel, path, project, git_status)
104117
end
105118
end
106119

107-
function M.get_project(project_root)
108-
return M.projects[project_root]
120+
--- Retrieve a known project
121+
--- @return table|nil project
122+
function M.get_project(toplevel)
123+
return M._projects_by_toplevel[toplevel]
109124
end
110125

111-
function M.get_project_root(cwd)
126+
--- Retrieve the toplevel for a path. nil on:
127+
--- git disabled
128+
--- not part of a project
129+
--- not a directory
130+
--- path in git.disable_for_dirs
131+
--- @param path string absolute
132+
--- @return string|nil
133+
function M.get_toplevel(path)
112134
if not M.config.git.enable then
113135
return nil
114136
end
115137

116-
if M.cwd_to_project_root[cwd] then
117-
return M.cwd_to_project_root[cwd]
138+
if M._toplevels_by_path[path] then
139+
return M._toplevels_by_path[path]
118140
end
119141

120-
if M.cwd_to_project_root[cwd] == false then
142+
if M._toplevels_by_path[path] == false then
121143
return nil
122144
end
123145

124-
local stat, _ = vim.loop.fs_stat(cwd)
146+
local stat, _ = vim.loop.fs_stat(path)
125147
if not stat or stat.type ~= "directory" then
126148
return nil
127149
end
128150

129151
-- short-circuit any known ignored paths
130-
for root, project in pairs(M.projects) do
131-
if project and path_ignored_in_project(cwd, project) then
132-
M.cwd_to_project_root[cwd] = root
152+
for root, project in pairs(M._projects_by_toplevel) do
153+
if project and path_ignored_in_project(path, project) then
154+
M._toplevels_by_path[path] = root
133155
return root
134156
end
135157
end
136158

137-
local toplevel = git_utils.get_toplevel(cwd)
159+
-- attempt to fetch toplevel
160+
local toplevel, git_dir = git_utils.get_toplevel(path)
161+
if not toplevel or not git_dir then
162+
return nil
163+
end
164+
165+
-- ignore disabled paths
138166
for _, disabled_for_dir in ipairs(M.config.git.disable_for_dirs) do
139167
local toplevel_norm = vim.fn.fnamemodify(toplevel, ":p")
140168
local disabled_norm = vim.fn.fnamemodify(disabled_for_dir, ":p")
@@ -143,23 +171,24 @@ function M.get_project_root(cwd)
143171
end
144172
end
145173

146-
M.cwd_to_project_root[cwd] = toplevel
147-
return M.cwd_to_project_root[cwd]
174+
M._toplevels_by_path[path] = toplevel
175+
M._git_dirs_by_toplevel[toplevel] = git_dir
176+
return M._toplevels_by_path[path]
148177
end
149178

150-
local function reload_tree_at(project_root)
151-
if not M.config.git.enable then
179+
local function reload_tree_at(toplevel)
180+
if not M.config.git.enable or not toplevel then
152181
return nil
153182
end
154183

155-
log.line("watcher", "git event executing '%s'", project_root)
156-
local root_node = utils.get_node_from_path(project_root)
184+
log.line("watcher", "git event executing '%s'", toplevel)
185+
local root_node = utils.get_node_from_path(toplevel)
157186
if not root_node then
158187
return
159188
end
160189

161-
M.reload_project(project_root, nil, function()
162-
local git_status = M.get_project(project_root)
190+
M.reload_project(toplevel, nil, function()
191+
local git_status = M.get_project(toplevel)
163192

164193
Iterator.builder(root_node.nodes)
165194
:hidden()
@@ -176,25 +205,29 @@ local function reload_tree_at(project_root)
176205
end)
177206
end
178207

179-
function M.load_project_status(cwd)
208+
--- Load the project status for a path. Does nothing when no toplevel for path.
209+
--- Only fetches project status when unknown, otherwise returns existing.
210+
--- @param path string absolute
211+
--- @return table project maybe empty
212+
function M.load_project_status(path)
180213
if not M.config.git.enable then
181214
return {}
182215
end
183216

184-
local project_root = M.get_project_root(cwd)
185-
if not project_root then
186-
M.cwd_to_project_root[cwd] = false
217+
local toplevel = M.get_toplevel(path)
218+
if not toplevel then
219+
M._toplevels_by_path[path] = false
187220
return {}
188221
end
189222

190-
local status = M.projects[project_root]
223+
local status = M._projects_by_toplevel[toplevel]
191224
if status then
192225
return status
193226
end
194227

195228
local git_status = Runner.run {
196-
project_root = project_root,
197-
list_untracked = git_utils.should_show_untracked(project_root),
229+
toplevel = toplevel,
230+
list_untracked = git_utils.should_show_untracked(toplevel),
198231
list_ignored = true,
199232
timeout = M.config.git.timeout,
200233
}
@@ -204,33 +237,41 @@ function M.load_project_status(cwd)
204237
log.line("watcher", "git start")
205238

206239
local callback = function(w)
207-
log.line("watcher", "git event scheduled '%s'", w.project_root)
208-
utils.debounce("git:watcher:" .. w.project_root, M.config.filesystem_watchers.debounce_delay, function()
240+
log.line("watcher", "git event scheduled '%s'", w.toplevel)
241+
utils.debounce("git:watcher:" .. w.toplevel, M.config.filesystem_watchers.debounce_delay, function()
209242
if w.destroyed then
210243
return
211244
end
212-
reload_tree_at(w.project_root)
245+
reload_tree_at(w.toplevel)
213246
end)
214247
end
215248

216-
local git_dir = vim.env.GIT_DIR or utils.path_join { project_root, ".git" }
249+
local git_dir = vim.env.GIT_DIR or M._git_dirs_by_toplevel[toplevel] or utils.path_join { toplevel, ".git" }
217250
watcher = Watcher:new(git_dir, WATCHED_FILES, callback, {
218-
project_root = project_root,
251+
toplevel = toplevel,
219252
})
220253
end
221254

222-
M.projects[project_root] = {
255+
M._projects_by_toplevel[toplevel] = {
223256
files = git_status,
224-
dirs = git_utils.file_status_to_dir_status(git_status, project_root),
257+
dirs = git_utils.file_status_to_dir_status(git_status, toplevel),
225258
watcher = watcher,
226259
}
227-
return M.projects[project_root]
260+
return M._projects_by_toplevel[toplevel]
228261
end
229262

230263
function M.purge_state()
231264
log.line("git", "purge_state")
232-
M.projects = {}
233-
M.cwd_to_project_root = {}
265+
266+
for _, project in pairs(M._projects_by_toplevel) do
267+
if project.watcher then
268+
project.watcher:destroy()
269+
end
270+
end
271+
272+
M._projects_by_toplevel = {}
273+
M._toplevels_by_path = {}
274+
M._git_dirs_by_toplevel = {}
234275
end
235276

236277
--- Disable git integration permanently

0 commit comments

Comments
 (0)