diff --git a/lua/gitsigns/attach.lua b/lua/gitsigns/attach.lua index 60b33687f..7829f952c 100644 --- a/lua/gitsigns/attach.lua +++ b/lua/gitsigns/attach.lua @@ -11,6 +11,7 @@ local config = require('gitsigns.config').config local dprint = log.dprint local dprintf = log.dprintf local throttle_by_id = require('gitsigns.debounce').throttle_by_id +local debounce_trailing = require('gitsigns.debounce').debounce_trailing local api = vim.api local uv = vim.loop @@ -216,6 +217,105 @@ local function get_buf_context(bufnr) } end +--- @param bufnr integer +--- @param old_relpath string +local function handle_moved(bufnr, old_relpath) + local bcache = assert(cache[bufnr]) + local git_obj = bcache.git_obj + + local new_name = git_obj:has_moved() + if new_name then + dprintf('File moved to %s', new_name) + git_obj.relpath = new_name + if not git_obj.orig_relpath then + git_obj.orig_relpath = old_relpath + end + elseif git_obj.orig_relpath then + local orig_file = git_obj.repo.toplevel .. util.path_sep .. git_obj.orig_relpath + if not git_obj:file_info(orig_file).relpath then + return + end + --- File was moved in the index, but then reset + dprintf('Moved file reset') + git_obj.relpath = git_obj.orig_relpath + git_obj.orig_relpath = nil + else + -- File removed from index, do nothing + return + end + + git_obj.file = git_obj.repo.toplevel .. util.path_sep .. git_obj.relpath + bcache.file = git_obj.file + git_obj:update() + if not manager.schedule(bufnr) then + return + end + + local bufexists = util.bufexists(bcache.file) + local old_name = api.nvim_buf_get_name(bufnr) + + if not bufexists then + -- Do not trigger BufFilePre/Post + -- TODO(lewis6991): figure out how to avoid reattaching without + -- disabling all autocommands. + util.noautocmd({ 'BufFilePre', 'BufFilePost' }, function() + util.buf_rename(bufnr, bcache.file) + end) + end + + local msg = bufexists and 'Cannot rename' or 'Renamed' + dprintf('%s buffer %d from %s to %s', msg, bufnr, old_name, bcache.file) +end + +--- @async +--- @param bufnr integer +local function watcher_handler0(bufnr) + local __FUNC__ = 'watcher_handler' + + -- Avoid cache hit for detached buffer + -- ref: https://github.com/lewis6991/gitsigns.nvim/issues/956 + if not manager.schedule(bufnr) then + dprint('buffer invalid (1)') + return + end + + local git_obj = cache[bufnr].git_obj + + Status:update(bufnr, { head = git_obj.repo.abbrev_head }) + + local was_tracked = git_obj.object_name ~= nil + local old_relpath = git_obj.relpath + + git_obj:update() + if not manager.schedule(bufnr) then + dprint('buffer invalid (3)') + return + end + + if config.watch_gitdir.follow_files and was_tracked and not git_obj.object_name then + -- File was tracked but is no longer tracked. Must of been removed or + -- moved. Check if it was moved and switch to it. + handle_moved(bufnr, old_relpath) + if not manager.schedule(bufnr) then + dprint('buffer invalid (4)') + return + end + end + + cache[bufnr]:invalidate(true) + + require('gitsigns.manager').update(bufnr) +end + +--- Debounce to: +--- - wait for all changes to the gitdir to complete. +--- Throttle to: +--- - ensure handler is only triggered once per git operation. +--- - prevent updates to the same buffer from interleaving as the handler is +--- async. +local watcher_handler = + debounce_trailing(200, async.create(1, throttle_by_id(watcher_handler0, true)), 1) + --- Ensure attaches cannot be interleaved for the same buffer. --- Since attaches are asynchronous we need to make sure an attach isn't --- performed whilst another one is in progress. @@ -325,8 +425,9 @@ local attach_throttled = throttle_by_id(function(cbuf, ctx, aucmd) }) if config.watch_gitdir.enable then - local watcher = require('gitsigns.watcher') - cache[cbuf].gitdir_watcher = watcher.watch_gitdir(cbuf, git_obj.repo.gitdir) + cache[cbuf].deregister_watcher = git_obj.repo:register_callback(function() + watcher_handler(cbuf) + end) end if not api.nvim_buf_is_loaded(cbuf) then diff --git a/lua/gitsigns/cache.lua b/lua/gitsigns/cache.lua index 5f7d7e3f6..3ff33c5f7 100644 --- a/lua/gitsigns/cache.lua +++ b/lua/gitsigns/cache.lua @@ -17,7 +17,7 @@ local M = { --- @field hunks_staged? Gitsigns.Hunk.Hunk[] --- --- @field staged_diffs? Gitsigns.Hunk.Hunk[] ---- @field gitdir_watcher? uv.uv_fs_event_t +--- @field deregister_watcher? fun() --- @field git_obj Gitsigns.GitObj --- @field blame? table local CacheEntry = M.CacheEntry @@ -127,9 +127,8 @@ function CacheEntry:get_blame(lnum, opts) end function CacheEntry:destroy() - local w = self.gitdir_watcher - if w and not w:is_closing() then - w:close() + if self.deregister_watcher then + self.deregister_watcher() end self.git_obj.repo:unref() end diff --git a/lua/gitsigns/git/repo.lua b/lua/gitsigns/git/repo.lua index 4c86ee556..15608c6d6 100644 --- a/lua/gitsigns/git/repo.lua +++ b/lua/gitsigns/git/repo.lua @@ -22,6 +22,7 @@ local uv = vim.uv or vim.loop --- Username configured for the repo. --- Needed for to determine "You" in current line blame. --- @field username string +--- @field watcher_callbacks table local M = {} --- Run git command the with the objects gitdir and toplevel @@ -96,6 +97,48 @@ function M:update_abbrev_head() self.abbrev_head = info.abbrev_head end +--- vim.inspect but on one line +--- @param x any +--- @return string +local function inspect(x) + return vim.inspect(x, { indent = '', newline = ' ' }) +end + +function M:_watcher_cb(err, filename, events) + if err then + log.dprintf('Git dir update error: %s', err) + return + end + + -- The luv docs say filename is passed as a string but it has been observed + -- to sometimes be nil. + -- https://github.com/lewis6991/gitsigns.nvim/issues/848 + if not filename then + log.eprint('No filename') + return + end + + log.dprintf("Git dir update: '%s' %s", filename, inspect(events)) + + async.run(function() + self:update_abbrev_head() + + for cb in pairs(self.watcher_callbacks) do + cb() + end + end) +end + +--- @param cb fun() +--- @return fun() deregister +function M:register_callback(cb) + self.watcher_callbacks[cb] = true + + return function() + self.watcher_callbacks[cb] = nil + end +end + --- @async --- @private --- @param info Gitsigns.RepoInfo @@ -110,6 +153,12 @@ local function new(info) end self.username = self:command({ 'config', 'user.name' }, { ignore_error = true })[1] + self.watcher_callbacks = {} + + local w = assert(uv.new_fs_event()) + w:start(self.gitdir, {}, function(err, filename, events) + self:_watcher_cb(err, filename, events) + end) return self end diff --git a/lua/gitsigns/watcher.lua b/lua/gitsigns/watcher.lua deleted file mode 100644 index a8b25c619..000000000 --- a/lua/gitsigns/watcher.lua +++ /dev/null @@ -1,161 +0,0 @@ -local api = vim.api -local uv = vim.loop - -local async = require('gitsigns.async') -local log = require('gitsigns.debug.log') -local util = require('gitsigns.util') -local Status = require('gitsigns.status') - -local cache = require('gitsigns.cache').cache -local config = require('gitsigns.config').config -local throttle_by_id = require('gitsigns.debounce').throttle_by_id -local debounce_trailing = require('gitsigns.debounce').debounce_trailing -local manager = require('gitsigns.manager') - -local dprint = log.dprint -local dprintf = log.dprintf - ---- @param bufnr integer ---- @param old_relpath string -local function handle_moved(bufnr, old_relpath) - local bcache = assert(cache[bufnr]) - local git_obj = bcache.git_obj - - local new_name = git_obj:has_moved() - if new_name then - dprintf('File moved to %s', new_name) - git_obj.relpath = new_name - if not git_obj.orig_relpath then - git_obj.orig_relpath = old_relpath - end - elseif git_obj.orig_relpath then - local orig_file = git_obj.repo.toplevel .. util.path_sep .. git_obj.orig_relpath - if not git_obj:file_info(orig_file).relpath then - return - end - --- File was moved in the index, but then reset - dprintf('Moved file reset') - git_obj.relpath = git_obj.orig_relpath - git_obj.orig_relpath = nil - else - -- File removed from index, do nothing - return - end - - git_obj.file = git_obj.repo.toplevel .. util.path_sep .. git_obj.relpath - bcache.file = git_obj.file - git_obj:update() - if not manager.schedule(bufnr) then - return - end - - local bufexists = util.bufexists(bcache.file) - local old_name = api.nvim_buf_get_name(bufnr) - - if not bufexists then - -- Do not trigger BufFilePre/Post - -- TODO(lewis6991): figure out how to avoid reattaching without - -- disabling all autocommands. - util.noautocmd({ 'BufFilePre', 'BufFilePost' }, function() - util.buf_rename(bufnr, bcache.file) - end) - end - - local msg = bufexists and 'Cannot rename' or 'Renamed' - dprintf('%s buffer %d from %s to %s', msg, bufnr, old_name, bcache.file) -end - ---- @async ---- @param bufnr integer -local function watcher_handler0(bufnr) - local __FUNC__ = 'watcher_handler' - - -- Avoid cache hit for detached buffer - -- ref: https://github.com/lewis6991/gitsigns.nvim/issues/956 - if not manager.schedule(bufnr) then - dprint('buffer invalid (1)') - return - end - - local git_obj = cache[bufnr].git_obj - - git_obj.repo:update_abbrev_head() - - if not manager.schedule(bufnr) then - dprint('buffer invalid (2)') - return - end - - Status:update(bufnr, { head = git_obj.repo.abbrev_head }) - - local was_tracked = git_obj.object_name ~= nil - local old_relpath = git_obj.relpath - - git_obj:update() - if not manager.schedule(bufnr) then - dprint('buffer invalid (3)') - return - end - - if config.watch_gitdir.follow_files and was_tracked and not git_obj.object_name then - -- File was tracked but is no longer tracked. Must of been removed or - -- moved. Check if it was moved and switch to it. - handle_moved(bufnr, old_relpath) - if not manager.schedule(bufnr) then - dprint('buffer invalid (4)') - return - end - end - - cache[bufnr]:invalidate(true) - - require('gitsigns.manager').update(bufnr) -end - ---- Debounce to: ---- - wait for all changes to the gitdir to complete. ---- Throttle to: ---- - ensure handler is only triggered once per git operation. ---- - prevent updates to the same buffer from interleaving as the handler is ---- async. -local watcher_handler = - debounce_trailing(200, async.create(1, throttle_by_id(watcher_handler0, true)), 1) - ---- vim.inspect but on one line ---- @param x any ---- @return string -local function inspect(x) - return vim.inspect(x, { indent = '', newline = ' ' }) -end - -local M = {} - ---- @param bufnr integer ---- @param gitdir string ---- @return uv.uv_fs_event_t -function M.watch_gitdir(bufnr, gitdir) - dprintf('Watching git dir') - local w = assert(uv.new_fs_event()) - w:start(gitdir, {}, function(err, filename, events) - local __FUNC__ = 'watcher_cb' - if err then - dprintf('Git dir update error: %s', err) - return - end - - -- The luv docs say filename is passed as a string but it has been observed - -- to sometimes be nil. - -- https://github.com/lewis6991/gitsigns.nvim/issues/848 - if not filename then - log.eprint('No filename') - return - end - - dprintf("Git dir update: '%s' %s", filename, inspect(events)) - - watcher_handler(bufnr) - end) - return w -end - -return M