Skip to content

Commit

Permalink
feat(watcher): move from libuv poll to file event watcher
Browse files Browse the repository at this point in the history
  • Loading branch information
lewis6991 committed Jun 19, 2023
1 parent 256569c commit f137883
Show file tree
Hide file tree
Showing 11 changed files with 245 additions and 210 deletions.
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

0 comments on commit f137883

Please sign in to comment.