Skip to content

Commit 0a1ef65

Browse files
committed
feat: move watchers to repo objects
1 parent e9c4187 commit 0a1ef65

File tree

4 files changed

+155
-167
lines changed

4 files changed

+155
-167
lines changed

lua/gitsigns/attach.lua

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ local config = require('gitsigns.config').config
1111
local dprint = log.dprint
1212
local dprintf = log.dprintf
1313
local throttle_by_id = require('gitsigns.debounce').throttle_by_id
14+
local debounce_trailing = require('gitsigns.debounce').debounce_trailing
1415

1516
local api = vim.api
1617
local uv = vim.loop
@@ -216,6 +217,105 @@ local function get_buf_context(bufnr)
216217
}
217218
end
218219

220+
--- @param bufnr integer
221+
--- @param old_relpath string
222+
local function handle_moved(bufnr, old_relpath)
223+
local bcache = assert(cache[bufnr])
224+
local git_obj = bcache.git_obj
225+
226+
local new_name = git_obj:has_moved()
227+
if new_name then
228+
dprintf('File moved to %s', new_name)
229+
git_obj.relpath = new_name
230+
if not git_obj.orig_relpath then
231+
git_obj.orig_relpath = old_relpath
232+
end
233+
elseif git_obj.orig_relpath then
234+
local orig_file = git_obj.repo.toplevel .. util.path_sep .. git_obj.orig_relpath
235+
if not git_obj:file_info(orig_file).relpath then
236+
return
237+
end
238+
--- File was moved in the index, but then reset
239+
dprintf('Moved file reset')
240+
git_obj.relpath = git_obj.orig_relpath
241+
git_obj.orig_relpath = nil
242+
else
243+
-- File removed from index, do nothing
244+
return
245+
end
246+
247+
git_obj.file = git_obj.repo.toplevel .. util.path_sep .. git_obj.relpath
248+
bcache.file = git_obj.file
249+
git_obj:update()
250+
if not manager.schedule(bufnr) then
251+
return
252+
end
253+
254+
local bufexists = util.bufexists(bcache.file)
255+
local old_name = api.nvim_buf_get_name(bufnr)
256+
257+
if not bufexists then
258+
-- Do not trigger BufFilePre/Post
259+
-- TODO(lewis6991): figure out how to avoid reattaching without
260+
-- disabling all autocommands.
261+
util.noautocmd({ 'BufFilePre', 'BufFilePost' }, function()
262+
util.buf_rename(bufnr, bcache.file)
263+
end)
264+
end
265+
266+
local msg = bufexists and 'Cannot rename' or 'Renamed'
267+
dprintf('%s buffer %d from %s to %s', msg, bufnr, old_name, bcache.file)
268+
end
269+
270+
--- @async
271+
--- @param bufnr integer
272+
local function watcher_handler0(bufnr)
273+
local __FUNC__ = 'watcher_handler'
274+
275+
-- Avoid cache hit for detached buffer
276+
-- ref: https://github.com/lewis6991/gitsigns.nvim/issues/956
277+
if not manager.schedule(bufnr) then
278+
dprint('buffer invalid (1)')
279+
return
280+
end
281+
282+
local git_obj = cache[bufnr].git_obj
283+
284+
Status:update(bufnr, { head = git_obj.repo.abbrev_head })
285+
286+
local was_tracked = git_obj.object_name ~= nil
287+
local old_relpath = git_obj.relpath
288+
289+
git_obj:update()
290+
if not manager.schedule(bufnr) then
291+
dprint('buffer invalid (3)')
292+
return
293+
end
294+
295+
if config.watch_gitdir.follow_files and was_tracked and not git_obj.object_name then
296+
-- File was tracked but is no longer tracked. Must of been removed or
297+
-- moved. Check if it was moved and switch to it.
298+
handle_moved(bufnr, old_relpath)
299+
if not manager.schedule(bufnr) then
300+
dprint('buffer invalid (4)')
301+
return
302+
end
303+
end
304+
305+
cache[bufnr]:invalidate(true)
306+
307+
require('gitsigns.manager').update(bufnr)
308+
end
309+
310+
--- Debounce to:
311+
--- - wait for all changes to the gitdir to complete.
312+
--- Throttle to:
313+
--- - ensure handler is only triggered once per git operation.
314+
--- - prevent updates to the same buffer from interleaving as the handler is
315+
--- async.
316+
local watcher_handler =
317+
debounce_trailing(200, async.create(1, throttle_by_id(watcher_handler0, true)), 1)
318+
219319
--- Ensure attaches cannot be interleaved for the same buffer.
220320
--- Since attaches are asynchronous we need to make sure an attach isn't
221321
--- performed whilst another one is in progress.
@@ -325,8 +425,9 @@ local attach_throttled = throttle_by_id(function(cbuf, ctx, aucmd)
325425
})
326426

