Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: move watchers to repo objects #1089

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions lua/gitsigns/async.lua
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ local function run(func, callback, ...)

assert(type(fn) == 'function', 'type error :: expected func')

local args = { select(4, unpack(ret)) }
--- @type any[]
local args = { unpack(ret, 4, table.maxn(ret)) }
args[nargs] = step

local r = fn(unpack(args, 1, nargs))
Expand Down Expand Up @@ -144,7 +145,7 @@ function M.wait(argc, func, ...)
end

--- Creates an async function with a callback style function.
--- @param argc number The number of arguments of func. Must be included.
--- @param argc integer The number of arguments of func. Must be included.
--- @param func function A callback style function to be converted. The last argument must be the callback.
--- @return function: Returns an async function
function M.wrap(argc, func)
Expand Down
105 changes: 103 additions & 2 deletions lua/gitsigns/attach.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
7 changes: 3 additions & 4 deletions lua/gitsigns/cache.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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<integer,Gitsigns.BlameInfo?>
local CacheEntry = M.CacheEntry
Expand Down Expand Up @@ -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
Expand Down
49 changes: 49 additions & 0 deletions lua/gitsigns/git/repo.lua
Original file line number Diff line number Diff line change
Expand Up @@ -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<fun(), true>
local M = {}

--- Run git command the with the objects gitdir and toplevel
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
161 changes: 0 additions & 161 deletions lua/gitsigns/watcher.lua

This file was deleted.

Loading