Skip to content

Commit 144a979

Browse files
authored
refactor(git): decouple git statuses from states, fix bubbling (#1943)
1 parent a1da8a6 commit 144a979

File tree

9 files changed

+152
-77
lines changed

9 files changed

+152
-77
lines changed

lua/neo-tree/git/init.lua

Lines changed: 71 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ local events = require("neo-tree.events")
33
local log = require("neo-tree.log")
44
local Job = require("plenary.job")
55
local uv = vim.uv or vim.loop
6+
local can_create_presized_table, new_table = pcall(require, "table.new")
67
local co = coroutine
8+
---@type metatable
9+
local weak_kv = { __mode = "kv" }
710

811
local M = {}
912
local gsplit_plain = vim.fn.has("nvim-0.9") == 1 and { plain = true } or true
@@ -23,9 +26,7 @@ local has_porcelain_v2 = git_version and git_version.major >= 2 and git_version.
2326
M._supported_porcelain_version = has_porcelain_v2 and 2 or 1
2427

2528
---@type table<string, neotree.git.Status>
26-
M.status_cache = setmetatable({}, {
27-
__mode = "kv",
28-
})
29+
M.statuses = setmetatable({}, weak_kv)
2930

3031
---@class (exact) neotree.git.Context : neotree.Config.GitStatusAsync
3132
---@field git_status neotree.git.Status?
@@ -37,7 +38,8 @@ M.status_cache = setmetatable({}, {
3738
---@param git_status neotree.git.Status?
3839
local update_git_status = function(context, git_status)
3940
context.git_status = git_status
40-
M.status_cache[context.git_root] = git_status
41+
M._root_dir_cache = setmetatable({}, { __mode = "kv" })
42+
M.statuses[context.git_root] = git_status
4143
vim.schedule(function()
4244
events.fire_event(events.GIT_STATUS_CHANGED, {
4345
git_root = context.git_root,
@@ -261,26 +263,39 @@ M._parse_porcelain = function(
261263
local typechanged = {}
262264
local renamed = {}
263265
local copied = {}
266+
local flattened_len = #statuses
264267
for i, s in ipairs(statuses) do
268+
-- simplify statuses to the highest priority ones
265269
if s:find("U", 1, true) then
270+
statuses[i] = "U"
266271
conflicts[#conflicts + 1] = i
267272
elseif s:find("?", 1, true) then
273+
statuses[i] = "?"
268274
untracked[#untracked + 1] = i
269275
elseif s:find("M", 1, true) then
276+
statuses[i] = "M"
270277
modified[#modified + 1] = i
271278
elseif s:find("A", 1, true) then
279+
statuses[i] = "A"
272280
added[#added + 1] = i
273281
elseif s:find("D", 1, true) then
282+
statuses[i] = "D"
274283
deleted[#deleted + 1] = i
275284
elseif s:find("T", 1, true) then
285+
statuses[i] = "T"
276286
typechanged[#typechanged + 1] = i
277287
elseif s:find("R", 1, true) then
288+
statuses[i] = "R"
278289
renamed[#renamed + 1] = i
279290
elseif s:find("C", 1, true) then
291+
statuses[i] = "C"
280292
copied[#copied + 1] = i
293+
else
294+
flattened_len = flattened_len - 1
281295
end
282296
end
283-
local flattened = {}
297+
local bubbleable_statuses_by_prio = can_create_presized_table and new_table(flattened_len, 0)
298+
or {}
284299

285300
for _, list in ipairs({
286301
conflicts,
@@ -292,13 +307,19 @@ M._parse_porcelain = function(
292307
renamed,
293308
copied,
294309
}) do
295-
require("neo-tree.utils._compat").table_move(list, 1, #list, #flattened + 1, flattened)
310+
require("neo-tree.utils._compat").table_move(
311+
list,
312+
1,
313+
#list,
314+
#bubbleable_statuses_by_prio + 1,
315+
bubbleable_statuses_by_prio
316+
)
296317
end
297318

319+
-- bubble them up
298320
local parent_statuses = {}
299321
do
300-
local dot_byte = ("."):byte(1, 1)
301-
for _, i in ipairs(flattened) do
322+
for _, i in ipairs(bubbleable_statuses_by_prio) do
302323
local path = paths[i]
303324
local status = statuses[i]
304325
local parent
@@ -322,8 +343,7 @@ M._parse_porcelain = function(
322343
break
323344
end
324345

325-
parent_statuses[parent] = status:byte(1, 1) == dot_byte and status:sub(1, 1)
326-
or status:sub(2, 2)
346+
parent_statuses[parent] = status
327347
path = parent
328348
until false
329349

@@ -350,7 +370,7 @@ M._parse_porcelain = function(
350370
end
351371
end
352372

353-
M.status_cache[git_root] = git_status
373+
M.statuses[git_root] = git_status
354374
return git_status
355375
end
356376

@@ -535,7 +555,7 @@ M.status_async = function(path, base, opts)
535555
batch_delay = opts.batch_delay or 10,
536556
max_lines = opts.max_lines or 100000,
537557
}
538-
if not M.status_cache[ctx.git_root] then
558+
if not M.statuses[ctx.git_root] then
539559
-- do a fast scan first to get basic things in, then a full scan with untracked files
540560
async_git_status_job(
541561
ctx,
@@ -564,33 +584,37 @@ end
564584

565585
---@param state neotree.State
566586
---@param items neotree.FileItem[]
567-
M.mark_ignored = function(state, items)
568-
local gs = state.git_status_lookup
569-
if not gs then
570-
return
587+
M.mark_gitignored = function(state, items)
588+
local statuses = {}
589+
for git_root, git_status in pairs(M.statuses) do
590+
if utils.is_subpath(git_root, state.path) or utils.is_subpath(state.path, git_root) then
591+
statuses[#statuses + 1] = git_status
592+
end
571593
end
572594
for _, i in ipairs(items) do
573-
local direct_lookup = gs[i.path]
574-
if direct_lookup == "!" then
575-
i.filtered_by = i.filtered_by or {}
576-
i.filtered_by.gitignored = true
595+
for _, git_status in ipairs(statuses) do
596+
local status = git_status[i.path]
597+
if status == "!" then
598+
i.filtered_by = i.filtered_by or {}
599+
i.filtered_by.gitignored = true
600+
break
601+
end
577602
end
578603
end
579604
end
580605

581-
local sp = utils.split_path
582606
---Invalidate cache for path and parents, updating trees as needed
583607
---@param path string
584608
local invalidate_cache = function(path)
585609
---@type string?
586-
local parent = sp(path)
610+
local parent = utils.split_path(path)
587611

588612
while parent do
589-
local cache_entry = M.status_cache[parent]
613+
local cache_entry = M.statuses[parent]
590614
if cache_entry ~= nil then
591615
update_git_status({ git_root = parent }, nil)
592616
end
593-
parent = sp(parent)
617+
parent = utils.split_path(parent)
594618
end
595619
end
596620

@@ -688,4 +712,27 @@ M.get_git_dir = function(path, callback)
688712
return git_root
689713
end
690714

715+
---@type table<string, string|false>
716+
M._root_dir_cache = setmetatable({}, weak_kv)
717+
---Given a path, find whether a git status exists for it.
718+
---@param path string
719+
---@return string|false? worktree_root
720+
---@return neotree.git.Status? status
721+
M.find_existing_status = function(path)
722+
local cached = M._root_dir_cache[path]
723+
if cached == false then
724+
return cached, nil
725+
end
726+
if cached ~= nil then
727+
return cached, M.statuses[cached]
728+
end
729+
for root_dir, status in pairs(M.statuses) do
730+
if utils.is_subpath(root_dir, path, true) then
731+
M._root_dir_cache[path] = root_dir
732+
return root_dir, status
733+
end
734+
end
735+
M._root_dir_cache[path] = false
736+
end
737+
691738
return M

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ M.setup = function(config, global_config)
143143
handler = function(state)
144144
local this_state = get_state()
145145
if state == this_state then
146-
state.git_status_lookup = git.status(state.git_base)
146+
git.status(state.git_base)
147147
end
148148
end,
149149
})

lua/neo-tree/sources/common/commands.lua

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ local log = require("neo-tree.log")
99
local help = require("neo-tree.sources.common.help")
1010
local Preview = require("neo-tree.sources.common.preview")
1111
local async = require("plenary.async")
12+
local git = require("neo-tree.git")
1213
local node_expander = require("neo-tree.sources.common.node_expander")
1314

1415
---@alias neotree.TreeCommandNormal fun(state: neotree.StateWithTree, ...: any)
@@ -333,8 +334,9 @@ M.git_toggle_file_stage = function(state)
333334
return
334335
end
335336
local path = node:get_id()
336-
local gs = log.assert(state.git_status_lookup, "No git status found for this state")
337-
local status = gs[path]
337+
local root_dir, git_status = git.find_existing_status(path)
338+
git_status = log.assert(git_status, "No git status found for this state")
339+
local status = git_status[path]
338340
if not status then
339341
log.warn("No status found for path", path)
340342
return
@@ -553,10 +555,14 @@ M.order_by_git_status = function(state)
553555
set_sort(state, "Git Status")
554556

555557
state.sort_field_provider = function(node)
556-
local git_status_lookup = state.git_status_lookup or {}
557-
local git_status = git_status_lookup[node.path]
558-
if git_status then
559-
return git_status
558+
local root_dir, git_status = git.find_existing_status(node.path)
559+
if not git_status then
560+
return ""
561+
end
562+
563+
local status = git_status[node.path]
564+
if status then
565+
return status
560566
end
561567

562568
return ""
@@ -614,9 +620,10 @@ M.show_file_details = function(state)
614620
table.insert(right, utils.date(modified_format, stat.mtime.sec))
615621
end
616622

617-
if state.git_status_lookup and state.git_status_lookup[node.path] then
623+
local root_dir, git_status = git.find_existing_status(node.path)
624+
if git_status and git_status[node.path] then
618625
table.insert(left, "Git code")
619-
table.insert(right, state.git_status_lookup[node.path])
626+
table.insert(right, git_status[node.path])
620627
end
621628

622629
local lines = {}

lua/neo-tree/sources/common/components.lua

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ local highlights = require("neo-tree.ui.highlights")
1414
local utils = require("neo-tree.utils")
1515
local file_nesting = require("neo-tree.sources.common.file-nesting")
1616
local container = require("neo-tree.sources.common.container")
17-
local nt = require("neo-tree")
17+
local git = require("neo-tree.git")
1818

1919
---@alias neotree.Component.Common._Key
2020
---|"bufnr"
@@ -233,15 +233,17 @@ end
233233

234234
---@param config neotree.Component.Common.GitStatus
235235
M.git_status = function(config, node, state)
236-
local git_status_lookup = state.git_status_lookup
237236
local node_is_dir = node.type == "directory"
238237
if node_is_dir and config.hide_when_expanded and node:is_expanded() then
239238
return {}
240239
end
241-
if not git_status_lookup then
240+
241+
local root_dir, git_status = git.find_existing_status(node.path)
242+
if not git_status then
242243
return {}
243244
end
244-
local git_status = git_status_lookup[node.path]
245+
246+
local git_status = git_status[node.path]
245247
if not git_status then
246248
return {}
247249
end

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

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -144,16 +144,17 @@ M.navigate_up = function(state)
144144
end
145145

146146
---@param state neotree.StateWithTree
147+
---@param reverse boolean?
147148
local focus_next_git_modified = function(state, reverse)
148149
local node = assert(state.tree:get_node())
149150
local current_path = node:get_id()
150-
local g = state.git_status_lookup
151-
if not utils.truthy(g) then
151+
local _, git_status = require("neo-tree.git").find_existing_status(current_path)
152+
if not utils.truthy(git_status) then
152153
return
153154
end
154-
---@cast g -nil
155+
assert(git_status)
155156
local paths = { current_path }
156-
for path, status in pairs(g) do
157+
for path, status in pairs(git_status) do
157158
if path ~= current_path and not vim.tbl_contains({ "!", "?" }, status) then
158159
--don't include files not in the current working directory
159160
if utils.is_subpath(state.path, path) then
@@ -162,18 +163,22 @@ local focus_next_git_modified = function(state, reverse)
162163
end
163164
end
164165
local sorted_paths = utils.sort_by_tree_display(paths)
165-
if reverse then
166-
sorted_paths = utils.reverse_list(sorted_paths)
167-
end
168166

167+
---@param path string
168+
---@return boolean is_file
169169
local is_file = function(path)
170170
local success, stats = pcall(uv.fs_stat, path)
171-
return (success and stats and stats.type ~= "directory")
171+
return (success and stats and stats.type ~= "directory" or false)
172172
end
173173

174174
local passed = false
175175
local target = nil
176-
for _, path in ipairs(sorted_paths) do
176+
local from, to, increment = 1, #sorted_paths, 1
177+
if reverse then
178+
from, to, increment = #sorted_paths, 1, -1
179+
end
180+
for i = from, to, increment do
181+
local path = sorted_paths[i]
177182
if target == nil and is_file(path) then
178183
target = path
179184
end

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

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -384,21 +384,23 @@ M.setup = function(config, global_config)
384384
end
385385
end,
386386
})
387-
elseif global_config.enable_git_status and global_config.git_status_async then
388-
manager.subscribe(M.name, {
389-
event = events.GIT_STATUS_CHANGED,
390-
handler = wrap(manager.git_status_changed),
391-
})
392387
elseif global_config.enable_git_status then
393-
manager.subscribe(M.name, {
394-
event = events.BEFORE_RENDER,
395-
handler = function(state)
396-
local this_state = get_state()
397-
if state == this_state then
398-
state.git_status_lookup = git.status(state.git_base, false, state.path)
399-
end
400-
end,
401-
})
388+
if global_config.git_status_async then
389+
manager.subscribe(M.name, {
390+
event = events.GIT_STATUS_CHANGED,
391+
handler = wrap(manager.git_status_changed),
392+
})
393+
else
394+
manager.subscribe(M.name, {
395+
event = events.BEFORE_RENDER,
396+
handler = function(state)
397+
local this_state = get_state()
398+
if state == this_state then
399+
git.status(state.git_base, false, state.path)
400+
end
401+
end,
402+
})
403+
end
402404
end
403405

404406
-- Respond to git events from git_status source or Fugitive

0 commit comments

Comments
 (0)