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(watcher): move from libuv poll to file event watcher #660

Merged
merged 1 commit into from
Jun 19, 2023
Merged
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
1 change: 0 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ jobs:
runs-on: ubuntu-latest

strategy:
fail-fast: true
matrix:
neovim_branch:
- 'v0.8.3'
Expand Down
18 changes: 12 additions & 6 deletions lua/gitsigns.lua
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ local uv = vim.loop

local M = {}

local cwd_watcher ---@type uv_fs_poll_t
local cwd_watcher ---@type uv_fs_event_t?

local update_cwd_head = void(function()
local paths = vim.fs.find('.git', {
Expand All @@ -29,7 +29,7 @@ local update_cwd_head = void(function()
if cwd_watcher then
cwd_watcher:stop()
else
cwd_watcher = assert(uv.new_fs_poll())
cwd_watcher = assert(uv.new_fs_event())
end

local cwd = assert(vim.loop.cwd())
Expand Down Expand Up @@ -70,10 +70,18 @@ local update_cwd_head = void(function()
return
end

local debounce_trailing = require('gitsigns.debounce').debounce_trailing

local update_head = debounce_trailing(100, void(function()
local new_head = git.get_repo_info(cwd).abbrev_head
scheduler()
vim.g.gitsigns_head = new_head
end))

-- Watch .git/HEAD to detect branch changes
cwd_watcher:start(
towatch,
config.watch_gitdir.interval,
{},
void(function(err)
local __FUNC__ = 'cwd_watcher_cb'
if err then
Expand All @@ -82,9 +90,7 @@ local update_cwd_head = void(function()
end
dprint('Git cwd dir update')

local new_head = git.get_repo_info(cwd).abbrev_head
scheduler()
vim.g.gitsigns_head = new_head
update_head()
end)
)
end)
Expand Down
4 changes: 0 additions & 4 deletions lua/gitsigns/attach.lua
Original file line number Diff line number Diff line change
Expand Up @@ -352,10 +352,6 @@ local attach_throttled = throttle_by_id(function(cbuf, ctx, aucmd)

-- Initial update
manager.update(cbuf, cache[cbuf])

if config.keymaps and not vim.tbl_isempty(config.keymaps) then
require('gitsigns.mappings')(config.keymaps, cbuf)
end
end)

--- Detach Gitsigns from all buffers it is attached to.
Expand Down
4 changes: 3 additions & 1 deletion 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_poll_t
--- @field gitdir_watcher? uv_fs_event_t
--- @field git_obj Gitsigns.GitObj
--- @field commit? string
local CacheEntry = M.CacheEntry
Expand Down Expand Up @@ -57,6 +57,8 @@ function CacheEntry:invalidate()
self.hunks_staged = nil
end

--- @param o Gitsigns.CacheEntry
--- @return Gitsigns.CacheEntry
function CacheEntry.new(o)
o.staged_diffs = o.staged_diffs or {}
return setmetatable(o, { __index = CacheEntry })
Expand Down
6 changes: 1 addition & 5 deletions lua/gitsigns/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ end
--- @field sign_priority integer
--- @field _on_attach_pre fun(bufnr: integer, callback: fun(_: table))
--- @field on_attach fun(bufnr: integer)
--- @field watch_gitdir { enable: boolean, interval: integer, follow_files: boolean }
--- @field watch_gitdir { enable: boolean, follow_files: boolean }
--- @field max_file_length integer
--- @field update_debounce integer
--- @field status_formatter fun(_: table<string,any>): string
Expand Down Expand Up @@ -295,7 +295,6 @@ M.schema = {
deep_extend = true,
default = {
enable = true,
interval = 1000,
follow_files = true,
},
description = [[
Expand All @@ -307,9 +306,6 @@ M.schema = {
• `enable`:
Whether the watcher is enabled.

• `interval`:
Interval the watcher waits between polls of the gitdir in milliseconds.

• `follow_files`:
If a file is moved with `git mv`, switch the buffer to the new location.
]],
Expand Down
85 changes: 52 additions & 33 deletions lua/gitsigns/manager.lua
Original file line number Diff line number Diff line change
Expand Up @@ -497,7 +497,7 @@ local function handle_moved(bufnr, bcache, old_relpath)
git_obj.orig_relpath = nil
do_update = true
end
else
-- else
-- File removed from index, do nothing
end

Expand All @@ -519,59 +519,78 @@ local function handle_moved(bufnr, bcache, old_relpath)
end
end

-- vim.inspect but on one line
--- @param x any
--- @return string
local function inspect(x)
return vim.inspect(x, {indent = '', newline = ' '})
end

local watch_gitdir_handler = debounce_trailing(100, void(function(bufnr)
local bcache = cache[bufnr]

if not bcache then
-- Very occasionally an external git operation may cause the buffer to
-- detach and update the git dir simultaneously. When this happens this
-- handler will trigger but there will be no cache.
dprint('Has detached, aborting')
return
end

local git_obj = bcache.git_obj

git_obj.repo:update_abbrev_head()

scheduler()
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_file_info()

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, bcache, old_relpath)
end

bcache:invalidate()

M.update(bufnr, bcache)
end))

--- @param bufnr integer
--- @param gitdir string
--- @return uv_fs_poll_t?
--- @return uv_fs_event_t?
function M.watch_gitdir(bufnr, gitdir)
if not config.watch_gitdir.enable then
return
end

dprintf('Watching git dir')
local w = assert(uv.new_fs_poll())
local w = assert(uv.new_fs_event())
w:start(
gitdir,
config.watch_gitdir.interval,
void(function(err)
{},
function(err, filename, events)
local __FUNC__ = 'watcher_cb'
if err then
dprintf('Git dir update error: %s', err)
return
end
dprint('Git dir update')

local bcache = cache[bufnr]
local info = string.format("Git dir update: '%s' %s", filename, inspect(events))

if not bcache then
-- Very occasionally an external git operation may cause the buffer to
-- detach and update the git dir simultaneously. When this happens this
-- handler will trigger but there will be no cache.
dprint('Has detached, aborting')
if vim.endswith(filename, '.lock') then
dprintf("%s (ignoring)", info)
return
end

local git_obj = bcache.git_obj

git_obj.repo:update_abbrev_head()

scheduler()
Status:update(bufnr, { head = git_obj.repo.abbrev_head })
dprint(info)

local was_tracked = git_obj.object_name ~= nil
local old_relpath = git_obj.relpath

git_obj:update_file_info()

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, bcache, old_relpath)
end

bcache:invalidate()

M.update(bufnr, bcache)
end)
watch_gitdir_handler(bufnr)
end
)
return w
end
Expand Down
85 changes: 49 additions & 36 deletions test/gitdir_watcher_spec.lua
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
local helpers = require('test.gs_helpers')

local Screen = require('test.screen')

local clear = helpers.clear
local exec_lua = helpers.exec_lua
local edit = helpers.edit
Expand All @@ -11,11 +9,11 @@ local cleanup = helpers.cleanup
local command = helpers.command
local test_config = helpers.test_config
local match_debug_messages = helpers.match_debug_messages
local p = helpers.p
local match_dag = helpers.match_dag
local n, p, np = helpers.n, helpers.p, helpers.np
local setup_gitsigns = helpers.setup_gitsigns
local test_file = helpers.test_file
local git = helpers.git
local get_buf_name = helpers.curbufmeths.get_name

local it = helpers.it(it)

Expand Down Expand Up @@ -48,12 +46,12 @@ describe('gitdir_watcher', function()

match_debug_messages {
'attach(1): Attaching (trigger=BufReadPost)',
p"run_job: git .* config user.name",
p"run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD",
p('run_job: git .* ls%-files .* '..vim.pesc(test_file)),
'watch_gitdir(1): Watching git dir',
p'run_job: git .* show :0:dummy.txt',
'update(1): updates: 1, jobs: 5',
np"run_job: git .* config user.name",
np"run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD",
np('run_job: git .* ls%-files .* '..vim.pesc(test_file)),
n'watch_gitdir(1): Watching git dir',
np'run_job: git .* show :0:dummy.txt',
n'update(1): updates: 1, jobs: 5',
}

eq({[1] = test_file}, get_bufs())
Expand All @@ -63,16 +61,21 @@ describe('gitdir_watcher', function()
local test_file2 = test_file..'2'
git{'mv', test_file, test_file2}

match_dag {
"watcher_cb(1): Git dir update: 'index.lock' { rename = true } (ignoring)",
"watcher_cb(1): Git dir update: 'index' { rename = true }",
"watcher_cb(1): Git dir update: 'index' { rename = true }",
}

match_debug_messages {
'watcher_cb(1): Git dir update',
p'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD',
p('run_job: git .* ls%-files .* '..vim.pesc(test_file)),
p'run_job: git .* diff %-%-name%-status %-C %-%-cached',
'handle_moved(1): File moved to dummy.txt2',
p('run_job: git .* ls%-files .* '..vim.pesc(test_file2)),
p'handle_moved%(1%): Renamed buffer 1 from .*/dummy.txt to .*/dummy.txt2',
p'run_job: git .* show :0:dummy.txt2',
'update(1): updates: 2, jobs: 10'
np'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD',
np('run_job: git .* ls%-files .* '..vim.pesc(test_file)),
np'run_job: git .* diff %-%-name%-status %-C %-%-cached',
n'handle_moved(1): File moved to dummy.txt2',
np('run_job: git .* ls%-files .* '..vim.pesc(test_file2)),
np'handle_moved%(1%): Renamed buffer 1 from .*/dummy.txt to .*/dummy.txt2',
np'run_job: git .* show :0:dummy.txt2',
n'update(1): updates: 2, jobs: 10'
}

eq({[1] = test_file2}, get_bufs())
Expand All @@ -83,16 +86,21 @@ describe('gitdir_watcher', function()

git{'mv', test_file2, test_file3}

match_dag {
"watcher_cb(1): Git dir update: 'index.lock' { rename = true } (ignoring)",
"watcher_cb(1): Git dir update: 'index' { rename = true }",
"watcher_cb(1): Git dir update: 'index' { rename = true }",
}

match_debug_messages {
'watcher_cb(1): Git dir update',
p'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD',
p('run_job: git .* ls%-files .* '..vim.pesc(test_file2)),
p'run_job: git .* diff %-%-name%-status %-C %-%-cached',
'handle_moved(1): File moved to dummy.txt3',
p('run_job: git .* ls%-files .* '..vim.pesc(test_file3)),
p'handle_moved%(1%): Renamed buffer 1 from .*/dummy.txt2 to .*/dummy.txt3',
p'run_job: git .* show :0:dummy.txt3',
'update(1): updates: 3, jobs: 15'
np('run_job: git .* ls%-files .* '..vim.pesc(test_file2)),
np'run_job: git .* diff %-%-name%-status %-C %-%-cached',
n'handle_moved(1): File moved to dummy.txt3',
np('run_job: git .* ls%-files .* '..vim.pesc(test_file3)),
np'handle_moved%(1%): Renamed buffer 1 from .*/dummy.txt2 to .*/dummy.txt3',
np'run_job: git .* show :0:dummy.txt3',
n'update(1): updates: 3, jobs: 15'
}

eq({[1] = test_file3}, get_bufs())
Expand All @@ -101,17 +109,22 @@ describe('gitdir_watcher', function()

git{'mv', test_file3, test_file}

match_dag {
"watcher_cb(1): Git dir update: 'index.lock' { rename = true } (ignoring)",
"watcher_cb(1): Git dir update: 'index' { rename = true }",
"watcher_cb(1): Git dir update: 'index' { rename = true }",
}

match_debug_messages {
'watcher_cb(1): Git dir update',
p'run_job: git .* rev%-parse %-%-show%-toplevel %-%-absolute%-git%-dir %-%-abbrev%-ref HEAD',
p('run_job: git .* ls%-files .* '..vim.pesc(test_file3)),
p'run_job: git .* diff %-%-name%-status %-C %-%-cached',
p('run_job: git .* ls%-files .* '..vim.pesc(test_file)),
'handle_moved(1): Moved file reset',
p('run_job: git .* ls%-files .* '..vim.pesc(test_file)),
p'handle_moved%(1%): Renamed buffer 1 from .*/dummy.txt3 to .*/dummy.txt',
p'run_job: git .* show :0:dummy.txt',
'update(1): updates: 4, jobs: 21'
np('run_job: git .* ls%-files .* '..vim.pesc(test_file3)),
np'run_job: git .* diff %-%-name%-status %-C %-%-cached',
np('run_job: git .* ls%-files .* '..vim.pesc(test_file)),
n'handle_moved(1): Moved file reset',
np('run_job: git .* ls%-files .* '..vim.pesc(test_file)),
np'handle_moved%(1%): Renamed buffer 1 from .*/dummy.txt3 to .*/dummy.txt',
np'run_job: git .* show :0:dummy.txt',
n'update(1): updates: 4, jobs: 21'
}

eq({[1] = test_file}, get_bufs())
Expand Down
Loading