perf: reduce startup time

This commit is contained in:
Lewis Russell 2023-02-23 11:16:10 +00:00 committed by Lewis Russell
parent 76b71f74ce
commit f965e3bad0
33 changed files with 1564 additions and 1410 deletions

View File

@ -7,9 +7,6 @@ on:
branches: [ main ]
workflow_dispatch:
env:
CC: clang
jobs:
commit_lint:
runs-on: ubuntu-latest
@ -21,7 +18,7 @@ jobs:
strategy:
fail-fast: true
matrix:
neovim_branch: ['v0.7.2', 'v0.8.3', 'nightly']
neovim_branch: ['v0.8.3', 'nightly']
runs-on: ubuntu-latest
env:
NEOVIM_BRANCH: ${{ matrix.neovim_branch }}
@ -30,14 +27,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v3
- name: Install LuaJIT
uses: leafo/gh-actions-lua@v9
with:
luaVersion: "luajit-2.1.0-beta3"
- name: Install Luarocks
uses: leafo/gh-actions-luarocks@v4
- name: Get Neovim SHA
id: get-nvim-sha
run: |
@ -50,12 +39,6 @@ jobs:
path: deps
key: ${{ steps.get-nvim-sha.outputs.sha }}-${{ hashFiles('.github/workflows/ci.yml, Makefile') }}
- name: Install Lua Deps
run: make lua_deps
- name: Check lua files are built from latest teal
run: make tl-ensure
- name: Install Neovim build dependencies
if: steps.cache-deps.outputs.cache-hit != 'true'
run: |
@ -78,5 +61,11 @@ jobs:
if: steps.cache-deps.outputs.cache-hit != 'true'
run: make test_deps
- name: Install Lua Deps
run: make lua_deps
- name: Check lua files are built from latest teal
run: make tl-ensure
- name: Run Test
run: make test

View File

@ -1,9 +1,6 @@
export PJ_ROOT=$(PWD)
# Suppress built in rules. This reduces clutter when running with -d
MAKEFLAGS += --no-builtin-rules
FILTER ?= .*
LUA_VERSION := 5.1
@ -13,7 +10,7 @@ NEOVIM_BRANCH ?= master
DEPS_DIR := $(PWD)/deps/nvim-$(NEOVIM_BRANCH)
NVIM_DIR := $(DEPS_DIR)/neovim
LUAROCKS := luarocks --lua-version=$(LUA_VERSION)
LUAROCKS := $(DEPS_DIR)/luarocks/usr/bin/luarocks
LUAROCKS_TREE := $(DEPS_DIR)/luarocks/usr
LUAROCKS_LPATH := $(LUAROCKS_TREE)/share/lua/$(LUA_VERSION)
LUAROCKS_INIT := eval $$($(LUAROCKS) --tree $(LUAROCKS_TREE) path) &&
@ -23,25 +20,27 @@ LUAROCKS_INIT := eval $$($(LUAROCKS) --tree $(LUAROCKS_TREE) path) &&
$(NVIM_DIR):
@mkdir -p $(DEPS_DIR)
git clone --depth 1 https://github.com/neovim/neovim --branch $(NEOVIM_BRANCH) $@
@# disable LTO to reduce compile time
make -C $@ \
DEPS_BUILD_DIR=$(dir $(LUAROCKS_TREE)) \
CMAKE_BUILD_TYPE=RelWithDebInfo
CMAKE_BUILD_TYPE=RelWithDebInfo \
CMAKE_EXTRA_FLAGS='-DCI_BUILD=OFF -DENABLE_LTO=OFF'
TL := $(LUAROCKS_TREE)/bin/tl
$(TL):
$(TL): $(NVIM_DIR)
@mkdir -p $$(dirname $@)
$(LUAROCKS) --tree $(LUAROCKS_TREE) install tl $(TL_VERSION)
INSPECT := $(LUAROCKS_LPATH)/inspect.lua
$(INSPECT):
$(INSPECT): $(NVIM_DIR)
@mkdir -p $$(dirname $@)
$(LUAROCKS) --tree $(LUAROCKS_TREE) install inspect
LUV := $(LUAROCKS_TREE)/lib/lua/$(LUA_VERSION)/luv.so
$(LUV):
$(LUV): $(NVIM_DIR)
@mkdir -p $$(dirname $@)
$(LUAROCKS) --tree $(LUAROCKS_TREE) install luv

View File

@ -335,6 +335,7 @@ local function get_marker_text(marker)
CONFIG = gen_config_doc,
FUNCTIONS = gen_functions_doc{
'teal/gitsigns.tl',
'teal/gitsigns/attach.tl',
'teal/gitsigns/actions.tl',
},
HIGHLIGHTS = gen_highlights_doc,

519
lua/gitsigns.lua generated
View File

