From b75bae87b2811609e163b5abc3712a2d33bd6931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sindre=20T=2E=20Str=C3=B8m?= Date: Mon, 5 Dec 2022 15:43:32 +0100 Subject: [PATCH 1/2] feat: Enable `attach()` to work with any buffer when given context data. --- doc/gitsigns.txt | 17 ++++++++- lua/gitsigns.lua | 91 +++++++++++++++++++++++++++++++------------- lua/gitsigns/git.lua | 2 +- teal/gitsigns.tl | 91 +++++++++++++++++++++++++++++++------------- teal/gitsigns/git.tl | 2 +- 5 files changed, 146 insertions(+), 57 deletions(-) diff --git a/doc/gitsigns.txt b/doc/gitsigns.txt index 8f9b6d82..817ae06d 100644 --- a/doc/gitsigns.txt +++ b/doc/gitsigns.txt @@ -102,7 +102,7 @@ setup({cfg}) *gitsigns.setup()* {cfg} Table object containing configuration for Gitsigns. See |gitsigns-usage| for more details. -attach({bufnr}) *gitsigns.attach()* +attach({bufnr}, {ctx}) *gitsigns.attach()* Attach Gitsigns to the buffer. Attributes: ~ @@ -110,6 +110,21 @@ attach({bufnr}) *gitsigns.attach()* Parameters: ~ {bufnr} (number): Buffer number + {ctx} (table|nil): + Git context data that may optionally be used to attach to any + buffer that represents a real git object. + • {file}: (string) + Path to the file represented by the buffer, relative to the + top-level. + • {toplevel}: (string) + Path to the top-level of the parent git repository. + • {gitdir}: (string) + Path to the git directory of the parent git repository + (typically the ".git/" directory). + • {commit}: (string) + The git revision that the file belongs to. + • {base}: (string|nil) + The git revision that the file should be compared to. detach({bufnr}) *gitsigns.detach()* Detach Gitsigns from the buffer {bufnr}. If {bufnr} is not diff --git a/lua/gitsigns.lua b/lua/gitsigns.lua index 54514b53..3792ecc3 100644 --- a/lua/gitsigns.lua +++ b/lua/gitsigns.lua @@ -28,7 +28,7 @@ local api = vim.api local uv = vim.loop local current_buf = api.nvim_get_current_buf -local M = {} +local M = {GitContext = {}, } @@ -36,6 +36,16 @@ local M = {} + + + + + + + +local GitContext = M.GitContext + + function M.detach_all() for k, _ in pairs(cache) do M.detach(k) @@ -186,7 +196,7 @@ end -local attach_throttled = throttle_by_id(function(cbuf, aucmd) +local attach_throttled = throttle_by_id(function(cbuf, ctx, aucmd) local __FUNC__ = 'attach' if vimgrep_running then dprint('attaching is disabled') @@ -209,33 +219,45 @@ local attach_throttled = throttle_by_id(function(cbuf, aucmd) return end - if api.nvim_buf_line_count(cbuf) > config.max_file_length then - dprint('Exceeds max_file_length') - return - end - - if vim.bo[cbuf].buftype ~= '' then - dprint('Non-normal buffer') - return - end - - local file, commit = get_buf_path(cbuf) local encoding = vim.bo[cbuf].fileencoding if encoding == '' then encoding = 'utf-8' end + local file + local commit + local gitdir_oap + local toplevel_oap - local file_dir = util.dirname(file) + if ctx then + gitdir_oap = ctx.gitdir + toplevel_oap = ctx.toplevel + file = ctx.toplevel .. util.path_sep .. ctx.file + commit = ctx.commit + else + if api.nvim_buf_line_count(cbuf) > config.max_file_length then + dprint('Exceeds max_file_length') + return + end - if not file_dir or not util.path_exists(file_dir) then - dprint('Not a path') - return + if vim.bo[cbuf].buftype ~= '' then + dprint('Non-normal buffer') + return + end + + file, commit = get_buf_path(cbuf) + local file_dir = util.dirname(file) + + if not file_dir or not util.path_exists(file_dir) then + dprint('Not a path') + return + end + + gitdir_oap, toplevel_oap = on_attach_pre(cbuf) end - local gitdir_oap, toplevel_oap = on_attach_pre(cbuf) local git_obj = git.Obj.new(file, encoding, gitdir_oap, toplevel_oap) - if not git_obj then + if not git_obj and not ctx then git_obj = try_worktrees(cbuf, file, encoding) scheduler() end @@ -258,7 +280,7 @@ local attach_throttled = throttle_by_id(function(cbuf, aucmd) return end - if not util.path_exists(file) or uv.fs_stat(file).type == 'directory' then + if not ctx and (not util.path_exists(file) or uv.fs_stat(file).type == 'directory') then dprint('Not a file') return end @@ -283,7 +305,7 @@ local attach_throttled = throttle_by_id(function(cbuf, aucmd) end cache[cbuf] = CacheEntry.new({ - base = config.base, + base = ctx and ctx.base or config.base, file = file, commit = commit, gitdir_watcher = manager.watch_gitdir(cbuf, repo.gitdir), @@ -318,8 +340,23 @@ end) -M.attach = void(function(bufnr, _trigger) - attach_throttled(bufnr or current_buf(), _trigger) + + + + + + + + + + + + + + + +M.attach = void(function(bufnr, ctx, _trigger) + attach_throttled(bufnr or current_buf(), ctx, _trigger) end) local function setup_cli() @@ -414,7 +451,7 @@ M.setup = void(function(cfg) for _, buf in ipairs(api.nvim_list_bufs()) do if api.nvim_buf_is_loaded(buf) and api.nvim_buf_get_name(buf) ~= '' then - M.attach(buf, 'setup') + M.attach(buf, nil, 'setup') scheduler() end end @@ -423,9 +460,9 @@ M.setup = void(function(cfg) autocmd('VimLeavePre', M.detach_all) autocmd('ColorScheme', hl.setup_highlights) - autocmd('BufRead', wrap_func(M.attach, nil, 'BufRead')) - autocmd('BufNewFile', wrap_func(M.attach, nil, 'BufNewFile')) - autocmd('BufWritePost', wrap_func(M.attach, nil, 'BufWritePost')) + autocmd('BufRead', wrap_func(M.attach, nil, nil, 'BufRead')) + autocmd('BufNewFile', wrap_func(M.attach, nil, nil, 'BufNewFile')) + autocmd('BufWritePost', wrap_func(M.attach, nil, nil, 'BufWritePost')) autocmd('OptionSet', { pattern = 'fileformat', diff --git a/lua/gitsigns/git.lua b/lua/gitsigns/git.lua index 40371244..b38736c5 100644 --- a/lua/gitsigns/git.lua +++ b/lua/gitsigns/git.lua @@ -290,7 +290,7 @@ function M.get_repo_info(path, cmd, gitdir, toplevel) local results = git_command(args, { command = cmd or 'git', suppress_stderr = true, - cwd = path, + cwd = toplevel or path, }) local ret = { diff --git a/teal/gitsigns.tl b/teal/gitsigns.tl index 1dfef6fe..34dc067e 100644 --- a/teal/gitsigns.tl +++ b/teal/gitsigns.tl @@ -32,9 +32,19 @@ local record M setup : function(cfg: Config) detach : function(bufnr: integer, _keep_signs: boolean) detach_all : function() - attach : function(cbuf: integer, trigger: string) + attach : function(cbuf: integer, ctx: GitContext, trigger: string) + + record GitContext + toplevel: string + gitdir: string + file: string + commit: string + base: string + end end +local GitContext = M.GitContext + --- Detach Gitsigns from all buffers it is attached to. function M.detach_all() for k, _ in pairs(cache as {integer:CacheEntry}) do @@ -186,7 +196,7 @@ end -- Ensure attaches cannot be interleaved. -- Since attaches are asynchronous we need to make sure an attach isn't -- performed whilst another one is in progress. -local attach_throttled = throttle_by_id(function(cbuf: integer, aucmd: string) +local attach_throttled = throttle_by_id(function(cbuf: integer, ctx: GitContext, aucmd: string) local __FUNC__ = 'attach' if vimgrep_running then dprint('attaching is disabled') @@ -209,33 +219,45 @@ local attach_throttled = throttle_by_id(function(cbuf: integer, aucmd: string) return end - if api.nvim_buf_line_count(cbuf) > config.max_file_length then - dprint('Exceeds max_file_length') - return - end - - if vim.bo[cbuf].buftype ~= '' then - dprint('Non-normal buffer') - return - end - - local file, commit = get_buf_path(cbuf) local encoding = vim.bo[cbuf].fileencoding if encoding == '' then encoding = 'utf-8' end + local file: string + local commit: string + local gitdir_oap: string + local toplevel_oap: string - local file_dir = util.dirname(file) + if ctx then + gitdir_oap = ctx.gitdir + toplevel_oap = ctx.toplevel + file = ctx.toplevel .. util.path_sep .. ctx.file + commit = ctx.commit + else + if api.nvim_buf_line_count(cbuf) > config.max_file_length then + dprint('Exceeds max_file_length') + return + end - if not file_dir or not util.path_exists(file_dir) then - dprint('Not a path') - return + if vim.bo[cbuf].buftype ~= '' then + dprint('Non-normal buffer') + return + end + + file, commit = get_buf_path(cbuf) + local file_dir = util.dirname(file) + + if not file_dir or not util.path_exists(file_dir) then + dprint('Not a path') + return + end + + gitdir_oap, toplevel_oap = on_attach_pre(cbuf) end - local gitdir_oap, toplevel_oap = on_attach_pre(cbuf) local git_obj = git.Obj.new(file, encoding, gitdir_oap, toplevel_oap) - if not git_obj then + if not git_obj and not ctx then git_obj = try_worktrees(cbuf, file, encoding) scheduler() end @@ -258,7 +280,7 @@ local attach_throttled = throttle_by_id(function(cbuf: integer, aucmd: string) return end - if not util.path_exists(file) or uv.fs_stat(file).type == 'directory' then + if not ctx and (not util.path_exists(file) or uv.fs_stat(file).type == 'directory') then dprint('Not a file') return end @@ -283,7 +305,7 @@ local attach_throttled = throttle_by_id(function(cbuf: integer, aucmd: string) end cache[cbuf] = CacheEntry.new { - base = config.base, + base = ctx and ctx.base or config.base, file = file, commit = commit, gitdir_watcher = manager.watch_gitdir(cbuf, repo.gitdir), @@ -318,8 +340,23 @@ end) --- --- Parameters: ~ --- {bufnr} (number): Buffer number -M.attach = void(function(bufnr: integer, _trigger: string) - attach_throttled(bufnr or current_buf(), _trigger) +--- {ctx} (table|nil): +--- Git context data that may optionally be used to attach to any +--- buffer that represents a real git object. +--- • {file}: (string) +--- Path to the file represented by the buffer, relative to the +--- top-level. +--- • {toplevel}: (string) +--- Path to the top-level of the parent git repository. +--- • {gitdir}: (string) +--- Path to the git directory of the parent git repository +--- (typically the ".git/" directory). +--- • {commit}: (string) +--- The git revision that the file belongs to. +--- • {base}: (string|nil) +--- The git revision that the file should be compared to. +M.attach = void(function(bufnr: integer, ctx: GitContext, _trigger: string) + attach_throttled(bufnr or current_buf(), ctx, _trigger) end) local function setup_cli() @@ -414,7 +451,7 @@ M.setup = void(function(cfg: Config) for _, buf in ipairs(api.nvim_list_bufs()) do if api.nvim_buf_is_loaded(buf) and api.nvim_buf_get_name(buf) ~= '' then - M.attach(buf, 'setup') + M.attach(buf, nil, 'setup') scheduler() end end @@ -423,9 +460,9 @@ M.setup = void(function(cfg: Config) autocmd('VimLeavePre' , M.detach_all) autocmd('ColorScheme' , hl.setup_highlights) - autocmd('BufRead' , wrap_func(M.attach, nil, 'BufRead')) - autocmd('BufNewFile' , wrap_func(M.attach, nil, 'BufNewFile')) - autocmd('BufWritePost', wrap_func(M.attach, nil, 'BufWritePost')) + autocmd('BufRead' , wrap_func(M.attach, nil, nil, 'BufRead')) + autocmd('BufNewFile' , wrap_func(M.attach, nil, nil, 'BufNewFile')) + autocmd('BufWritePost', wrap_func(M.attach, nil, nil, 'BufWritePost')) autocmd('OptionSet', { pattern = 'fileformat', diff --git a/teal/gitsigns/git.tl b/teal/gitsigns/git.tl index de40aebe..deaf96a1 100644 --- a/teal/gitsigns/git.tl +++ b/teal/gitsigns/git.tl @@ -290,7 +290,7 @@ function M.get_repo_info(path: string, cmd: string, gitdir: string, toplevel: st local results = git_command(args, { command = cmd or 'git', suppress_stderr = true, - cwd = path + cwd = toplevel or path, }) local ret: M.RepoInfo = { From 71e0e102e2ac83c6685c6a92fa4f714eef17778a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sindre=20T=2E=20Str=C3=B8m?= Date: Tue, 6 Dec 2022 13:17:08 +0100 Subject: [PATCH 2/2] fix(test): Pattern escape file names when matching debug messages. --- test/gitdir_watcher_spec.lua | 16 ++++++++-------- test/gitsigns_spec.lua | 4 ++-- test/gs_helpers.lua | 9 +++++++++ 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/test/gitdir_watcher_spec.lua b/test/gitdir_watcher_spec.lua index 93f93ec4..7bf9ffb5 100644 --- a/test/gitdir_watcher_spec.lua +++ b/test/gitdir_watcher_spec.lua @@ -55,7 +55,7 @@ describe('gitdir_watcher', function() 'attach(1): Attaching (trigger=BufRead)', 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 .* '..test_file), + p('run_job: git .* ls%-files .* '..helpers.pesc(test_file)), 'watch_gitdir(1): Watching git dir', p'run_job: git .* show :0:dummy.txt', 'update(1): updates: 1, jobs: 6', @@ -70,10 +70,10 @@ describe('gitdir_watcher', function() 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 .* '..test_file), + p('run_job: git .* ls%-files .* '..helpers.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 .* '..test_file2), + p('run_job: git .* ls%-files .* '..helpers.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: 11' @@ -88,10 +88,10 @@ describe('gitdir_watcher', function() 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 .* '..test_file2), + p('run_job: git .* ls%-files .* '..helpers.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 .* '..test_file3), + p('run_job: git .* ls%-files .* '..helpers.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: 16' @@ -106,11 +106,11 @@ describe('gitdir_watcher', function() 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 .* '..test_file3), + p('run_job: git .* ls%-files .* '..helpers.pesc(test_file3)), p'run_job: git .* diff %-%-name%-status %-C %-%-cached', - p('run_job: git .* ls%-files .* '..test_file), + p('run_job: git .* ls%-files .* '..helpers.pesc(test_file)), 'handle_moved(1): Moved file reset', - p('run_job: git .* ls%-files .* '..test_file), + p('run_job: git .* ls%-files .* '..helpers.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: 22' diff --git a/test/gitsigns_spec.lua b/test/gitsigns_spec.lua index b5f22377..20f78ca8 100644 --- a/test/gitsigns_spec.lua +++ b/test/gitsigns_spec.lua @@ -91,7 +91,7 @@ describe('gitsigns', function() 'attach(1): Attaching (trigger=BufRead)', 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 %-%-stage %-%-others %-%-exclude%-standard %-%-eol '..test_file), + p('run_job: git .* ls%-files %-%-stage %-%-others %-%-exclude%-standard %-%-eol '..helpers.pesc(test_file)), 'watch_gitdir(1): Watching git dir', p'run_job: git .* show :0:dummy.txt', 'update(1): updates: 1, jobs: 7' @@ -201,7 +201,7 @@ describe('gitsigns', function() 'attach(1): Attaching (trigger=BufNewFile)', 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 %-%-stage %-%-others %-%-exclude%-standard %-%-eol '..newfile), + p('run_job: git .* ls%-files %-%-stage %-%-others %-%-exclude%-standard %-%-eol '..helpers.pesc(newfile)), 'attach(1): Not a file', } diff --git a/test/gs_helpers.lua b/test/gs_helpers.lua index 127f836d..42ad4d65 100644 --- a/test/gs_helpers.lua +++ b/test/gs_helpers.lua @@ -47,6 +47,15 @@ local test_file_text = { 'content', 'doesn\'t', 'matter,', 'it', 'just', 'needs', 'to', 'be', 'static.' } +--- Escapes magic chars in |lua-patterns|. +--- +---@see https://github.com/rxi/lume +---@param s string String to escape +---@return string %-escaped pattern string +function M.pesc(s) + return (s:gsub('[%(%)%.%%%+%-%*%?%[%]%^%$]', '%%%1')) +end + function M.git(args) system{"git", "-C", M.scratch, unpack(args)} end