327427
if config.watch_gitdir.enable then
328-
local watcher = require('gitsigns.watcher')
329-
cache[cbuf].gitdir_watcher = watcher.watch_gitdir(cbuf, git_obj.repo.gitdir)
428+
cache[cbuf].deregister_watcher = git_obj.repo:register_callback(function()
429+
watcher_handler(cbuf)
430+
end)
330431
end
331432

332433
if not api.nvim_buf_is_loaded(cbuf) then

lua/gitsigns/cache.lua

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ local M = {
1717
--- @field hunks_staged? Gitsigns.Hunk.Hunk[]
1818
---
1919
--- @field staged_diffs? Gitsigns.Hunk.Hunk[]
20-
--- @field gitdir_watcher? uv.uv_fs_event_t
20+
--- @field deregister_watcher? fun()
2121
--- @field git_obj Gitsigns.GitObj
2222
--- @field blame? table<integer,Gitsigns.BlameInfo?>
2323
local CacheEntry = M.CacheEntry
@@ -127,9 +127,8 @@ function CacheEntry:get_blame(lnum, opts)
127127
end
128128

129129
function CacheEntry:destroy()
130-
local w = self.gitdir_watcher
131-
if w and not w:is_closing() then
132-
w:close()
130+
if self.deregister_watcher then
131+
self.deregister_watcher()
133132
end
134133
self.git_obj.repo:unref()
135134
end

lua/gitsigns/git/repo.lua

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ local uv = vim.uv or vim.loop
2222
--- Username configured for the repo.
2323
--- Needed for to determine "You" in current line blame.
2424
--- @field username string
25+
--- @field watcher_callbacks table<fun(), true>
2526
local M = {}
2627

2728
--- Run git command the with the objects gitdir and toplevel
@@ -96,6 +97,48 @@ function M:update_abbrev_head()
9697
self.abbrev_head = info.abbrev_head
9798
end
9899

100+
--- vim.inspect but on one line
101+
--- @param x any
102+
--- @return string
103+
local function inspect(x)
104+
return vim.inspect(x, { indent = '', newline = ' ' })
105+
end
106+
107+
function M:_watcher_cb(err, filename, events)
108+
if err then
109+
log.dprintf('Git dir update error: %s', err)
110+
return
111+
end
112+
113+
-- The luv docs say filename is passed as a string but it has been observed
114+
-- to sometimes be nil.
115+
-- https://github.com/lewis6991/gitsigns.nvim/issues/848
116+
if not filename then
117+
log.eprint('No filename')
118+
return
119+
end
120+
121+
log.dprintf("Git dir update: '%s' %s", filename, inspect(events))
122+
123+
async.run(function()
124+
self:update_abbrev_head()
125+
126+
for cb in pairs(self.watcher_callbacks) do
127+
cb()
128+
end
129+
end)
130+
end
131+
132+
--- @param cb fun()
133+
--- @return fun() deregister
134+
function M:register_callback(cb)
135+
self.watcher_callbacks[cb] = true
136+
137+
return function()
138+
self.watcher_callbacks[cb] = nil
139+
end
140+
end
141+
99142
--- @async
100143
--- @private
101144
--- @param info Gitsigns.RepoInfo
@@ -110,6 +153,12 @@ local function new(info)
110153
end
111154

112155
self.username = self:command({ 'config', 'user.name' }, { ignore_error = true })[1]
156+
self.watcher_callbacks = {}
157+
158+
local w = assert(uv.new_fs_event())
159+
w:start(self.gitdir, {}, function(err, filename, events)
160+
self:_watcher_cb(err, filename, events)
161+
end)
113162

114163
return self
115164
end

lua/gitsigns/watcher.lua

Lines changed: 0 additions & 161 deletions
This file was deleted.

0 commit comments

Comments
 (0)