@ -1,405 +1,159 @@
local async = require('gitsigns.async')
local void = require('gitsigns.async').void
local scheduler = require('gitsigns.async').scheduler
local Status = require("gitsigns.status")
local git = require('gitsigns.git')
local manager = require('gitsigns.manager')
local util = require('gitsigns.util')
local hl = require('gitsigns.highlight')
local gs_cache = require('gitsigns.cache')
local cache = gs_cache.cache
local CacheEntry = gs_cache.CacheEntry
local gs_config = require('gitsigns.config')
local Config = gs_config.Config
local config = gs_config.config
local gs_debug = require("gitsigns.debug")
local dprintf = gs_debug.dprintf
local dprint = gs_debug.dprint
local Debounce = require("gitsigns.debounce")
local debounce_trailing = Debounce.debounce_trailing
local throttle_by_id = Debounce.throttle_by_id
local log = require('gitsigns.debug.log')
local dprintf = log.dprintf
local dprint = log.dprint
local api = vim.api
local uv = vim.loop
local current_buf = api.nvim_get_current_buf
local uv = require('gitsigns.uv')
local M = {GitContext = {}, }
local M = {}
-- from attach.tl
local cwd_watcher
local update_cwd_head = void(function()
local paths = vim.fs.find('.git', {
limit = 1,
upward = true,
type = 'directory',
})
local GitContext = M.GitContext
--- Detach Gitsigns from all buffers it is attached to.
function M.detach_all()
for k, _ in pairs(cache) do
M.detach(k)
end
end
--- Detach Gitsigns from the buffer {bufnr}. If {bufnr} is not
--- provided then the current buffer is used.
---
--- Parameters: ~
--- {bufnr} (number): Buffer number
function M.detach(bufnr, _keep_signs)
-- When this is called interactively (with no arguments) we want to remove all
-- the signs, however if called via a detach event (due to nvim_buf_attach)
-- then we don't want to clear the signs in case the buffer is just being
-- updated due to the file externally changing. When this happens a detach and
-- attach event happen in sequence and so we keep the old signs to stop the
-- sign column width moving about between updates.
bufnr = bufnr or current_buf()
dprint('Detached')
local bcache = cache[bufnr]
if not bcache then
dprint('Cache was nil')
if #paths == 0 then
return
end
manager.detach(bufnr, _keep_signs)
-- Clear status variables
Status:clear(bufnr)
cache:destroy(bufnr)
end
-- @return (string, string) Tuple of buffer name and commit
local function parse_fugitive_uri(name)
if vim.fn.exists('*FugitiveReal') == 0 then
dprint("Fugitive not installed")
return
end
local path = vim.fn.FugitiveReal(name)
local commit = vim.fn.FugitiveParse(name)[1]:match('([^:]+):.*')
if commit == '0' then
-- '0' means the index so clear commit so we attach normally
commit = nil
end
return path, commit
end
local function parse_gitsigns_uri(name)
-- TODO(lewis6991): Support submodules
local _, _, root_path, commit, rel_path =
name:find([[^gitsigns://(.*)/%.git/(.*):(.*)]])
if commit == ':0' then
-- ':0' means the index so clear commit so we attach normally
commit = nil
end
if root_path then
name = root_path .. '/' .. rel_path
end
return name, commit
end
local function get_buf_path(bufnr)
local file =
uv.fs_realpath(api.nvim_buf_get_name(bufnr)) or
api.nvim_buf_call(bufnr, function()
return vim.fn.expand('%:p')
end)
if not vim.wo.diff then
if vim.startswith(file, 'fugitive://') then
local path, commit = parse_fugitive_uri(file)
dprintf("Fugitive buffer for file '%s' from path '%s'", path, file)
path = uv.fs_realpath(path)
if path then
return path, commit
end
end
if vim.startswith(file, 'gitsigns://') then
local path, commit = parse_gitsigns_uri(file)
dprintf("Gitsigns buffer for file '%s' from path '%s'", path, file)
path = uv.fs_realpath(path)
if path then
return path, commit
end
end
end
return file
end
local vimgrep_running = false
local function on_lines(_, bufnr, _, first, last_orig, last_new, byte_count)
if first == last_orig and last_orig == last_new and byte_count == 0 then
-- on_lines can be called twice for undo events; ignore the second
-- call which indicates no changes.
return
end
return manager.on_lines(bufnr, first, last_orig, last_new)
end
local function on_reload(_, bufnr)
local __FUNC__ = 'on_reload'
dprint('Reload')
manager.update_debounced(bufnr)
end
local function on_detach(_, bufnr)
M.detach(bufnr, true)
end
local function on_attach_pre(bufnr)
local gitdir, toplevel
if config._on_attach_pre then
local res = async.wrap(config._on_attach_pre, 2)(bufnr)
dprintf('ran on_attach_pre with result %s', vim.inspect(res))
if type(res) == "table" then
if type(res.gitdir) == 'string' then
gitdir = res.gitdir
end
if type(res.toplevel) == 'string' then
toplevel = res.toplevel
end
end
end
return gitdir, toplevel
end
local function try_worktrees(_bufnr, file, encoding)
if not config.worktrees then
return
end
for _, wt in ipairs(config.worktrees) do
local git_obj = git.Obj.new(file, encoding, wt.gitdir, wt.toplevel)
if git_obj and git_obj.object_name then
dprintf('Using worktree %s', vim.inspect(wt))
return git_obj
end
end
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, ctx, aucmd)
local __FUNC__ = 'attach'
if vimgrep_running then
dprint('attaching is disabled')
return
end
if cache[cbuf] then
dprint('Already attached')
return
end
if aucmd then
dprintf('Attaching (trigger=%s)', aucmd)
if cwd_watcher then
cwd_watcher:stop()
else
dprint('Attaching')
cwd_watcher = uv.new_fs_poll(true)
end
if not api.nvim_buf_is_loaded(cbuf) then
dprint('Non-loaded buffer')
return
local cwd = vim.loop.cwd()
local gitdir, head
local gs_cache = require('gitsigns.cache')
-- Look in the cache first
for _, bcache in pairs(gs_cache.cache) do
local repo = bcache.git_obj.repo
if repo.toplevel == cwd then
head = repo.abbrev_head
gitdir = repo.gitdir
break
end
end
local encoding = vim.bo[cbuf].fileencoding
if encoding == '' then
encoding = 'utf-8'
local git = require('gitsigns.git')
if not head or not gitdir then
local info = git.get_repo_info(cwd)
gitdir = info.gitdir
head = info.abbrev_head
end
local file
local commit
local gitdir_oap
local toplevel_oap
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 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 git_obj = git.Obj.new(file, encoding, gitdir_oap, toplevel_oap)
if not git_obj and not ctx then
git_obj = try_worktrees(cbuf, file, encoding)
scheduler()
end
if not git_obj then
dprint('Empty git obj')
return
end
local repo = git_obj.repo
scheduler()
Status:update(cbuf, {
head = repo.abbrev_head,
root = repo.toplevel,
gitdir = repo.gitdir,
})
vim.g.gitsigns_head = head
if vim.startswith(file, repo.gitdir .. util.path_sep) then
dprint('In non-standard git dir')
if not gitdir then
return
end
if not ctx and (not util.path_exists(file) or uv.fs_stat(file).type == 'directory') then
dprint('Not a file')
local towatch = gitdir .. '/HEAD'
if cwd_watcher:getpath() == towatch then
-- Already watching
return
end
if not git_obj.relpath then
dprint('Cannot resolve file in repo')
-- 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
dprintf('Git dir update error: %s', err)
return
end
dprint('Git cwd dir update')
if not config.attach_to_untracked and git_obj.object_name == nil then
dprint('File is untracked')
return
end
-- On windows os.tmpname() crashes in callback threads so initialise this
-- variable on the main thread.
local new_head = git.get_repo_info(cwd).abbrev_head
scheduler()
vim.g.gitsigns_head = new_head
end))
if config.on_attach and config.on_attach(cbuf) == false then
dprint('User on_attach() returned false')
return
end
cache[cbuf] = CacheEntry.new({
base = ctx and ctx.base or config.base,
file = file,
commit = commit,
gitdir_watcher = manager.watch_gitdir(cbuf, repo.gitdir),
git_obj = git_obj,
})
if not api.nvim_buf_is_loaded(cbuf) then
dprint('Un-loaded buffer')
return
end
-- Make sure to attach before the first update (which is async) so we pick up
-- changes from BufReadCmd.
api.nvim_buf_attach(cbuf, false, {
on_lines = on_lines,
on_reload = on_reload,
on_detach = on_detach,
})
-- 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)
--- Attach Gitsigns to the buffer.
---
--- Attributes: ~
--- {async}
---
--- 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.
M.attach = void(function(bufnr, ctx, _trigger)
attach_throttled(bufnr or current_buf(), ctx, _trigger)
end)
local function setup_cli()
local funcs = M
api.nvim_create_user_command('Gitsigns', function(params)
require('gitsigns.cli').run(funcs, params)
require('gitsigns.cli').run(params)
end, {
force = true,
nargs = '*',
range = true,
complete = function(arglead, line)
return require('gitsigns.cli').complete(funcs, arglead, line)
return require('gitsigns.cli').complete(arglead, line)
end, })
end
local function wrap_func(fn, ...)
local args = { ... }
local nargs = select('#', ...)
return function()
fn(unpack(args, 1, nargs))
local exported = {
'attach',
'actions',
}
local function setup_debug()
log.debug_mode = config.debug_mode
log.verbose = config._verbose
if config.debug_mode then
exported[#exported + 1] = 'debug'
end
end
local function autocmd(event, opts)
local opts0 = {}
if type(opts) == "function" then
opts0.callback = wrap_func(opts)
else
opts0 = opts
local function setup_attach()
scheduler()
-- Attach to all open buffers
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, nil, 'setup')
scheduler()
end
opts0.group = 'gitsigns'
api.nvim_create_autocmd(event, opts0)
end
local function on_or_after_vimenter(fn)
if vim.v.vim_did_enter == 1 then
fn()
else
api.nvim_create_autocmd('VimEnter', {
callback = wrap_func(fn),
once = true,
api.nvim_create_autocmd({ 'BufRead', 'BufNewFile', 'BufWritePost' }, {
group = 'gitsigns',
callback = function(data)
M.attach(nil, nil, data.event)
end,
})
end
local function setup_cwd_head()
scheduler()
update_cwd_head()
-- Need to debounce in case some plugin changes the cwd too often
-- (like vim-grepper)
api.nvim_create_autocmd('DirChanged', {
group = 'gitsigns',
callback = function()
local debounce = require("gitsigns.debounce").debounce_trailing
debounce(100, update_cwd_head)
end,
})
end
--- Setup and start Gitsigns.
@ -417,87 +171,36 @@ M.setup = void(function(cfg)
print('gitsigns: git not in path. Aborting setup')
return
end
if config.yadm.enable and vim.fn.executable('yadm') == 0 then
print("gitsigns: yadm not in path. Ignoring 'yadm.enable' in config")
config.yadm.enable = false
return
end
gs_debug.debug_mode = config.debug_mode
gs_debug.verbose = config._verbose
if config.debug_mode then
for nm, f in pairs(gs_debug.add_debug_functions(cache)) do
(M)[nm] = f
end
end
manager.setup()
Status.formatter = config.status_formatter
-- Make sure highlights are setup on or after VimEnter so the colorscheme is
-- loaded. Do not set them up with vim.schedule as this removes the intro
-- message.
on_or_after_vimenter(hl.setup_highlights)
setup_debug()
setup_cli()
git.enable_yadm = config.yadm.enable
git.set_version(config._git_version)
scheduler()
-- Attach to all open buffers
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, nil, 'setup')
scheduler()
end
end
api.nvim_create_augroup('gitsigns', {})
autocmd('VimLeavePre', M.detach_all)
autocmd('ColorScheme', hl.setup_highlights)
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'))
if config._test_mode then
require('gitsigns.attach')._setup()
require('gitsigns.git')._set_version(config._git_version)
end
autocmd('OptionSet', {
pattern = 'fileformat',
callback = function()
require('gitsigns.actions').refresh()
end, })
setup_attach()
setup_cwd_head()
-- vimpgrep creates and deletes lots of buffers so attaching to each one will
-- waste lots of resource and even slow down vimgrep.
autocmd('QuickFixCmdPre', {
pattern = '*vimgrep*',
callback = function()
vimgrep_running = true
end,
})
autocmd('QuickFixCmdPost', {
pattern = '*vimgrep*',
callback = function()
vimgrep_running = false
end,
})
require('gitsigns.current_line_blame').setup()
scheduler()
manager.update_cwd_head()
-- Need to debounce in case some plugin changes the cwd too often
-- (like vim-grepper)
autocmd('DirChanged', debounce_trailing(100, manager.update_cwd_head))
M._setup_done = true
end)
return setmetatable(M, {
__index = function(_, f)
return (require('gitsigns.actions'))[f]
for _, mod in ipairs(exported) do
local m = (require)('gitsigns.' .. mod)
if m[f] then
return m[f]
end
end
end,
})

411
lua/gitsigns/attach.lua generated Normal file
View File

@ -0,0 +1,411 @@
local async = require('gitsigns.async')
local git = require('gitsigns.git')
local log = require("gitsigns.debug.log")
local dprintf = log.dprintf
local dprint = log.dprint
local manager = require('gitsigns.manager')
local hl = require('gitsigns.highlight')
local gs_cache = require('gitsigns.cache')
local cache = gs_cache.cache
local CacheEntry = gs_cache.CacheEntry
local Status = require("gitsigns.status")
local gs_config = require('gitsigns.config')
local config = gs_config.config
local void = require('gitsigns.async').void
local util = require('gitsigns.util')
local throttle_by_id = require("gitsigns.debounce").throttle_by_id
local api = vim.api
local uv = vim.loop
local M = {}
local vimgrep_running = false
-- @return (string, string) Tuple of buffer name and commit
local function parse_fugitive_uri(name)
if vim.fn.exists('*FugitiveReal') == 0 then
dprint("Fugitive not installed")
return
end
local path = vim.fn.FugitiveReal(name)
local commit = vim.fn.FugitiveParse(name)[1]:match('([^:]+):.*')
if commit == '0' then
-- '0' means the index so clear commit so we attach normally
commit = nil
end
return path, commit
end
local function parse_gitsigns_uri(name)
-- TODO(lewis6991): Support submodules
local _, _, root_path, commit, rel_path =
name:find([[^gitsigns://(.*)/%.git/(.*):(.*)]])
if commit == ':0' then
-- ':0' means the index so clear commit so we attach normally
commit = nil
end
if root_path then
name = root_path .. '/' .. rel_path
end
return name, commit
end
local function get_buf_path(bufnr)
local file =
uv.fs_realpath(api.nvim_buf_get_name(bufnr)) or
api.nvim_buf_call(bufnr, function()
return vim.fn.expand('%:p')
end)
if not vim.wo.diff then
if vim.startswith(file, 'fugitive://') then
local path, commit = parse_fugitive_uri(file)
dprintf("Fugitive buffer for file '%s' from path '%s'", path, file)
path = uv.fs_realpath(path)
if path then
return path, commit
end
end
if vim.startswith(file, 'gitsigns://') then
local path, commit = parse_gitsigns_uri(file)
dprintf("Gitsigns buffer for file '%s' from path '%s'", path, file)
path = uv.fs_realpath(path)
if path then
return path, commit
end
end
end
return file
end
local function on_lines(_, bufnr, _, first, last_orig, last_new, byte_count)
if first == last_orig and last_orig == last_new and byte_count == 0 then
-- on_lines can be called twice for undo events; ignore the second
-- call which indicates no changes.
return
end
return manager.on_lines(bufnr, first, last_orig, last_new)
end
local function on_reload(_, bufnr)
local __FUNC__ = 'on_reload'
dprint('Reload')
manager.update_debounced(bufnr)
end
local function on_detach(_, bufnr)
M.detach(bufnr, true)
end
local function on_attach_pre(bufnr)
local gitdir, toplevel
if config._on_attach_pre then
local res = async.wrap(config._on_attach_pre, 2)(bufnr)
dprintf('ran on_attach_pre with result %s', vim.inspect(res))
if type(res) == "table" then
if type(res.gitdir) == 'string' then
gitdir = res.gitdir
end
if type(res.toplevel) == 'string' then
toplevel = res.toplevel
end
end
end
return gitdir, toplevel
end
local function try_worktrees(_bufnr, file, encoding)
if not config.worktrees then
return
end
for _, wt in ipairs(config.worktrees) do
local git_obj = git.Obj.new(file, encoding, wt.gitdir, wt.toplevel)
if git_obj and git_obj.object_name then
dprintf('Using worktree %s', vim.inspect(wt))
return git_obj
end
end
end
local done_setup = false
function M._setup()
if done_setup then
return
end
done_setup = true
manager.setup()
hl.setup_highlights()
api.nvim_create_autocmd('ColorScheme', {
group = 'gitsigns',
callback = hl.setup_highlights,
})
api.nvim_create_autocmd('OptionSet', {
group = 'gitsigns',
pattern = 'fileformat',
callback = function()
require('gitsigns.actions').refresh()
end, })
-- vimpgrep creates and deletes lots of buffers so attaching to each one will
-- waste lots of resource and even slow down vimgrep.
api.nvim_create_autocmd('QuickFixCmdPre', {
group = 'gitsigns',
pattern = '*vimgrep*',
callback = function()
vimgrep_running = true
end,
})
api.nvim_create_autocmd('QuickFixCmdPost', {
group = 'gitsigns',
pattern = '*vimgrep*',
callback = function()
vimgrep_running = false
end,
})
require('gitsigns.current_line_blame').setup()
api.nvim_create_autocmd('VimLeavePre', {
group = 'gitsigns',
callback = M.detach_all,
})
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, ctx, aucmd)
local __FUNC__ = 'attach'
M._setup()
if vimgrep_running then
dprint('attaching is disabled')
return
end
if cache[cbuf] then
dprint('Already attached')
return
end
if aucmd then
dprintf('Attaching (trigger=%s)', aucmd)
else
dprint('Attaching')
end
if not api.nvim_buf_is_loaded(cbuf) then
dprint('Non-loaded buffer')
return
end
local encoding = vim.bo[cbuf].fileencoding
if encoding == '' then
encoding = 'utf-8'
end
local file
local commit
local gitdir_oap
local toplevel_oap
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 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 git_obj = git.Obj.new(file, encoding, gitdir_oap, toplevel_oap)
if not git_obj and not ctx then
git_obj = try_worktrees(cbuf, file, encoding)
async.scheduler()
end
if not git_obj then
dprint('Empty git obj')
return
end
local repo = git_obj.repo
async.scheduler()
Status:update(cbuf, {
head = repo.abbrev_head,
root = repo.toplevel,
gitdir = repo.gitdir,
})
if vim.startswith(file, repo.gitdir .. util.path_sep) then
dprint('In non-standard git dir')
return
end
if not ctx and (not util.path_exists(file) or uv.fs_stat(file).type == 'directory') then
dprint('Not a file')
return
end
if not git_obj.relpath then
dprint('Cannot resolve file in repo')
return
end
if not config.attach_to_untracked and git_obj.object_name == nil then
dprint('File is untracked')
return
end
-- On windows os.tmpname() crashes in callback threads so initialise this
-- variable on the main thread.
async.scheduler()
if config.on_attach and config.on_attach(cbuf) == false then
dprint('User on_attach() returned false')
return
end
cache[cbuf] = CacheEntry.new({
base = ctx and ctx.base or config.base,
file = file,
commit = commit,
gitdir_watcher = manager.watch_gitdir(cbuf, repo.gitdir),
git_obj = git_obj,
})
if not api.nvim_buf_is_loaded(cbuf) then
dprint('Un-loaded buffer')
return
end
-- Make sure to attach before the first update (which is async) so we pick up
-- changes from BufReadCmd.
api.nvim_buf_attach(cbuf, false, {
on_lines = on_lines,
on_reload = on_reload,
on_detach = on_detach,
})
-- 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.
function M.detach_all()
for k, _ in pairs(cache) do
M.detach(k)
end
end
--- Detach Gitsigns from the buffer {bufnr}. If {bufnr} is not
--- provided then the current buffer is used.
---
--- Parameters: ~
--- {bufnr} (number): Buffer number
function M.detach(bufnr, _keep_signs)
-- When this is called interactively (with no arguments) we want to remove all
-- the signs, however if called via a detach event (due to nvim_buf_attach)
-- then we don't want to clear the signs in case the buffer is just being
-- updated due to the file externally changing. When this happens a detach and
-- attach event happen in sequence and so we keep the old signs to stop the
-- sign column width moving about between updates.
bufnr = bufnr or api.nvim_get_current_buf()
dprint('Detached')
local bcache = cache[bufnr]
if not bcache then
dprint('Cache was nil')
return
end
manager.detach(bufnr, _keep_signs)
-- Clear status variables
Status:clear(bufnr)
cache:destroy(bufnr)
end
--- Attach Gitsigns to the buffer.
---
--- Attributes: ~
--- {async}
---
--- 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.
M.attach = void(function(bufnr, ctx, _trigger)
attach_throttled(bufnr or api.nvim_get_current_buf(), ctx, _trigger)
end)
return M

39
lua/gitsigns/cli.lua generated
View File

@ -1,12 +1,22 @@
local async = require('gitsigns.async')
local void = require('gitsigns.async').void
local gs_debug = require("gitsigns.debug")
local dprintf = gs_debug.dprintf
local log = require('gitsigns.debug.log')
local dprintf = log.dprintf
local message = require('gitsigns.message')
local parse_args = require('gitsigns.cli.argparse').parse_args
local actions = require('gitsigns.actions')
local attach = require('gitsigns.attach')
local gs_debug = require('gitsigns.debug')
local sources = {
[actions] = true,
[attach] = false,
[gs_debug] = false,
}
-- try to parse each argument as a lua boolean, nil or number, if fails then
-- keep argument as a string:
--
@ -29,14 +39,13 @@ local M = {}
function M.complete(funcs, arglead, line)
function M.complete(arglead, line)
local words = vim.split(line, '%s+')
local n = #words
local actions = require('gitsigns.actions')
local matches = {}
if n == 2 then
for _, m in ipairs({ actions, funcs }) do
for m, _ in pairs(sources) do
for func, _ in pairs(m) do
if not func:match('^[a-z]') then
-- exclude
@ -55,22 +64,20 @@ function M.complete(funcs, arglead, line)
return matches
end
M.run = void(function(funcs, params)
M.run = void(function(params)
local __FUNC__ = 'cli.run'
local pos_args_raw, named_args_raw = parse_args(params.args)
local func = pos_args_raw[1]
if not func then
func = async.wrap(vim.ui.select, 3)(M.complete(funcs, '', 'Gitsigns '), {})
func = async.wrap(vim.ui.select, 3)(M.complete('', 'Gitsigns '), {})
end
local pos_args = vim.tbl_map(parse_to_lua, vim.list_slice(pos_args_raw, 2))
local named_args = vim.tbl_map(parse_to_lua, named_args_raw)
local args = vim.tbl_extend('error', pos_args, named_args)
local actions = require('gitsigns.actions')
local actions0 = actions
dprintf("Running action '%s' with arguments %s", func, vim.inspect(args, { newline = ' ', indent = '' }))
local cmd_func = actions._get_cmd_func(func)
@ -81,15 +88,13 @@ M.run = void(function(funcs, params)
return
end
if type(actions0[func]) == 'function' then
actions0[func](unpack(pos_args), named_args)
for m, has_named in pairs(sources) do
local f = (m)[func]
if type(f) == "function" then
-- Note functions here do not have named arguments
f(unpack(pos_args), has_named and named_args or nil)
return
end
if type(funcs[func]) == 'function' then
-- Note functions here do not have named arguments
funcs[func](unpack(pos_args))
return
end
message.error('%s is not a valid function or action', func)

View File

@ -135,6 +135,7 @@ local M = {Config = {DiffOpts = {}, SignConfig = {}, watch_gitdir = {}, current_
M.config = {}
M.schema = {
@ -712,6 +713,11 @@ M.schema = {
]],
},
_test_mode = {
type = 'boolean',
default = false,
},
word_diff = {
type = 'boolean',
default = false,

127
lua/gitsigns/debug.lua generated
View File

@ -1,110 +1,6 @@
local M = {
debug_mode = false,
verbose = false,
messages = {},
}
local log = require('gitsigns.debug.log')
local function getvarvalue(name, lvl)
lvl = lvl + 1
local value
local found
-- try local variables
local i = 1
while true do
local n, v = debug.getlocal(lvl, i)
if not n then break end
if n == name then
value = v
found = true
end
i = i + 1
end
if found then return value end
-- try upvalues
local func = debug.getinfo(lvl).func
i = 1
while true do
local n, v = debug.getupvalue(func, i)
if not n then break end
if n == name then return v end
i = i + 1
end
-- not found; get global
return getfenv(func)[name]
end
local function get_context(lvl)
lvl = lvl + 1
local ret = {}
ret.name = getvarvalue('__FUNC__', lvl)
if not ret.name then
local name0 = debug.getinfo(lvl, 'n').name or ''
ret.name = name0:gsub('(.*)%d+$', '%1')
end
ret.bufnr = getvarvalue('bufnr', lvl) or
getvarvalue('_bufnr', lvl) or
getvarvalue('cbuf', lvl) or
getvarvalue('buf', lvl)
return ret
end
-- If called in a callback then make sure the callback defines a __FUNC__
-- variable which can be used to identify the name of the function.
local function cprint(obj, lvl)
lvl = lvl + 1
local msg = type(obj) == "string" and obj or vim.inspect(obj)
local ctx = get_context(lvl)
local msg2
if ctx.bufnr then
msg2 = string.format('%s(%s): %s', ctx.name, ctx.bufnr, msg)
else
msg2 = string.format('%s: %s', ctx.name, msg)
end
table.insert(M.messages, msg2)
end
function M.dprint(obj)
if not M.debug_mode then return end
cprint(obj, 2)
end
function M.dprintf(obj, ...)
if not M.debug_mode then return end
cprint(obj:format(...), 2)
end
function M.vprint(obj)
if not (M.debug_mode and M.verbose) then return end
cprint(obj, 2)
end
function M.vprintf(obj, ...)
if not (M.debug_mode and M.verbose) then return end
cprint(obj:format(...), 2)
end
local function eprint(msg, level)
local info = debug.getinfo(level + 2, 'Sl')
if info then
msg = string.format('(ERROR) %s(%d): %s', info.short_src, info.currentline, msg)
end
M.messages[#M.messages + 1] = msg
if M.debug_mode then
error(msg)
end
end
function M.eprint(msg)
eprint(msg, 1)
end
function M.eprintf(fmt, ...)
eprint(fmt:format(...), 1)
end
local M = {}
local function process(raw_item, path)
if path[#path] == vim.inspect.METATABLE then
@ -123,28 +19,25 @@ local function process(raw_item, path)
return raw_item
end
function M.add_debug_functions(cache)
local R = {}
R.dump_cache = function()
function M.dump_cache()
-- TODO(lewis6991): hack: use package.loaded to avoid circular deps
local cache = (package.loaded['gitsigns.cache']).cache
local text = vim.inspect(cache, { process = process })
vim.api.nvim_echo({ { text } }, false, {})
return cache
end
R.debug_messages = function(noecho)
function M.debug_messages(noecho)
if not noecho then
for _, m in ipairs(M.messages) do
for _, m in ipairs(log.messages) do
vim.api.nvim_echo({ { m } }, false, {})
end
end
return M.messages
return log.messages
end
R.clear_debug = function()
M.messages = {}
end
return R
function M.clear_debug()
log.messages = {}
end
return M

109
lua/gitsigns/debug/log.lua generated Normal file
View File

@ -0,0 +1,109 @@
local M = {
debug_mode = false,
verbose = false,
messages = {},
}
local function getvarvalue(name, lvl)
lvl = lvl + 1
local value
local found
-- try local variables
local i = 1
while true do
local n, v = debug.getlocal(lvl, i)
if not n then break end
if n == name then
value = v
found = true
end
i = i + 1
end
if found then return value end
-- try upvalues
local func = debug.getinfo(lvl).func
i = 1
while true do
local n, v = debug.getupvalue(func, i)
if not n then break end
if n == name then return v end
i = i + 1
end
-- not found; get global
return getfenv(func)[name]
end
local function get_context(lvl)
lvl = lvl + 1
local ret = {}
ret.name = getvarvalue('__FUNC__', lvl)
if not ret.name then
local name0 = debug.getinfo(lvl, 'n').name or ''
ret.name = name0:gsub('(.*)%d+$', '%1')
end
ret.bufnr = getvarvalue('bufnr', lvl) or
getvarvalue('_bufnr', lvl) or
getvarvalue('cbuf', lvl) or
getvarvalue('buf', lvl)
return ret
end
-- If called in a callback then make sure the callback defines a __FUNC__
-- variable which can be used to identify the name of the function.
local function cprint(obj, lvl)
lvl = lvl + 1
local msg = type(obj) == "string" and obj or vim.inspect(obj)
local ctx = get_context(lvl)
local msg2
if ctx.bufnr then
msg2 = string.format('%s(%s): %s', ctx.name, ctx.bufnr, msg)
else
msg2 = string.format('%s: %s', ctx.name, msg)
end
table.insert(M.messages, msg2)
end
function M.dprint(obj)
if not M.debug_mode then return end
cprint(obj, 2)
end
function M.dprintf(obj, ...)
if not M.debug_mode then return end
cprint(obj:format(...), 2)
end
function M.vprint(obj)
if not (M.debug_mode and M.verbose) then return end
cprint(obj, 2)
end
function M.vprintf(obj, ...)
if not (M.debug_mode and M.verbose) then return end
cprint(obj:format(...), 2)
end
local function eprint(msg, level)
local info = debug.getinfo(level + 2, 'Sl')
if info then
msg = string.format('(ERROR) %s(%d): %s', info.short_src, info.currentline, msg)
end
M.messages[#M.messages + 1] = msg
if M.debug_mode then
error(msg)
end
end
function M.eprint(msg)
eprint(msg, 1)
end
function M.eprintf(fmt, ...)
eprint(fmt:format(...), 1)
end
return M

73
lua/gitsigns/git.lua generated
View File

@ -1,18 +1,21 @@
local async = require('gitsigns.async')
local scheduler = require('gitsigns.async').scheduler
local gsd = require("gitsigns.debug")
local log = require("gitsigns.debug.log")
local util = require('gitsigns.util')
local subprocess = require('gitsigns.subprocess')
local gs_config = require('gitsigns.config')
local config = gs_config.config
local gs_hunks = require("gitsigns.hunks")
local Hunk = gs_hunks.Hunk
local uv = vim.loop
local startswith = vim.startswith
local dprint = require("gitsigns.debug").dprint
local eprint = require("gitsigns.debug").eprint
local dprint = require('gitsigns.debug.log').dprint
local eprint = require('gitsigns.debug.log').eprint
local err = require('gitsigns.message').error
@ -78,10 +81,6 @@ local M = {BlameInfo = {}, Version = {}, RepoInfo = {}, Repo = {}, FileProps = {
@ -167,8 +166,35 @@ local function check_version(version)
return true
end
--- @async
function M._set_version(version)
if version ~= 'auto' then
M.version = parse_version(version)
return
end
local _, _, stdout, stderr = async.wait(2, subprocess.run_job, {
command = 'git', args = { '--version' },
})
local line = vim.split(stdout or '', '\n', true)[1]
if not line then
err("Unable to detect git version as 'git --version' failed to return anything")
eprint(stderr)
return
end
assert(type(line) == 'string', 'Unexpected output: ' .. line)
assert(startswith(line, 'git version'), 'Unexpected output: ' .. line)
local parts = vim.split(line, '%s+')
M.version = parse_version(parts[3])
end
--- @async
local git_command = async.create(function(args, spec)
if not M.version then
M._set_version(config._git_version)
end
spec = spec or {}
spec.command = spec.command or 'git'
spec.args = spec.command == 'git' and {
@ -187,7 +213,7 @@ local git_command = async.create(function(args, spec)
if not spec.suppress_stderr then
if stderr then
local cmd_str = table.concat({ spec.command, unpack(args) }, ' ')
gsd.eprintf("Recieved stderr when running command\n'%s':\n%s", cmd_str, stderr)
log.eprintf("Recieved stderr when running command\n'%s':\n%s", cmd_str, stderr)
end
end
@ -199,10 +225,10 @@ local git_command = async.create(function(args, spec)
stdout_lines[#stdout_lines] = nil
end
if gsd.verbose then
gsd.vprintf('%d lines:', #stdout_lines)
if log.verbose then
log.vprintf('%d lines:', #stdout_lines)
for i = 1, math.min(10, #stdout_lines) do
gsd.vprintf('\t%s', stdout_lines[i])
log.vprintf('\t%s', stdout_lines[i])
end
end
@ -235,7 +261,7 @@ local function process_abbrev_head(gitdir, head_str, path, cmd)
suppress_stderr = true,
cwd = path,
})[1] or ''
if gsd.debug_mode and short_sha ~= '' then
if log.debug_mode and short_sha ~= '' then
short_sha = 'HEAD'
end
if util.path_exists(gitdir .. '/rebase-merge') or
@ -310,25 +336,6 @@ function M.get_repo_info(path, cmd, gitdir, toplevel)
return ret
end
--- @async
function M.set_version(version)
if version ~= 'auto' then
M.version = parse_version(version)
return
end
local results, stderr = git_command({ '--version' })
local line = results[1]
if not line then
err("Unable to detect git version as 'git --version' failed to return anything")
eprint(stderr)
return
end
assert(type(line) == 'string', 'Unexpected output: ' .. line)
assert(startswith(line, 'git version'), 'Unexpected output: ' .. line)
local parts = vim.split(line, '%s+')
M.version = parse_version(parts[3])
end
--------------------------------------------------------------------------------
-- Git repo object methods
--------------------------------------------------------------------------------
@ -444,7 +451,7 @@ function Repo.new(dir, gitdir, toplevel)
end
-- Try yadm
if M.enable_yadm and not self.gitdir then
if config.yadm.enable and not self.gitdir then
if vim.startswith(dir, os.getenv('HOME')) and
#git_command({ 'ls-files', dir }, { command = 'yadm' }) ~= 0 then
M.get_repo_info(dir, 'yadm', gitdir, toplevel)
@ -501,7 +508,7 @@ function Obj:file_info(file, silent)
-- Suppress_stderr for the cases when we run:
-- git ls-files --others exists/nonexist
if not stderr:match('^warning: could not open directory .*: No such file or directory') then
gsd.eprint(stderr)
log.eprint(stderr)
end
end

View File

@ -168,8 +168,11 @@ local function cmul(x, factor)
return math.floor(math.floor(r * factor) * 2 ^ 16 + math.floor(g * factor) * 2 ^ 8 + math.floor(b * factor))
end
local function dprintf(fmt, ...)
require('gitsigns.debug.log').dprintf(fmt, ...)
end
local function derive(hl, hldef)
local dprintf = require("gitsigns.debug").dprintf
for _, d in ipairs(hldef) do
if is_hl_set(d) then
dprintf('Deriving %s from %s', hl, d)
@ -201,7 +204,6 @@ end
-- Setup a GitSign* highlight by deriving it from other potentially present
-- highlights.
M.setup_highlights = function()
local dprintf = require("gitsigns.debug").dprintf
for _, hlg in ipairs(M.hls) do
for hl, hldef in pairs(hlg) do
if is_hl_set(hl) then

View File

@ -12,15 +12,14 @@ local Status = require("gitsigns.status")
local debounce_trailing = require('gitsigns.debounce').debounce_trailing
local throttle_by_id = require('gitsigns.debounce').throttle_by_id
local gs_debug = require("gitsigns.debug")
local dprint = gs_debug.dprint
local dprintf = gs_debug.dprintf
local eprint = gs_debug.eprint
local log = require('gitsigns.debug.log')
local dprint = log.dprint
local dprintf = log.dprintf
local eprint = log.eprint
local subprocess = require('gitsigns.subprocess')
local util = require('gitsigns.util')
local run_diff = require('gitsigns.diff')
local git = require('gitsigns.git')
local uv = require('gitsigns.uv')
local gs_hunks = require("gitsigns.hunks")
@ -43,7 +42,6 @@ local M = {}
local scheduler_if_buf_valid = awrap(function(buf, cb)
vim.schedule(function()
if vim.api.nvim_buf_is_valid(buf) then
@ -444,67 +442,6 @@ function M.watch_gitdir(bufnr, gitdir)
return w
end
local cwd_watcher
M.update_cwd_head = void(function()
if cwd_watcher then
cwd_watcher:stop()
else
cwd_watcher = uv.new_fs_poll(true)
end
local cwd = vim.loop.cwd()
local gitdir, head
-- Look in the cache first
for _, bcache in pairs(cache) do
local repo = bcache.git_obj.repo
if repo.toplevel == cwd then
head = repo.abbrev_head
gitdir = repo.gitdir
break
end
end
if not head or not gitdir then
local info = git.get_repo_info(cwd)
gitdir = info.gitdir
head = info.abbrev_head
end
scheduler()
vim.g.gitsigns_head = head
if not gitdir then
return
end
local towatch = gitdir .. '/HEAD'
if cwd_watcher:getpath() == towatch then
-- Already watching
return
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
dprintf('Git dir update error: %s', err)
return
end
dprint('Git cwd dir update')
local new_head = git.get_repo_info(cwd).abbrev_head
scheduler()
vim.g.gitsigns_head = new_head
end))
end)
function M.reset_signs()
-- Remove all signs
signs_normal:reset()

View File

@ -1,7 +1,7 @@
local config = require('gitsigns.config').config
local SignsConfig = require('gitsigns.config').Config.SignsConfig
local dprint = require('gitsigns.debug').dprint
local dprint = require('gitsigns.debug.log').dprint
local B = require('gitsigns.signs.base')

View File

@ -1,5 +1,6 @@
local api = vim.api
local StatusObj = {}
@ -11,7 +12,6 @@ local StatusObj = {}
local Status = {
StatusObj = StatusObj,
formatter = nil,
}
function Status:update(bufnr, status)
@ -24,7 +24,10 @@ function Status:update(bufnr, status)
end
vim.b[bufnr].gitsigns_head = status.head or ''
vim.b[bufnr].gitsigns_status_dict = status
vim.b[bufnr].gitsigns_status = self.formatter(status)
local config = require('gitsigns.config').config
vim.b[bufnr].gitsigns_status = config.status_formatter(status)
end
function Status:clear(bufnr)

View File

@ -1,4 +1,4 @@
local gsd = require("gitsigns.debug")
local log = require("gitsigns.debug.log")
local guv = require("gitsigns.uv")
local uv = vim.loop
@ -47,7 +47,7 @@ end
local function handle_reader(pipe, output)
pipe:read_start(function(err, data)
if err then
gsd.eprint(err)
log.eprint(err)
end
if data then
output[#output + 1] = data
@ -59,9 +59,9 @@ end
function M.run_job(obj, callback)
local __FUNC__ = 'run_job'
if gsd.debug_mode then
if log.debug_mode then
local cmd = obj.command .. ' ' .. table.concat(obj.args, ' ')
gsd.dprint(cmd)
log.dprint(cmd)
end
local stdout_data = {}
@ -75,7 +75,7 @@ function M.run_job(obj, callback)
end
local handle, pid
handle, pid = guv.spawn(obj.command, {
handle, pid = vim.loop.spawn(obj.command, {
args = obj.args,
stdio = { stdin, stdout, stderr },
cwd = obj.cwd,

View File

@ -1,405 +1,159 @@
local async = require('gitsigns.async')
local void = require('gitsigns.async').void
local scheduler = require('gitsigns.async').scheduler
local Status = require("gitsigns.status")
local git = require('gitsigns.git')
local manager = require('gitsigns.manager')
local util = require('gitsigns.util')
local hl = require('gitsigns.highlight')
local gs_cache = require('gitsigns.cache')
local cache = gs_cache.cache
local CacheEntry = gs_cache.CacheEntry
local gs_config = require('gitsigns.config')
local Config = gs_config.Config
local config = gs_config.config
local gs_debug = require("gitsigns.debug")
local dprintf = gs_debug.dprintf
local dprint = gs_debug.dprint
local Debounce = require("gitsigns.debounce")
local debounce_trailing = Debounce.debounce_trailing
local throttle_by_id = Debounce.throttle_by_id
local log = require('gitsigns.debug.log')
local dprintf = log.dprintf
local dprint = log.dprint
local api = vim.api
local uv = vim.loop
local current_buf = api.nvim_get_current_buf
local uv = require('gitsigns.uv')
local record M
setup: function(cfg: Config)
detach : function(bufnr: integer, _keep_signs: boolean)
detach_all : function()
attach : function(cbuf: integer, ctx: GitContext, trigger: string)
record GitContext
toplevel: string
gitdir: string
file: string
commit: string
base: string
end
-- from attach.tl
attach: function(cbuf: integer, ctx: table, trigger: string)
_setup_done: boolean
end
local GitContext = M.GitContext
local cwd_watcher: vim.loop.FSPollObj
--- Detach Gitsigns from all buffers it is attached to.
function M.detach_all()
for k, _ in pairs(cache as {integer:CacheEntry}) do
M.detach(k)
end
end
--- Detach Gitsigns from the buffer {bufnr}. If {bufnr} is not
--- provided then the current buffer is used.
---
--- Parameters: ~
--- {bufnr} (number): Buffer number
function M.detach(bufnr: integer, _keep_signs: boolean)
-- When this is called interactively (with no arguments) we want to remove all
-- the signs, however if called via a detach event (due to nvim_buf_attach)
-- then we don't want to clear the signs in case the buffer is just being
-- updated due to the file externally changing. When this happens a detach and
-- attach event happen in sequence and so we keep the old signs to stop the
-- sign column width moving about between updates.
bufnr = bufnr or current_buf()
dprint('Detached')
local bcache = cache[bufnr]
if not bcache then
dprint('Cache was nil')
return
end
manager.detach(bufnr, _keep_signs)
-- Clear status variables
Status:clear(bufnr)
cache:destroy(bufnr)
end
-- @return (string, string) Tuple of buffer name and commit
local function parse_fugitive_uri(name: string): string, string
if vim.fn.exists('*FugitiveReal') == 0 then
dprint("Fugitive not installed")
return
end
local path = vim.fn.FugitiveReal(name)
local commit = vim.fn.FugitiveParse(name)[1]:match('([^:]+):.*')
if commit == '0' then
-- '0' means the index so clear commit so we attach normally
commit = nil
end
return path, commit
end
local function parse_gitsigns_uri(name: string): string, string
-- TODO(lewis6991): Support submodules
local _, _, root_path, commit, rel_path =
name:find([[^gitsigns://(.*)/%.git/(.*):(.*)]])
if commit == ':0' then
-- ':0' means the index so clear commit so we attach normally
commit = nil
end
if root_path then
name = root_path .. '/' .. rel_path
end
return name, commit
end
local function get_buf_path(bufnr: integer): string, string
local file =
uv.fs_realpath(api.nvim_buf_get_name(bufnr))
or
api.nvim_buf_call(bufnr, function(): string
return vim.fn.expand('%:p')
end)
if not vim.wo.diff then
if vim.startswith(file, 'fugitive://') then
local path, commit = parse_fugitive_uri(file)
dprintf("Fugitive buffer for file '%s' from path '%s'", path, file)
path = uv.fs_realpath(path)
if path then
return path, commit
end
end
if vim.startswith(file, 'gitsigns://') then
local path, commit = parse_gitsigns_uri(file)
dprintf("Gitsigns buffer for file '%s' from path '%s'", path, file)
path = uv.fs_realpath(path)
if path then
return path, commit
end
end
end
return file
end
local vimgrep_running = false
local function on_lines(_, bufnr: integer, _, first: integer, last_orig: integer, last_new: integer, byte_count: integer): boolean
if first == last_orig and last_orig == last_new and byte_count == 0 then
-- on_lines can be called twice for undo events; ignore the second
-- call which indicates no changes.
return
end
return manager.on_lines(bufnr, first, last_orig, last_new)
end
local function on_reload(_, bufnr: integer)
local __FUNC__ = 'on_reload'
dprint('Reload')
manager.update_debounced(bufnr)
end
local function on_detach(_, bufnr: integer)
M.detach(bufnr, true)
end
local function on_attach_pre(bufnr: integer): string, string
local gitdir, toplevel: string, string
if config._on_attach_pre then
local res: any = async.wrap(config._on_attach_pre, 2)(bufnr)
dprintf('ran on_attach_pre with result %s', vim.inspect(res))
if res is table then
if type(res.gitdir) == 'string' then
gitdir = res.gitdir as string
end
if type(res.toplevel) == 'string' then
toplevel = res.toplevel as string
end
end
end
return gitdir, toplevel
end
local function try_worktrees(_bufnr: integer, file: string, encoding: string): git.Obj
if not config.worktrees then
return
end
for _, wt in ipairs(config.worktrees) do
local git_obj = git.Obj.new(file, encoding, wt.gitdir, wt.toplevel)
if git_obj and git_obj.object_name then
dprintf('Using worktree %s', vim.inspect(wt))
return git_obj
end
end
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, ctx: GitContext, aucmd: string)
local __FUNC__ = 'attach'
if vimgrep_running then
dprint('attaching is disabled')
return
end
if cache[cbuf] then
dprint('Already attached')
return
end
if aucmd then
dprintf('Attaching (trigger=%s)', aucmd)
else
dprint('Attaching')
end
if not api.nvim_buf_is_loaded(cbuf) then
dprint('Non-loaded buffer')
return
end
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
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 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 git_obj = git.Obj.new(file, encoding, gitdir_oap, toplevel_oap)
if not git_obj and not ctx then
git_obj = try_worktrees(cbuf, file, encoding)
scheduler()
end
if not git_obj then
dprint('Empty git obj')
return
end
local repo = git_obj.repo
scheduler()
Status:update(cbuf, {
head = repo.abbrev_head,
root = repo.toplevel,
gitdir = repo.gitdir,
local update_cwd_head = void(function()
local paths = vim.fs.find('.git', {
limit = 1,
upward = true,
type = 'directory'
})
if vim.startswith(file, repo.gitdir..util.path_sep) then
dprint('In non-standard git dir')
if #paths == 0 then
return
end
if not ctx and (not util.path_exists(file) or uv.fs_stat(file).type == 'directory') then
dprint('Not a file')
return
if cwd_watcher then
cwd_watcher:stop()
else
cwd_watcher = uv.new_fs_poll(true)
end
if not git_obj.relpath then
dprint('Cannot resolve file in repo')
return
local cwd = vim.loop.cwd()
local gitdir, head: string, string
local gs_cache = require('gitsigns.cache')
-- Look in the cache first
for _, bcache in pairs(gs_cache.cache as {number:gs_cache.CacheEntry}) do
local repo = bcache.git_obj.repo
if repo.toplevel == cwd then
head = repo.abbrev_head
gitdir = repo.gitdir
break
end
end
if not config.attach_to_untracked and git_obj.object_name == nil then
dprint('File is untracked')
return
local git = require('gitsigns.git')
if not head or not gitdir then
local info = git.get_repo_info(cwd)
gitdir = info.gitdir
head = info.abbrev_head
end
-- On windows os.tmpname() crashes in callback threads so initialise this
-- variable on the main thread.
scheduler()
vim.g.gitsigns_head = head
if config.on_attach and config.on_attach(cbuf) == false then
dprint('User on_attach() returned false')
if not gitdir then
return
end
cache[cbuf] = CacheEntry.new {
base = ctx and ctx.base or config.base,
file = file,
commit = commit,
gitdir_watcher = manager.watch_gitdir(cbuf, repo.gitdir),
git_obj = git_obj
}
local towatch = gitdir..'/HEAD'
if not api.nvim_buf_is_loaded(cbuf) then
dprint('Un-loaded buffer')
if cwd_watcher:getpath() == towatch then
-- Already watching
return
end
-- Make sure to attach before the first update (which is async) so we pick up
-- changes from BufReadCmd.
api.nvim_buf_attach(cbuf, false, {
on_lines = on_lines,
on_reload = on_reload,
on_detach = on_detach
})
-- Initial update
manager.update(cbuf, cache[cbuf])
if config.keymaps and not vim.tbl_isempty(config.keymaps) then
require('gitsigns.mappings')(config.keymaps as {string:any}, cbuf)
-- Watch .git/HEAD to detect branch changes
cwd_watcher:start(
towatch,
config.watch_gitdir.interval,
void(function(err: string)
local __FUNC__ = 'cwd_watcher_cb'
if err then
dprintf('Git dir update error: %s', err)
return
end
dprint('Git cwd dir update')
local new_head = git.get_repo_info(cwd).abbrev_head
scheduler()
vim.g.gitsigns_head = new_head
end)
--- Attach Gitsigns to the buffer.
---
--- Attributes: ~
--- {async}
---
--- 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.
M.attach = void(function(bufnr: integer, ctx: GitContext, _trigger: string)
attach_throttled(bufnr or current_buf(), ctx, _trigger)
)
end)
local function setup_cli()
local funcs = M as {string:function}
api.nvim_create_user_command('Gitsigns', function(params: api.UserCmdParams)
require'gitsigns.cli'.run(funcs, params)
require'gitsigns.cli'.run(params)
end, {
force = true,
nargs = '*',
range = true,
complete = function(arglead: string, line: string): {string}
return require'gitsigns.cli'.complete(funcs, arglead, line)
return require'gitsigns.cli'.complete(arglead, line)
end})
end
local function wrap_func(fn: function, ...: any): function()
local args = {...}
local nargs = select('#', ...)
return function()
fn(unpack(args, 1, nargs))
local exported = {
'attach',
'actions'
}
local function setup_debug()
log.debug_mode = config.debug_mode
log.verbose = config._verbose
if config.debug_mode then
exported[#exported+1] = 'debug'
end
end
local function autocmd(event: string, opts: function|vim.api.AutoCmdOpts)
local opts0: vim.api.AutoCmdOpts = {}
if opts is function then
opts0.callback = wrap_func(opts)
else
opts0 = opts
local function setup_attach()
scheduler()
-- Attach to all open buffers
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, nil, 'setup')
scheduler()
end
opts0.group = 'gitsigns'
api.nvim_create_autocmd(event, opts0)
end
local function on_or_after_vimenter(fn: function)
if vim.v.vim_did_enter == 1 then
fn()
else
api.nvim_create_autocmd('VimEnter', {
callback = wrap_func(fn),
once = true
api.nvim_create_autocmd({'BufRead', 'BufNewFile', 'BufWritePost'}, {
group = 'gitsigns',
callback = function(data: vim.api.AutoCmdOpts.CallbackData)
M.attach(nil, nil, data.event)
end
})
end
local function setup_cwd_head()
scheduler()
update_cwd_head()
-- Need to debounce in case some plugin changes the cwd too often
-- (like vim-grepper)
api.nvim_create_autocmd('DirChanged', {
group = 'gitsigns',
callback = function()
local debounce = require("gitsigns.debounce").debounce_trailing
debounce(100, update_cwd_head)
end
})
end
--- Setup and start Gitsigns.
@ -417,87 +171,36 @@ M.setup = void(function(cfg: Config)
print('gitsigns: git not in path. Aborting setup')
return
end
if config.yadm.enable and vim.fn.executable('yadm') == 0 then
print("gitsigns: yadm not in path. Ignoring 'yadm.enable' in config")
config.yadm.enable = false
return
end
gs_debug.debug_mode = config.debug_mode
gs_debug.verbose = config._verbose
if config.debug_mode then
for nm, f in pairs(gs_debug.add_debug_functions(cache)) do
(M as {string:function})[nm] = f
end
end
manager.setup()
Status.formatter = config.status_formatter as function(Status.StatusObj): string
-- Make sure highlights are setup on or after VimEnter so the colorscheme is
-- loaded. Do not set them up with vim.schedule as this removes the intro
-- message.
on_or_after_vimenter(hl.setup_highlights)
setup_debug()
setup_cli()
git.enable_yadm = config.yadm.enable
git.set_version(config._git_version)
scheduler()
-- Attach to all open buffers
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, nil, 'setup')
scheduler()
end
end
api.nvim_create_augroup('gitsigns', {})
autocmd('VimLeavePre' , M.detach_all)
autocmd('ColorScheme' , hl.setup_highlights)
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',
callback = function()
require('gitsigns.actions').refresh()
end}
)
-- vimpgrep creates and deletes lots of buffers so attaching to each one will
-- waste lots of resource and even slow down vimgrep.
autocmd('QuickFixCmdPre', {
pattern ='*vimgrep*',
callback = function()
vimgrep_running = true
if config._test_mode then
require'gitsigns.attach'._setup()
require'gitsigns.git'._set_version(config._git_version)
end
})
autocmd('QuickFixCmdPost', {
pattern ='*vimgrep*',
callback = function()
vimgrep_running = false
end
})
setup_attach()
setup_cwd_head()
require('gitsigns.current_line_blame').setup()
scheduler()
manager.update_cwd_head()
-- Need to debounce in case some plugin changes the cwd too often
-- (like vim-grepper)
autocmd('DirChanged', debounce_trailing(100, manager.update_cwd_head))
M._setup_done = true
end)
return setmetatable(M, {
__index = function(_, f: string): any
return (require('gitsigns.actions') as {string:function})[f]
for _, mod in ipairs(exported) do
local m = (require as function)('gitsigns.'..mod) as table
if m[f] then
return m[f]
end
end
end
})

411
teal/gitsigns/attach.tl Normal file
View File

@ -0,0 +1,411 @@
local async = require('gitsigns.async')
local git = require('gitsigns.git')
local log = require("gitsigns.debug.log")
local dprintf = log.dprintf
local dprint = log.dprint
local manager = require('gitsigns.manager')
local hl = require('gitsigns.highlight')
local gs_cache = require('gitsigns.cache')
local cache = gs_cache.cache
local CacheEntry = gs_cache.CacheEntry
local Status = require("gitsigns.status")
local gs_config = require('gitsigns.config')
local config = gs_config.config
local void = require('gitsigns.async').void
local util = require('gitsigns.util')
local throttle_by_id = require("gitsigns.debounce").throttle_by_id
local api = vim.api
local uv = vim.loop
local record GitContext
toplevel: string
gitdir: string
file: string
commit: string
base: string
end
local record M
attach: function(cbuf: integer, ctx: GitContext, trigger: string)
detach: function(bufnr: integer, _keep_signs: boolean)
detach_all: function()
end
local vimgrep_running = false
-- @return (string, string) Tuple of buffer name and commit
local function parse_fugitive_uri(name: string): string, string
if vim.fn.exists('*FugitiveReal') == 0 then
dprint("Fugitive not installed")
return
end
local path = vim.fn.FugitiveReal(name)
local commit = vim.fn.FugitiveParse(name)[1]:match('([^:]+):.*')
if commit == '0' then
-- '0' means the index so clear commit so we attach normally
commit = nil
end
return path, commit
end
local function parse_gitsigns_uri(name: string): string, string
-- TODO(lewis6991): Support submodules
local _, _, root_path, commit, rel_path =
name:find([[^gitsigns://(.*)/%.git/(.*):(.*)]])
if commit == ':0' then
-- ':0' means the index so clear commit so we attach normally
commit = nil
end
if root_path then
name = root_path .. '/' .. rel_path
end
return name, commit
end
local function get_buf_path(bufnr: integer): string, string
local file =
uv.fs_realpath(api.nvim_buf_get_name(bufnr))
or
api.nvim_buf_call(bufnr, function(): string
return vim.fn.expand('%:p')
end)
if not vim.wo.diff then
if vim.startswith(file, 'fugitive://') then
local path, commit = parse_fugitive_uri(file)
dprintf("Fugitive buffer for file '%s' from path '%s'", path, file)
path = uv.fs_realpath(path)
if path then
return path, commit
end
end
if vim.startswith(file, 'gitsigns://') then
local path, commit = parse_gitsigns_uri(file)
dprintf("Gitsigns buffer for file '%s' from path '%s'", path, file)
path = uv.fs_realpath(path)
if path then
return path, commit
end
end
end
return file
end
local function on_lines(_, bufnr: integer, _, first: integer, last_orig: integer, last_new: integer, byte_count: integer): boolean
if first == last_orig and last_orig == last_new and byte_count == 0 then
-- on_lines can be called twice for undo events; ignore the second
-- call which indicates no changes.
return
end
return manager.on_lines(bufnr, first, last_orig, last_new)
end
local function on_reload(_, bufnr: integer)
local __FUNC__ = 'on_reload'
dprint('Reload')
manager.update_debounced(bufnr)
end
local function on_detach(_, bufnr: integer)
M.detach(bufnr, true)
end
local function on_attach_pre(bufnr: integer): string, string
local gitdir, toplevel: string, string
if config._on_attach_pre then
local res: any = async.wrap(config._on_attach_pre, 2)(bufnr)
dprintf('ran on_attach_pre with result %s', vim.inspect(res))
if res is table then
if type(res.gitdir) == 'string' then
gitdir = res.gitdir as string
end
if type(res.toplevel) == 'string' then
toplevel = res.toplevel as string
end
end
end
return gitdir, toplevel
end
local function try_worktrees(_bufnr: integer, file: string, encoding: string): git.Obj
if not config.worktrees then
return
end
for _, wt in ipairs(config.worktrees) do
local git_obj = git.Obj.new(file, encoding, wt.gitdir, wt.toplevel)
if git_obj and git_obj.object_name then
dprintf('Using worktree %s', vim.inspect(wt))
return git_obj
end
end
end
local done_setup = false
function M._setup()
if done_setup then
return
end
done_setup = true
manager.setup()
hl.setup_highlights()
api.nvim_create_autocmd('ColorScheme', {
group = 'gitsigns',
callback = hl.setup_highlights
})
api.nvim_create_autocmd('OptionSet', {
group = 'gitsigns',
pattern = 'fileformat',
callback = function()
require('gitsigns.actions').refresh()
end}
)
-- vimpgrep creates and deletes lots of buffers so attaching to each one will
-- waste lots of resource and even slow down vimgrep.
api.nvim_create_autocmd('QuickFixCmdPre', {
group = 'gitsigns',
pattern ='*vimgrep*',
callback = function()
vimgrep_running = true
end
})
api.nvim_create_autocmd('QuickFixCmdPost', {
group = 'gitsigns',
pattern ='*vimgrep*',
callback = function()
vimgrep_running = false
end
})
require('gitsigns.current_line_blame').setup()
api.nvim_create_autocmd('VimLeavePre' , {
group = 'gitsigns',
callback = M.detach_all
})
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, ctx: GitContext, aucmd: string)
local __FUNC__ = 'attach'
M._setup()
if vimgrep_running then
dprint('attaching is disabled')
return
end
if cache[cbuf] then
dprint('Already attached')
return
end
if aucmd then
dprintf('Attaching (trigger=%s)', aucmd)
else
dprint('Attaching')
end
if not api.nvim_buf_is_loaded(cbuf) then
dprint('Non-loaded buffer')
return
end
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
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 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 git_obj = git.Obj.new(file, encoding, gitdir_oap, toplevel_oap)
if not git_obj and not ctx then
git_obj = try_worktrees(cbuf, file, encoding)
async.scheduler()
end
if not git_obj then
dprint('Empty git obj')
return
end
local repo = git_obj.repo
async.scheduler()
Status:update(cbuf, {
head = repo.abbrev_head,
root = repo.toplevel,
gitdir = repo.gitdir,
})
if vim.startswith(file, repo.gitdir..util.path_sep) then
dprint('In non-standard git dir')
return
end
if not ctx and (not util.path_exists(file) or uv.fs_stat(file).type == 'directory') then
dprint('Not a file')
return
end
if not git_obj.relpath then
dprint('Cannot resolve file in repo')
return
end
if not config.attach_to_untracked and git_obj.object_name == nil then
dprint('File is untracked')
return
end
-- On windows os.tmpname() crashes in callback threads so initialise this
-- variable on the main thread.
async.scheduler()
if config.on_attach and config.on_attach(cbuf) == false then
dprint('User on_attach() returned false')
return
end
cache[cbuf] = CacheEntry.new {
base = ctx and ctx.base or config.base,
file = file,
commit = commit,
gitdir_watcher = manager.watch_gitdir(cbuf, repo.gitdir),
git_obj = git_obj
}
if not api.nvim_buf_is_loaded(cbuf) then
dprint('Un-loaded buffer')
return
end
-- Make sure to attach before the first update (which is async) so we pick up
-- changes from BufReadCmd.
api.nvim_buf_attach(cbuf, false, {
on_lines = on_lines,
on_reload = on_reload,
on_detach = on_detach
})
-- Initial update
manager.update(cbuf, cache[cbuf])
if config.keymaps and not vim.tbl_isempty(config.keymaps) then
require('gitsigns.mappings')(config.keymaps as {string:any}, cbuf)
end
end)
--- Detach Gitsigns from all buffers it is attached to.
function M.detach_all()
for k, _ in pairs(cache as {integer:CacheEntry}) do
M.detach(k)
end
end
--- Detach Gitsigns from the buffer {bufnr}. If {bufnr} is not
--- provided then the current buffer is used.
---
--- Parameters: ~
--- {bufnr} (number): Buffer number
function M.detach(bufnr: integer, _keep_signs: boolean)
-- When this is called interactively (with no arguments) we want to remove all
-- the signs, however if called via a detach event (due to nvim_buf_attach)
-- then we don't want to clear the signs in case the buffer is just being
-- updated due to the file externally changing. When this happens a detach and
-- attach event happen in sequence and so we keep the old signs to stop the
-- sign column width moving about between updates.
bufnr = bufnr or api.nvim_get_current_buf()
dprint('Detached')
local bcache = cache[bufnr]
if not bcache then
dprint('Cache was nil')
return
end
manager.detach(bufnr, _keep_signs)
-- Clear status variables
Status:clear(bufnr)
cache:destroy(bufnr)
end
--- Attach Gitsigns to the buffer.
---
--- Attributes: ~
--- {async}
---
--- 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.
M.attach = void(function(bufnr: integer, ctx: GitContext, _trigger: string)
attach_throttled(bufnr or api.nvim_get_current_buf(), ctx, _trigger)
end)
return M

View File

@ -1,12 +1,22 @@
local async = require('gitsigns.async')
local void = require('gitsigns.async').void
local gs_debug = require("gitsigns.debug")
local dprintf = gs_debug.dprintf
local log = require('gitsigns.debug.log')
local dprintf = log.dprintf
local message = require'gitsigns.message'
local parse_args = require('gitsigns.cli.argparse').parse_args
local actions = require('gitsigns.actions')
local attach = require('gitsigns.attach')
local gs_debug = require('gitsigns.debug')
local sources = {
[actions] = true,
[attach] = false,
[gs_debug] = false
} as {{string:function}:boolean}
-- try to parse each argument as a lua boolean, nil or number, if fails then
-- keep argument as a string:
--
@ -26,18 +36,17 @@ local function parse_to_lua(a: string): any
end
local record M
run : function(funcs: {string:function}, params: vim.api.UserCmdParams)
run: function(params: vim.api.UserCmdParams)
end
function M.complete(funcs: {string:function}, arglead: string, line: string): {string}
function M.complete(arglead: string, line: string): {string}
local words = vim.split(line, '%s+')
local n: integer = #words
local actions = require('gitsigns.actions')
local matches: {string} = {}
if n == 2 then
for _, m in ipairs{actions as {string:function}, funcs} do
for func, _ in pairs(m) do
for m, _ in pairs(sources) do
for func, _ in pairs(m as {string:function}) do
if not func:match('^[a-z]') then
-- exclude
elseif vim.startswith(func, arglead) then
@ -55,22 +64,20 @@ function M.complete(funcs: {string:function}, arglead: string, line: string): {s
return matches
end
M.run = void(function(funcs: {string:function}, params: vim.api.UserCmdParams)
M.run = void(function(params: vim.api.UserCmdParams)
local __FUNC__ = 'cli.run'
local pos_args_raw, named_args_raw = parse_args(params.args)
local func = pos_args_raw[1]
if not func then
func = async.wrap(vim.ui.select, 3)(M.complete(funcs, '', 'Gitsigns '), {})
func = async.wrap(vim.ui.select, 3)(M.complete('', 'Gitsigns '), {})
end
local pos_args = vim.tbl_map(parse_to_lua, vim.list_slice(pos_args_raw, 2)) as {any}
local named_args = vim.tbl_map(parse_to_lua, named_args_raw) as {string:any}
local args = vim.tbl_extend('error', pos_args, named_args)
local actions = require('gitsigns.actions')
local actions0 = actions as {string:function}
dprintf("Running action '%s' with arguments %s", func, vim.inspect(args, {newline=' ', indent=''}))
local cmd_func = actions._get_cmd_func(func)
@ -81,15 +88,13 @@ M.run = void(function(funcs: {string:function}, params: vim.api.UserCmdParams)
return
end
if type(actions0[func]) == 'function' then
actions0[func](unpack(pos_args), named_args)
for m, has_named in pairs(sources) do
local f = (m as {string:any})[func]
if f is function then
-- Note functions here do not have named arguments
f(unpack(pos_args), has_named and named_args or nil)
return
end
if type(funcs[func]) == 'function' then
-- Note functions here do not have named arguments
funcs[func](unpack(pos_args))
return
end
message.error('%s is not a valid function or action', func)

View File

@ -129,6 +129,7 @@ local record M
_git_version: string
_verbose: boolean
_test_mode: boolean
end
schema: {string:SchemaElem}
@ -712,6 +713,11 @@ M.schema = {
]]
},
_test_mode = {
type = 'boolean',
default = false,
},
word_diff = {
type = 'boolean',
default = false,

View File

@ -1,110 +1,6 @@
local M = {
debug_mode = false,
verbose = false,
messages: {string} = {}
}
local log = require'gitsigns.debug.log'
local function getvarvalue(name: string, lvl: integer): any
lvl = lvl + 1
local value: any
local found: boolean
-- try local variables
local i = 1
while true do
local n, v = debug.getlocal(lvl as function, i) as (string, any)
if not n then break end
if n == name then
value = v
found = true
end
i = i + 1
end
if found then return value end
-- try upvalues
local func = debug.getinfo(lvl).func as function
i = 1
while true do
local n, v = debug.getupvalue(func, i) as (string, any)
if not n then break end
if n == name then return v end
i = i + 1
end
-- not found; get global
return getfenv(func)[name]
end
local function get_context(lvl: integer): table
lvl = lvl + 1
local ret: table = {}
ret.name = getvarvalue('__FUNC__', lvl) as string
if not ret.name then
local name0 = debug.getinfo(lvl, 'n').name or ''
ret.name = name0:gsub('(.*)%d+$', '%1')
end
ret.bufnr = getvarvalue('bufnr', lvl)
or getvarvalue('_bufnr', lvl)
or getvarvalue('cbuf', lvl)
or getvarvalue('buf', lvl)
return ret
end
-- If called in a callback then make sure the callback defines a __FUNC__
-- variable which can be used to identify the name of the function.
local function cprint(obj: any, lvl: integer)
lvl = lvl + 1
local msg = obj is string and obj or vim.inspect(obj)
local ctx = get_context(lvl)
local msg2: string
if ctx.bufnr then
msg2 = string.format('%s(%s): %s', ctx.name, ctx.bufnr, msg)
else
msg2 = string.format('%s: %s', ctx.name, msg)
end
table.insert(M.messages, msg2)
end
function M.dprint(obj: any)
if not M.debug_mode then return end
cprint(obj, 2)
end
function M.dprintf(obj: string, ...:any)
if not M.debug_mode then return end
cprint(obj:format(...), 2)
end
function M.vprint(obj: any)
if not (M.debug_mode and M.verbose) then return end
cprint(obj, 2)
end
function M.vprintf(obj: string, ...:any)
if not (M.debug_mode and M.verbose) then return end
cprint(obj:format(...), 2)
end
local function eprint(msg: string, level: integer)
local info = debug.getinfo(level+2, 'Sl')
if info then
msg = string.format('(ERROR) %s(%d): %s', info.short_src, info.currentline, msg)
end
M.messages[#M.messages+1] = msg
if M.debug_mode then
error(msg)
end
end
function M.eprint(msg: string)
eprint(msg, 1)
end
function M.eprintf(fmt: string, ...:any)
eprint(fmt:format(...), 1)
end
local M = {}
local function process(raw_item: any, path: {string}): any
if path[#path] == vim.inspect.METATABLE then
@ -123,28 +19,25 @@ local function process(raw_item: any, path: {string}): any
return raw_item
end
function M.add_debug_functions(cache: any): {string:function}
local R: {string:function} = {}
R.dump_cache = function(): any
function M.dump_cache(): any
-- TODO(lewis6991): hack: use package.loaded to avoid circular deps
local cache = (package.loaded['gitsigns.cache'] as table).cache
local text = vim.inspect(cache, { process = process })
vim.api.nvim_echo({{text}}, false, {})
return cache
end
R.debug_messages = function(noecho: boolean): {string}
function M.debug_messages(noecho: boolean): {string}
if not noecho then
for _, m in ipairs(M.messages) do
for _, m in ipairs(log.messages) do
vim.api.nvim_echo({{m}}, false, {})
end
end
return M.messages
return log.messages
end
R.clear_debug = function()
M.messages = {}
end
return R
function M.clear_debug()
log.messages = {}
end
return M

109
teal/gitsigns/debug/log.tl Normal file
View File

@ -0,0 +1,109 @@
local M = {
debug_mode = false,
verbose = false,
messages: {string} = {},
}
local function getvarvalue(name: string, lvl: integer): any
lvl = lvl + 1
local value: any
local found: boolean
-- try local variables
local i = 1
while true do
local n, v = debug.getlocal(lvl as function, i) as (string, any)
if not n then break end
if n == name then
value = v
found = true
end
i = i + 1
end
if found then return value end
-- try upvalues
local func = debug.getinfo(lvl).func as function
i = 1
while true do
local n, v = debug.getupvalue(func, i) as (string, any)
if not n then break end
if n == name then return v end
i = i + 1
end
-- not found; get global
return getfenv(func)[name]
end
local function get_context(lvl: integer): table
lvl = lvl + 1
local ret: table = {}
ret.name = getvarvalue('__FUNC__', lvl) as string
if not ret.name then
local name0 = debug.getinfo(lvl, 'n').name or ''
ret.name = name0:gsub('(.*)%d+$', '%1')
end
ret.bufnr = getvarvalue('bufnr', lvl)
or getvarvalue('_bufnr', lvl)
or getvarvalue('cbuf', lvl)
or getvarvalue('buf', lvl)
return ret
end
-- If called in a callback then make sure the callback defines a __FUNC__
-- variable which can be used to identify the name of the function.
local function cprint(obj: any, lvl: integer)
lvl = lvl + 1
local msg = obj is string and obj or vim.inspect(obj)
local ctx = get_context(lvl)
local msg2: string
if ctx.bufnr then
msg2 = string.format('%s(%s): %s', ctx.name, ctx.bufnr, msg)
else
msg2 = string.format('%s: %s', ctx.name, msg)
end
table.insert(M.messages, msg2)
end
function M.dprint(obj: any)
if not M.debug_mode then return end
cprint(obj, 2)
end
function M.dprintf(obj: string, ...:any)
if not M.debug_mode then return end
cprint(obj:format(...), 2)
end
function M.vprint(obj: any)
if not (M.debug_mode and M.verbose) then return end
cprint(obj, 2)
end
function M.vprintf(obj: string, ...:any)
if not (M.debug_mode and M.verbose) then return end
cprint(obj:format(...), 2)
end
local function eprint(msg: string, level: integer)
local info = debug.getinfo(level+2, 'Sl')
if info then
msg = string.format('(ERROR) %s(%d): %s', info.short_src, info.currentline, msg)
end
M.messages[#M.messages+1] = msg
if M.debug_mode then
error(msg)
end
end
function M.eprint(msg: string)
eprint(msg, 1)
end
function M.eprintf(fmt: string, ...:any)
eprint(fmt:format(...), 1)
end
return M

View File

@ -1,18 +1,21 @@
local async = require('gitsigns.async')
local scheduler = require('gitsigns.async').scheduler
local gsd = require("gitsigns.debug")
local log = require("gitsigns.debug.log")
local util = require('gitsigns.util')
local subprocess = require('gitsigns.subprocess')
local gs_config = require('gitsigns.config')
local config = gs_config.config
local gs_hunks = require("gitsigns.hunks")
local Hunk = gs_hunks.Hunk
local uv = vim.loop
local startswith = vim.startswith
local dprint = require("gitsigns.debug").dprint
local eprint = require("gitsigns.debug").eprint
local dprint = require('gitsigns.debug.log').dprint
local eprint = require('gitsigns.debug.log').eprint
local err = require('gitsigns.message').error
local record GJobSpec
@ -56,10 +59,6 @@ local record M
end
version: Version
enable_yadm: boolean
set_version: function(string)
record RepoInfo
gitdir: string
toplevel: string
@ -167,8 +166,35 @@ local function check_version(version: {number,number,number}): boolean
return true
end
--- @async
function M._set_version(version: string)
if version ~= 'auto' then
M.version = parse_version(version)
return
end
local _, _, stdout, stderr = async.wait(2, subprocess.run_job, {
command = 'git', args = { '--version' }
})
local line = vim.split(stdout or '', '\n', true)[1]
if not line then
err("Unable to detect git version as 'git --version' failed to return anything")
eprint(stderr)
return
end
assert(type(line) == 'string', 'Unexpected output: '..line)
assert(startswith(line, 'git version'), 'Unexpected output: '..line)
local parts = vim.split(line, '%s+')
M.version = parse_version(parts[3])
end
--- @async
local git_command = async.create(function(args: {string}, spec: GJobSpec): {string}, string
if not M.version then
M._set_version(config._git_version)
end
spec = spec or {}
spec.command = spec.command or 'git'
spec.args = spec.command == 'git' and {
@ -187,7 +213,7 @@ local git_command = async.create(function(args: {string}, spec: GJobSpec): {stri
if not spec.suppress_stderr then
if stderr then
local cmd_str = table.concat({spec.command, unpack(args)}, ' ')
gsd.eprintf("Recieved stderr when running command\n'%s':\n%s", cmd_str, stderr)
log.eprintf("Recieved stderr when running command\n'%s':\n%s", cmd_str, stderr)
end
end
@ -199,10 +225,10 @@ local git_command = async.create(function(args: {string}, spec: GJobSpec): {stri
stdout_lines[#stdout_lines] = nil
end
if gsd.verbose then
gsd.vprintf('%d lines:', #stdout_lines)
if log.verbose then
log.vprintf('%d lines:', #stdout_lines)
for i = 1, math.min(10, #stdout_lines) do
gsd.vprintf('\t%s', stdout_lines[i])
log.vprintf('\t%s', stdout_lines[i])
end
end
@ -235,7 +261,7 @@ local function process_abbrev_head(gitdir: string, head_str: string, path: strin
suppress_stderr = true,
cwd = path,
})[1] or ''
if gsd.debug_mode and short_sha ~= '' then
if log.debug_mode and short_sha ~= '' then
short_sha = 'HEAD'
end
if util.path_exists(gitdir..'/rebase-merge')
@ -310,25 +336,6 @@ function M.get_repo_info(path: string, cmd: string, gitdir: string, toplevel: st
return ret
end
--- @async
function M.set_version(version: string)
if version ~= 'auto' then
M.version = parse_version(version)
return
end
local results, stderr = git_command{'--version'}
local line = results[1]
if not line then
err("Unable to detect git version as 'git --version' failed to return anything")
eprint(stderr)
return
end
assert(type(line) == 'string', 'Unexpected output: '..line)
assert(startswith(line, 'git version'), 'Unexpected output: '..line)
local parts = vim.split(line, '%s+')
M.version = parse_version(parts[3])
end
--------------------------------------------------------------------------------
-- Git repo object methods
--------------------------------------------------------------------------------
@ -444,7 +451,7 @@ function Repo.new(dir: string, gitdir: string, toplevel: string): Repo
end
-- Try yadm
if M.enable_yadm and not self.gitdir then
if config.yadm.enable and not self.gitdir then
if vim.startswith(dir, os.getenv('HOME'))
and #git_command({'ls-files', dir}, {command = 'yadm'}) ~= 0 then
M.get_repo_info(dir, 'yadm', gitdir, toplevel)
@ -501,7 +508,7 @@ function Obj:file_info(file: string, silent: boolean): M.FileProps
-- Suppress_stderr for the cases when we run:
-- git ls-files --others exists/nonexist
if not stderr:match('^warning: could not open directory .*: No such file or directory') then
gsd.eprint(stderr)
log.eprint(stderr)
end
end

View File

@ -168,8 +168,11 @@ local function cmul(x: integer, factor: number): integer
return math.floor(math.floor(r*factor) * 2^16 + math.floor(g*factor) * 2^8 + math.floor(b*factor))
end
local function dprintf(fmt: string, ...:any)
require('gitsigns.debug.log').dprintf(fmt, ...)
end
local function derive(hl: string, hldef: Hldef)
local dprintf = require("gitsigns.debug").dprintf
for _, d in ipairs(hldef) do
if is_hl_set(d) then
dprintf('Deriving %s from %s', hl, d)
@ -201,7 +204,6 @@ end
-- Setup a GitSign* highlight by deriving it from other potentially present
-- highlights.
M.setup_highlights = function()
local dprintf = require("gitsigns.debug").dprintf
for _, hlg in ipairs(M.hls) do
for hl, hldef in pairs(hlg) do
if is_hl_set(hl) then

View File

@ -12,15 +12,14 @@ local Status = require("gitsigns.status")
local debounce_trailing = require('gitsigns.debounce').debounce_trailing
local throttle_by_id = require('gitsigns.debounce').throttle_by_id
local gs_debug = require("gitsigns.debug")
local dprint = gs_debug.dprint
local dprintf = gs_debug.dprintf
local eprint = gs_debug.eprint
local log = require('gitsigns.debug.log')
local dprint = log.dprint
local dprintf = log.dprintf
local eprint = log.eprint
local subprocess = require('gitsigns.subprocess')
local util = require('gitsigns.util')
local run_diff = require('gitsigns.diff')
local git = require('gitsigns.git')
local uv = require('gitsigns.uv')
local gs_hunks = require("gitsigns.hunks")
@ -38,7 +37,6 @@ local record M
update_debounced : function(bufnr: integer, CacheEntry)
on_lines : function(buf: integer, first: integer, last_orig: integer, last_new: integer): boolean
watch_gitdir : function(bufnr: integer, gitdir: string): vim.loop.FSPollObj
update_cwd_head : function()
detach : function(bufnr: integer, keep_signs: boolean)
reset_signs : function()
setup : function()
@ -444,67 +442,6 @@ function M.watch_gitdir(bufnr: integer, gitdir: string): vim.loop.FSPollObj
return w
end
local cwd_watcher: vim.loop.FSPollObj
M.update_cwd_head = void(function()
if cwd_watcher then
cwd_watcher:stop()
else
cwd_watcher = uv.new_fs_poll(true)
end
local cwd = vim.loop.cwd()
local gitdir, head: string, string
-- Look in the cache first
for _, bcache in pairs(cache as {number:CacheEntry}) do
local repo = bcache.git_obj.repo
if repo.toplevel == cwd then
head = repo.abbrev_head
gitdir = repo.gitdir
break
end
end
if not head or not gitdir then
local info = git.get_repo_info(cwd)
gitdir = info.gitdir
head = info.abbrev_head
end
scheduler()
vim.g.gitsigns_head = head
if not gitdir then
return
end
local towatch = gitdir..'/HEAD'
if cwd_watcher:getpath() == towatch then
-- Already watching
return
end
-- Watch .git/HEAD to detect branch changes
cwd_watcher:start(
towatch,
config.watch_gitdir.interval,
void(function(err: string)
local __FUNC__ = 'cwd_watcher_cb'
if err then
dprintf('Git dir update error: %s', err)
return
end
dprint('Git cwd dir update')
local new_head = git.get_repo_info(cwd).abbrev_head
scheduler()
vim.g.gitsigns_head = new_head
end)
)
end)
function M.reset_signs()
-- Remove all signs
signs_normal:reset()

View File

@ -1,7 +1,7 @@
local config = require('gitsigns.config').config
local SignsConfig = require('gitsigns.config').Config.SignsConfig
local dprint = require('gitsigns.debug').dprint
local dprint = require('gitsigns.debug.log').dprint
local B = require('gitsigns.signs.base')

View File

@ -1,5 +1,6 @@
local api = vim.api
local record StatusObj
added : integer
removed : integer
@ -11,7 +12,6 @@ end
local Status = {
StatusObj = StatusObj,
formatter: function(StatusObj): string = nil
}
function Status:update(bufnr: integer, status: StatusObj)
@ -24,7 +24,10 @@ function Status:update(bufnr: integer, status: StatusObj)
end
vim.b[bufnr].gitsigns_head = status.head or ''
vim.b[bufnr].gitsigns_status_dict = status
vim.b[bufnr].gitsigns_status = self.formatter(status)
local config = require('gitsigns.config').config
vim.b[bufnr].gitsigns_status = config.status_formatter(status)
end
function Status:clear(bufnr: integer)

View File

@ -1,4 +1,4 @@
local gsd = require("gitsigns.debug")
local log = require("gitsigns.debug.log")
local guv = require("gitsigns.uv")
local uv = vim.loop
@ -47,7 +47,7 @@ end
local function handle_reader(pipe: uv.Pipe, output: {string})
pipe:read_start(function(err: string, data: string)
if err then
gsd.eprint(err)
log.eprint(err)
end
if data then
output[#output+1] = data
@ -59,9 +59,9 @@ end
function M.run_job(obj: M.JobSpec, callback: function(integer, integer, string, string))
local __FUNC__ = 'run_job'
if gsd.debug_mode then
if log.debug_mode then
local cmd: string = obj.command..' '..table.concat(obj.args, ' ')
gsd.dprint(cmd)
log.dprint(cmd)
end
local stdout_data: {string} = {}
@ -75,7 +75,7 @@ function M.run_job(obj: M.JobSpec, callback: function(integer, integer, string,
end
local handle, pid: uv.Process, integer
handle, pid = guv.spawn(obj.command, {
handle, pid = vim.loop.spawn(obj.command, {
args = obj.args,
stdio = { stdin, stdout, stderr },
cwd = obj.cwd

View File

@ -41,30 +41,26 @@ describe('gitdir_watcher', function()
end)
it('can follow moved files', function()
local screen = Screen.new(20, 17)
screen:attach({ext_messages=false})
setup_test_repo()
setup_gitsigns(test_config)
command('Gitsigns clear_debug')
edit(test_file)
local test_file2 = test_file..'2'
local test_file3 = test_file..'3'
match_debug_messages {
'attach(1): Attaching (trigger=BufRead)',
'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 .* '..helpers.pesc(test_file)),
'watch_gitdir(1): Watching git dir',
p'run_job: git .* show :0:dummy.txt',
'update(1): updates: 1, jobs: 6',
'update(1): updates: 1, jobs: 5',
}
eq({[1] = test_file}, get_bufs())
command('Gitsigns clear_debug')
local test_file2 = test_file..'2'
git{'mv', test_file, test_file2}
match_debug_messages {
@ -76,13 +72,14 @@ describe('gitdir_watcher', function()
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'
'update(1): updates: 2, jobs: 10'
}
eq({[1] = test_file2}, get_bufs())
command('Gitsigns clear_debug')
local test_file3 = test_file..'3'
git{'mv', test_file2, test_file3}
match_debug_messages {
@ -94,7 +91,7 @@ describe('gitdir_watcher', function()
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'
'update(1): updates: 3, jobs: 15'
}
eq({[1] = test_file3}, get_bufs())
@ -113,7 +110,7 @@ describe('gitdir_watcher', function()
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'
'update(1): updates: 4, jobs: 21'
}
eq({[1] = test_file}, get_bufs())

View File

@ -83,18 +83,18 @@ describe('gitsigns', function()
-- Don't set this too low, or else the test will lock up
config.watch_gitdir = {interval = 100}
setup_gitsigns(config)
command('Gitsigns clear_debug')
edit(test_file)
expectf(function()
match_dag(debug_messages(), {
p'run_job: git .* %-%-version',
'attach(1): Attaching (trigger=BufRead)',
'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 %-%-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'
'update(1): updates: 1, jobs: 6'
})
end)
@ -118,7 +118,7 @@ describe('gitsigns', function()
edit(tmpfile)
match_debug_messages {
'attach(1): Attaching (trigger=BufRead)',
'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',
'new: Not in git repo',
@ -167,7 +167,7 @@ describe('gitsigns', function()
edit(scratch..'/.git/index')
match_debug_messages {
'attach(1): Attaching (trigger=BufRead)',
'attach(1): Attaching (trigger=BufReadPost)',
'new: In git dir',
'attach(1): Empty git obj'
}
@ -183,7 +183,7 @@ describe('gitsigns', function()
edit(ignored_file)
match_debug_messages {
'attach(1): Attaching (trigger=BufRead)',
'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 .*/dummy_ignored.txt',
@ -225,7 +225,7 @@ describe('gitsigns', function()
command("Gitsigns clear_debug")
command("copen")
match_debug_messages {
'attach(2): Attaching (trigger=BufRead)',
'attach(2): Attaching (trigger=BufReadPost)',
'attach(2): Non-normal buffer',
}
end)
@ -349,7 +349,7 @@ describe('gitsigns', function()
edit(test_file)
match_debug_messages {
'attach(1): Attaching (trigger=BufRead)',
'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 .* rev%-parse %-%-short HEAD',
@ -484,7 +484,7 @@ describe('gitsigns', function()
table.insert(messages, p'run_job: git .* diff .* /tmp/lua_.* /tmp/lua_.*')
end
local jobs = internal_diff and 9 or 10
local jobs = internal_diff and 8 or 9
table.insert(messages, "update(1): updates: 1, jobs: "..jobs)
match_debug_messages(messages)

View File

@ -21,6 +21,7 @@ M.newfile = M.scratch.."/newfile.txt"
M.test_config = {
debug_mode = true,
_test_mode = true,
signs = {
add = {hl = 'DiffAdd' , text = '+'},
delete = {hl = 'DiffDelete', text = '_'},
@ -47,15 +48,6 @@ 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
@ -64,7 +56,6 @@ function M.cleanup()
system{"rm", "-rf", M.scratch}
end
function M.setup_git()
M.git{"init", '-b', 'master'}
@ -105,7 +96,8 @@ function M.expectf(cond, interval)
local duration = 0
interval = interval or 1
while duration < timeout do
if pcall(cond) then
local ok, ret = pcall(cond)
if ok and (ret == nil or ret == true) then
return
end
duration = duration + interval
@ -124,7 +116,7 @@ function M.edit(path)
end
function M.write_to_file(path, text)
local f = io.open(path, 'wb')
local f = assert(io.open(path, 'wb'))
for _, l in ipairs(text) do
f:write(l)
f:write('\n')
@ -132,6 +124,13 @@ function M.write_to_file(path, text)
f:close()
end
local function spec_text(s)
if type(s) == 'table' then
return s.text
end
return s
end
function M.match_lines(lines, spec)
local i = 1
for lid, line in ipairs(lines) do
@ -153,12 +152,13 @@ function M.match_lines(lines, spec)
i = i + 1
end
end
if i < #spec + 1 then
local msg = {'lines:'}
for _, l in ipairs(lines) do
msg[#msg+1] = string.format( '"%s"', l)
msg[#msg+1] = string.format(' - "%s"', l)
end
error(('Did not match pattern \'%s\' with %s'):format(spec[i], table.concat(msg, '\n')))
error(('Did not match pattern \'%s\' with %s'):format(spec_text(spec[i]), table.concat(msg, '\n')))
end
end
@ -209,7 +209,7 @@ function M.n(str)
end
function M.debug_messages()
return exec_lua("return require'gitsigns'.debug_messages(true)")
return exec_lua("return require'gitsigns.debug.log'.messages")
end
function M.match_dag(lines, spec)
@ -224,6 +224,8 @@ function M.match_debug_messages(spec)
end)
end
local git_version
function M.setup_gitsigns(config, extra)
extra = extra or ''
exec_lua([[
@ -232,7 +234,7 @@ function M.setup_gitsigns(config, extra)
require('gitsigns').setup(...)
]], config)
M.expectf(function()
exec_capture('au gitsigns')
return exec_lua[[return require'gitsigns'._setup_done == true]]
end)
end

View File

@ -58,6 +58,7 @@ describe('highlights', function()
config.signs.topdelete.hl = nil
config.numhl = true
config.linehl = true
config._test_mode = true
exec_lua('gs.setup(...)', config)

View File

@ -226,6 +226,10 @@ local record M
end
is_thread: function(): boolean
record fs
find: function(string|{string}, table): {string}
end
end
return M

View File

@ -92,7 +92,16 @@ local record M
nvim_create_augroup: function(string, AugroupOpts): integer
record AutoCmdOpts
callback: function()
record CallbackData
id: integer
group: integer
event: string
match: string
buf: integer
file: string
data: any
end
callback: function(CallbackData)
command: string
group: integer|string
pattern: string|{string}