From 2845a84a8628e0e8f468778dc80432453d8685ce Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 21 Sep 2023 13:41:49 +0100 Subject: [PATCH] perf(blame): run blame for entire file instead of per line --- lua/gitsigns/actions.lua | 8 +- lua/gitsigns/attach.lua | 1 + lua/gitsigns/cache.lua | 1 + lua/gitsigns/current_line_blame.lua | 68 ++++---------- lua/gitsigns/git.lua | 136 ++++++++++++++++++---------- 5 files changed, 117 insertions(+), 97 deletions(-) diff --git a/lua/gitsigns/actions.lua b/lua/gitsigns/actions.lua index 92c69b42..6b3cd518 100644 --- a/lua/gitsigns/actions.lua +++ b/lua/gitsigns/actions.lua @@ -813,7 +813,7 @@ end local function get_blame_hunk(repo, info) local a = {} -- If no previous so sha of blame added the file - if info.previous then + if info.previous_sha and info.previous_filename then a = repo:get_show_text(info.previous_sha .. ':' .. info.previous_filename) end local b = repo:get_show_text(info.sha .. ':' .. info.filename) @@ -884,12 +884,14 @@ M.blame_line = async.void(function(opts) local buftext = util.buf_lines(bufnr) local fileformat = vim.bo[bufnr].fileformat local lnum = api.nvim_win_get_cursor(0)[1] - local result = bcache.git_obj:run_blame(buftext, lnum, opts.ignore_whitespace) + local results = bcache.git_obj:run_blame(buftext, lnum, opts.ignore_whitespace) pcall(function() loading:close() end) - assert(result) + assert(results and results[lnum]) + + local result = results[lnum] local is_committed = result.sha and tonumber('0x' .. result.sha) ~= 0 diff --git a/lua/gitsigns/attach.lua b/lua/gitsigns/attach.lua index f8dad72f..4dd7173c 100644 --- a/lua/gitsigns/attach.lua +++ b/lua/gitsigns/attach.lua @@ -337,6 +337,7 @@ local attach_throttled = throttle_by_id(function(cbuf, ctx, aucmd) end cache[cbuf] = CacheEntry.new({ + bufnr = cbuf, base = ctx and ctx.base or config.base, file = file, commit = commit, diff --git a/lua/gitsigns/cache.lua b/lua/gitsigns/cache.lua index 8dc2433d..0eee4bad 100644 --- a/lua/gitsigns/cache.lua +++ b/lua/gitsigns/cache.lua @@ -7,6 +7,7 @@ local M = { -- Timer object watching the gitdir --- @class Gitsigns.CacheEntry +--- @field bufnr integer --- @field file string --- @field base? string --- @field compare_text? string[] diff --git a/lua/gitsigns/current_line_blame.lua b/lua/gitsigns/current_line_blame.lua index 602eae1c..28d84b66 100644 --- a/lua/gitsigns/current_line_blame.lua +++ b/lua/gitsigns/current_line_blame.lua @@ -20,52 +20,12 @@ local function reset(bufnr) vim.b[bufnr].gitsigns_blame_line_dict = nil end --- TODO: expose as config -local max_cache_size = 1000 - ---- @class Gitsigns.BlameCache ---- @field cache table ---- @field size integer +--- @class (exact) Gitsigns.BlameCache +--- @field cache Gitsigns.BlameInfo[]? --- @field tick integer -local BlameCache = {} - --- @type table -BlameCache.contents = {} - ---- @param bufnr integer ---- @param lnum integer ---- @param x? Gitsigns.BlameInfo -function BlameCache:add(bufnr, lnum, x) - if not x then - return - end - if not config._blame_cache then - return - end - local scache = self.contents[bufnr] - if scache.size <= max_cache_size then - scache.cache[lnum] = x - scache.size = scache.size + 1 - end -end - ---- @param bufnr integer ---- @param lnum integer ---- @return Gitsigns.BlameInfo? -function BlameCache:get(bufnr, lnum) - if not config._blame_cache then - return - end - - -- init and invalidate - local tick = vim.b[bufnr].changedtick - if not self.contents[bufnr] or self.contents[bufnr].tick ~= tick then - self.contents[bufnr] = { tick = tick, cache = {}, size = 0 } - end - - return self.contents[bufnr].cache[lnum] -end +local blame_cache = {} --- @param fmt string --- @param name string @@ -93,17 +53,29 @@ end --- @param opts Gitsigns.CurrentLineBlameOpts --- @return Gitsigns.BlameInfo? local function run_blame(bufnr, lnum, opts) - local result = BlameCache:get(bufnr, lnum) + -- init and invalidate + local tick = vim.b[bufnr].changedtick + if not blame_cache[bufnr] or blame_cache[bufnr].tick ~= tick then + blame_cache[bufnr] = { tick = tick } + end + + local result = blame_cache[bufnr].cache + if result then - return result + return result[lnum] end local buftext = util.buf_lines(bufnr) local bcache = cache[bufnr] - result = bcache.git_obj:run_blame(buftext, lnum, opts.ignore_whitespace) - BlameCache:add(bufnr, lnum, result) + result = bcache.git_obj:run_blame(buftext, nil, opts.ignore_whitespace) + + if not result then + return + end + + blame_cache[bufnr].cache = result - return result + return result[lnum] end --- @param bufnr integer diff --git a/lua/gitsigns/git.lua b/lua/gitsigns/git.lua index 319bb766..30d23e9c 100644 --- a/lua/gitsigns/git.lua +++ b/lua/gitsigns/git.lua @@ -580,7 +580,6 @@ end --- @field committer_time integer --- @field committer_tz string --- @field summary string ---- @field previous string --- @field previous_filename string --- @field previous_sha string --- @field filename string @@ -592,34 +591,38 @@ end --- @field hunk? string[] --- @field hunk_head? string +local NOT_COMMITTED = { + author = 'Not Committed Yet', + ['author_mail'] = '', + committer = 'Not Committed Yet', + ['committer_mail'] = '', +} + +---@param x any +---@return integer +local function asinteger(x) + return assert(tonumber(x)) +end + --- @param lines string[] ---- @param lnum integer ---- @param ignore_whitespace boolean ---- @return Gitsigns.BlameInfo? +--- @param lnum? integer +--- @param ignore_whitespace? boolean +--- @return Gitsigns.BlameInfo[]? function Obj:run_blame(lines, lnum, ignore_whitespace) - local not_committed = { - author = 'Not Committed Yet', - ['author_mail'] = '', - committer = 'Not Committed Yet', - ['committer_mail'] = '', - } - if not self.object_name or self.repo.abbrev_head == '' then -- As we support attaching to untracked files we need to return something if -- the file isn't isn't tracked in git. -- If abbrev_head is empty, then assume the repo has no commits - return not_committed + return NOT_COMMITTED end - local args = { - 'blame', - '--contents', - '-', - '-L', - lnum .. ',+1', - '--line-porcelain', - self.file, - } + local args = { 'blame', '--contents', '-', '--incremental' } + + if lnum then + vim.list_extend(args, { '-L', lnum..',+1' }) + end + + args[#args+1] = self.file if ignore_whitespace then args[#args + 1] = '-w' @@ -634,36 +637,77 @@ function Obj:run_blame(lines, lnum, ignore_whitespace) if #results == 0 then return end - local header = vim.split(table.remove(results, 1), ' ') - - --- @diagnostic disable-next-line:missing-fields - local ret = {} --- @type Gitsigns.BlameInfo - ret.sha = header[1] - ret.orig_lnum = tonumber(header[2]) --[[@as integer]] - ret.final_lnum = tonumber(header[3]) --[[@as integer]] - ret.abbrev_sha = string.sub(ret.sha, 1, 8) - for _, l in ipairs(results) do - if not startswith(l, '\t') then - local cols = vim.split(l, ' ') - --- @type string - local key = table.remove(cols, 1):gsub('-', '_') - --- @diagnostic disable-next-line:no-unknown - ret[key] = table.concat(cols, ' ') - if key == 'previous' then - ret.previous_sha = cols[1] - ret.previous_filename = cols[2] + + local ret = {} --- @type Gitsigns.BlameInfo[] + local commits = {} --- @type table + local i = 1 + + while i <= #results do + --- @param pat? string + --- @return string + local function get(pat) + local l = assert(results[i]) + i = i + 1 + if pat then + return assert(l:match(pat)) end + return l end - end - -- New in git 2.41: - -- The output given by "git blame" that attributes a line to contents - -- taken from the file specified by the "--contents" option shows it - -- differently from a line attributed to the working tree file. - if ret.author_mail == '' or ret.author_mail == 'External file (--contents)' then - ret = vim.tbl_extend('force', ret, not_committed) + local function peek() + return results[i] + end + + local sha, orig_lnum_str, final_lnum_str, size_str = get('(%x+) (%d+) (%d+) (%d+)') + local orig_lnum = asinteger(orig_lnum_str) + local final_lnum = asinteger(final_lnum_str) + local size = asinteger(size_str) + + if peek():match('^author ') then + local commit = { + sha = sha, + abbrev_sha = sha:sub(1, 8), + author = get('^author (.*)'), + author_mail = get('^author%-mail (.*)'), + author_time = get('^author%-time (.*)'), + author_tz = get('^author%-tz (.*)'), + committer = get('^committer (.*)'), + committer_mail = get('^committer%-mail (.*)'), + committer_time = get('^committer%-time (.*)'), + committer_tz = get('^committer%-tz (.*)'), + summary = get():match('^summary (.*)'), + } + + -- New in git 2.41: + -- The output given by "git blame" that attributes a line to contents + -- taken from the file specified by the "--contents" option shows it + -- differently from a line attributed to the working tree file. + if commit.author_mail == '' or commit.author_mail == 'External file (--contents)' then + commit = vim.tbl_extend('force', commit, NOT_COMMITTED) + end + commits[sha] = commit + end + + --- @type Gitsigns.BlameInfo + local item = vim.deepcopy(commits[sha]) + + item.previous_sha, item.previous_filename = peek():match('^previous (%x+) (.*)') + if item.previous_sha then + get() + end + item.filename = assert(get():match('^filename (.*)')) + + for j = 0, size - 1 do + local final_lnum0 = final_lnum + j + local orig_lnum0 = orig_lnum + j + ret[final_lnum0] = vim.deepcopy(item) + ret[final_lnum0].final_lnum = final_lnum0 + ret[final_lnum0].orig_lnum = orig_lnum0 + end end + assert(vim.tbl_isarray(ret)) + return ret end