From a28bb1db506df663b063cc63f44fbbda178255a7 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Thu, 25 Apr 2024 16:02:32 +0100 Subject: [PATCH] fix(update): always get object contents from object names Fixes #847 --- lua/gitsigns/actions.lua | 7 ++- lua/gitsigns/attach.lua | 19 ++++---- lua/gitsigns/cache.lua | 24 +-------- lua/gitsigns/debug.lua | 32 +++++++----- lua/gitsigns/debug/log.lua | 3 ++ lua/gitsigns/diffthis.lua | 19 ++++---- lua/gitsigns/git.lua | 95 +++++++++++++++++++++++++++++------- lua/gitsigns/manager.lua | 18 +++---- lua/gitsigns/util.lua | 5 +- lua/gitsigns/watcher.lua | 4 +- test/gitdir_watcher_spec.lua | 8 +-- test/gitsigns_spec.lua | 2 - 12 files changed, 147 insertions(+), 89 deletions(-) diff --git a/lua/gitsigns/actions.lua b/lua/gitsigns/actions.lua index 99c631bfa..d9d2561b9 100644 --- a/lua/gitsigns/actions.lua +++ b/lua/gitsigns/actions.lua @@ -1026,7 +1026,10 @@ end --- @param bcache Gitsigns.CacheEntry --- @param base string? local function update_buf_base(bcache, base) - bcache.base = base + bcache.file_mode = base == 'FILE' + if not bcache.file_mode then + bcache.git_obj:update_revision(base) + end bcache:invalidate(true) update(bcache.bufnr) end @@ -1064,7 +1067,7 @@ end --- @param base string|nil The object/revision to diff against. --- @param global boolean|nil Change the base of all buffers. M.change_base = async.create(2, function(base, global) - base = util.calc_base(base) + base = util.norm_base(base) if global then config.base = base diff --git a/lua/gitsigns/attach.lua b/lua/gitsigns/attach.lua index ce543e07e..fc3c2e512 100644 --- a/lua/gitsigns/attach.lua +++ b/lua/gitsigns/attach.lua @@ -49,10 +49,7 @@ local function parse_gitsigns_uri(name) -- TODO(lewis6991): Support submodules --- @type any, any, string?, string?, string 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 + commit = util.norm_base(commit) if root_path then name = root_path .. '/' .. rel_path end @@ -186,7 +183,6 @@ end) --- @field file string --- @field toplevel? string --- @field gitdir? string ---- @field commit? string --- @field base? string --- @param bufnr integer @@ -215,7 +211,13 @@ local function get_buf_context(bufnr) file = file, gitdir = gitdir, toplevel = toplevel, - commit = commit, + -- Commit buffers have there base set back one revision with '^' + -- Stage buffers always compare against the common ancestor (':1') + -- :0: index + -- :1: common ancestor + -- :2: target commit (HEAD) + -- :3: commit which is being merged + base = commit and (commit:match('^:[1-3]') and ':1' or commit .. '^') or nil, } end @@ -267,7 +269,8 @@ local attach_throttled = throttle_by_id(function(cbuf, ctx, aucmd) file = ctx.toplevel .. util.path_sep .. file end - local git_obj = git.Obj.new(file, encoding, ctx.gitdir, ctx.toplevel) + local revision = ctx.base or config.base + local git_obj = git.Obj.new(file, revision, encoding, ctx.gitdir, ctx.toplevel) if not git_obj and not passed_ctx then git_obj = try_worktrees(cbuf, file, encoding) @@ -322,9 +325,7 @@ local attach_throttled = throttle_by_id(function(cbuf, ctx, aucmd) cache[cbuf] = gs_cache.new({ bufnr = cbuf, - base = ctx.base or config.base, file = file, - commit = ctx.commit, git_obj = git_obj, }) diff --git a/lua/gitsigns/cache.lua b/lua/gitsigns/cache.lua index 02d5303db..4ca529f34 100644 --- a/lua/gitsigns/cache.lua +++ b/lua/gitsigns/cache.lua @@ -9,10 +9,10 @@ local M = { --- @class (exact) Gitsigns.CacheEntry --- @field bufnr integer --- @field file string ---- @field base? string --- @field compare_text? string[] --- @field hunks? Gitsigns.Hunk.Hunk[] --- @field force_next_update? boolean +--- @field file_mode? boolean --- --- @field compare_text_head? string[] --- @field hunks_staged? Gitsigns.Hunk.Hunk[] @@ -20,31 +20,11 @@ local M = { --- @field staged_diffs? Gitsigns.Hunk.Hunk[] --- @field gitdir_watcher? uv.uv_fs_event_t --- @field git_obj Gitsigns.GitObj ---- @field commit? string --- @field blame? table local CacheEntry = M.CacheEntry -function CacheEntry:get_compare_rev(base) - base = base or self.base - if base then - return base - end - - if self.commit then - -- Buffer is a fugitive commit so compare against the parent of the commit - if config._signs_staged_enable then - return self.commit - else - return string.format('%s^', self.commit) - end - end - - local stage = self.git_obj.has_conflicts and 1 or 0 - return string.format(':%d', stage) -end - function CacheEntry:get_rev_bufname(rev) - rev = rev or self:get_compare_rev() + rev = rev or self.git_obj.revision or ':0' return string.format('gitsigns://%s/%s:%s', self.git_obj.repo.gitdir, rev, self.git_obj.relpath) end diff --git a/lua/gitsigns/debug.lua b/lua/gitsigns/debug.lua index 05eb5ed1a..949e0120e 100644 --- a/lua/gitsigns/debug.lua +++ b/lua/gitsigns/debug.lua @@ -8,19 +8,29 @@ local M = {} local function process(raw_item, path) --- @diagnostic disable-next-line:undefined-field if path[#path] == vim.inspect.METATABLE then - return nil + return elseif type(raw_item) == 'function' then - return nil - elseif type(raw_item) == 'table' then - local key = path[#path] - if key == 'compare_text' or key == 'compare_text_head' then - local item = raw_item - --- @diagnostic disable-next-line:no-unknown - return { '...', length = #item, head = item[1] } - elseif not vim.tbl_isempty(raw_item) and key == 'staged_diffs' then - return { '...', length = #vim.tbl_keys(raw_item) } - end + return + elseif type(raw_item) ~= 'table' then + return raw_item + end + --- @cast raw_item table + + local key = path[#path] + if + vim.tbl_contains({ + 'compare_text', + 'compare_text_head', + 'hunks', + 'hunks_staged', + 'staged_diffs', + }, key) + then + return { '...', length = #vim.tbl_keys(raw_item), head = raw_item[next(raw_item)] } + elseif key == 'blame' then + return { '...', length = #vim.tbl_keys(raw_item) } end + return raw_item end diff --git a/lua/gitsigns/debug/log.lua b/lua/gitsigns/debug/log.lua index d497b7a0b..cce8e4ff8 100644 --- a/lua/gitsigns/debug/log.lua +++ b/lua/gitsigns/debug/log.lua @@ -130,6 +130,9 @@ function M.eprintf(fmt, ...) eprint(fmt:format(...), 1) end +--- @param cond boolean +--- @param msg string +--- @return boolean function M.assert(cond, msg) if not cond then eprint(msg, 1) diff --git a/lua/gitsigns/diffthis.lua b/lua/gitsigns/diffthis.lua index 7b8b0567f..55e70bc8e 100644 --- a/lua/gitsigns/diffthis.lua +++ b/lua/gitsigns/diffthis.lua @@ -17,13 +17,13 @@ local M = {} --- @param base string? local function bufread(bufnr, dbufnr, base) local bcache = cache[bufnr] - local comp_rev = bcache:get_compare_rev(util.calc_base(base)) + base = util.norm_base(base) local text --- @type string[] - if util.calc_base(base) == util.calc_base(bcache.base) then + if base == bcache.git_obj.revision then text = assert(bcache.compare_text) else local err - text, err = bcache.git_obj:get_show_text(comp_rev) + text, err = bcache.git_obj:get_show_text(base) if err then error(err, 2) end @@ -39,7 +39,7 @@ local function bufread(bufnr, dbufnr, base) local modifiable = vim.bo[dbufnr].modifiable vim.bo[dbufnr].modifiable = true - Status:update(dbufnr, { head = comp_rev }) + Status:update(dbufnr, { head = base }) util.set_lines(dbufnr, 0, -1, text) @@ -54,15 +54,16 @@ end local bufwrite = async.create(3, function(bufnr, dbufnr, base) local bcache = cache[bufnr] local buftext = util.buf_lines(dbufnr) + base = util.norm_base(base) bcache.git_obj:stage_lines(buftext) async.scheduler() if not api.nvim_buf_is_valid(bufnr) then return end vim.bo[dbufnr].modified = false - -- If diff buffer base matches the bcache base then also update the + -- If diff buffer base matches the git_obj revision then also update the -- signs. - if util.calc_base(base) == util.calc_base(bcache.base) then + if base == bcache.git_obj.revision then bcache.compare_text = buftext manager.update(bufnr) end @@ -75,9 +76,9 @@ end) --- @return string? buf Buffer name local function create_show_buf(bufnr, base) local bcache = assert(cache[bufnr]) + base = util.norm_base(base) - local revision = bcache:get_compare_rev(util.calc_base(base)) - local bufname = bcache:get_rev_bufname(revision) + local bufname = bcache:get_rev_bufname(base) if util.bufexists(bufname) then return bufname @@ -95,7 +96,7 @@ local function create_show_buf(bufnr, base) end -- allow editing the index revision - if revision == ':0' then + if not bcache.git_obj.revision then vim.bo[dbuf].buftype = 'acwrite' api.nvim_create_autocmd('BufReadCmd', { diff --git a/lua/gitsigns/git.lua b/lua/gitsigns/git.lua index 704c4444e..d506132ee 100644 --- a/lua/gitsigns/git.lua +++ b/lua/gitsigns/git.lua @@ -38,7 +38,8 @@ end --- @field i_crlf boolean Object has crlf --- @field w_crlf boolean Working copy has crlf --- @field mode_bits string ---- @field object_name string +--- @field revision? string Revision the object is tracking against. Nil for index +--- @field object_name string The fixed object name to use. --- @field relpath string --- @field orig_relpath? string Use for tracking moved files --- @field repo Gitsigns.Repo @@ -72,6 +73,7 @@ local function git_command(args, spec) local cmd = { spec.command or 'git', '--no-pager', + '--no-optional-locks', '--literal-pathspecs', '-c', 'gc.auto=0', -- Disable auto-packing which emits messages to stderr @@ -95,7 +97,7 @@ local function git_command(args, spec) log.eprintf("Received exit code %d when running command\n'%s':\n%s", obj.code, cmd_str, stderr) end - local stdout_lines = vim.split(stdout or '', '\n', { plain = true }) + local stdout_lines = vim.split(stdout or '', '\n') if spec.text then -- If stdout ends with a newline, then remove the final empty string after @@ -388,10 +390,17 @@ function Obj:command(args, spec) return self.repo:command(args, spec) end +--- @param revision? string +function Obj:update_revision(revision) + revision = util.norm_base(revision) + self.revision = revision + self:update() +end + --- @param update_relpath? boolean --- @param silent? boolean --- @return boolean -function Obj:update_file_info(update_relpath, silent) +function Obj:update(update_relpath, silent) local old_object_name = self.object_name local props = self:file_info(self.file, silent) @@ -409,16 +418,28 @@ end --- @class (exact) Gitsigns.FileInfo --- @field relpath string ---- @field i_crlf boolean ---- @field w_crlf boolean ---- @field mode_bits string ---- @field object_name string ---- @field has_conflicts true? +--- @field i_crlf? boolean +--- @field w_crlf? boolean +--- @field mode_bits? string +--- @field object_name? string +--- @field has_conflicts? true ---- @param file string +--- @param file? string --- @param silent? boolean --- @return Gitsigns.FileInfo function Obj:file_info(file, silent) + if self.revision and not vim.startswith(self.revision, ':') then + return self:file_info_tree(file, silent) + else + return self:file_info_index(file, silent) + end +end + +--- @private +--- @param file? string +--- @param silent? boolean +--- @return Gitsigns.FileInfo +function Obj:file_info_index(file, silent) local has_eol = check_version({ 2, 9 }) local cmd = { @@ -473,21 +494,57 @@ function Obj:file_info(file, silent) result.relpath = parts[relpath_idx] end end + return result end ---- @param revision string +--- @private +--- @param file? string +--- @param silent? boolean +--- @return Gitsigns.FileInfo +function Obj:file_info_tree(file, silent) + local results, stderr = self:command({ + '-c', + 'core.quotepath=off', + 'ls-tree', + self.revision, + file or self.file, + }, { ignore_error = true }) + + if stderr then + if not silent then + log.eprint(stderr) + end + return {} + end + + local info, relpath = unpack(vim.split(results[1], '\t')) + local mode_bits, objtype, object_name = unpack(vim.split(info, '%s+')) + assert(objtype == 'blob') + + return { + mode_bits = mode_bits, + object_name = object_name, + relpath = relpath, + } +end + +--- @param revision? string --- @return string[] stdout, string? stderr function Obj:get_show_text(revision) - if revision == 'FILE' then - return util.file_lines(self.file) + if revision and not self.relpath then + dprint('no relpath') + return {} end - if not self.relpath then - return {} + local object = revision and (revision .. ':' .. self.relpath) or self.object_name + + if not object then + dprint('no revision or object_name') + return { '' } end - local stdout, stderr = self.repo:get_show_text(revision .. ':' .. self.relpath, self.encoding) + local stdout, stderr = self.repo:get_show_text(object, self.encoding) if not self.i_crlf and self.w_crlf then -- Add cr @@ -738,7 +795,7 @@ local function ensure_file_in_index(obj) obj:command({ 'update-index', '--add', '--cacheinfo', info }) end - obj:update_file_info() + obj:update() end --- Stage 'lines' as the entire contents of the file @@ -811,11 +868,12 @@ function Obj:has_moved() end --- @param file string +--- @param revision string? --- @param encoding string --- @param gitdir string? --- @param toplevel string? --- @return Gitsigns.GitObj? -function Obj.new(file, encoding, gitdir, toplevel) +function Obj.new(file, revision, encoding, gitdir, toplevel) if in_git_dir(file) then dprint('In git dir') return nil @@ -827,6 +885,7 @@ function Obj.new(file, encoding, gitdir, toplevel) end self.file = file + self.revision = util.norm_base(revision) self.encoding = encoding self.repo = Repo.new(util.dirname(file), gitdir, toplevel) @@ -838,7 +897,7 @@ function Obj.new(file, encoding, gitdir, toplevel) -- When passing gitdir and toplevel, suppress stderr when resolving the file local silent = gitdir ~= nil and toplevel ~= nil - self:update_file_info(true, silent) + self:update(true, silent) return self end diff --git a/lua/gitsigns/manager.lua b/lua/gitsigns/manager.lua index 6ea05f46b..924ab30b9 100644 --- a/lua/gitsigns/manager.lua +++ b/lua/gitsigns/manager.lua @@ -72,7 +72,7 @@ local function apply_win_signs(bufnr, top, bot, clear) return end - local untracked = bcache.git_obj.object_name == nil and not bcache.base + local untracked = bcache.git_obj.object_name == nil apply_win_signs0(bufnr, signs_normal, bcache.hunks, top, bot, clear, untracked) if signs_staged then apply_win_signs0(bufnr, signs_staged, bcache.hunks_staged, top, bot, clear, false) @@ -468,13 +468,14 @@ M.update = throttle_by_id(function(bufnr) bcache.hunks, bcache.hunks_staged = nil, nil local git_obj = bcache.git_obj - - local compare_rev = bcache:get_compare_rev() - - local file_mode = compare_rev == 'FILE' + local file_mode = bcache.file_mode if not bcache.compare_text or config._refresh_staged_on_update or file_mode then - bcache.compare_text = git_obj:get_show_text(compare_rev) + if file_mode then + bcache.compare_text = util.file_lines(git_obj.file) + else + bcache.compare_text = git_obj:get_show_text() + end if not M.schedule(bufnr, true) then return end @@ -487,10 +488,9 @@ M.update = throttle_by_id(function(bufnr) return end - if config._signs_staged_enable and not file_mode then + if config._signs_staged_enable and not file_mode and not git_obj.revision then if not bcache.compare_text_head or config._refresh_staged_on_update then - local staged_compare_rev = bcache.commit and string.format('%s^', bcache.commit) or 'HEAD' - bcache.compare_text_head = git_obj:get_show_text(staged_compare_rev) + bcache.compare_text_head = git_obj:get_show_text('HEAD') if not M.schedule(bufnr, true) then return end diff --git a/lua/gitsigns/util.lua b/lua/gitsigns/util.lua index 623560d4a..613c6679a 100644 --- a/lua/gitsigns/util.lua +++ b/lua/gitsigns/util.lua @@ -223,7 +223,10 @@ end --- @param base? string --- @return string? -function M.calc_base(base) +function M.norm_base(base) + if base == ':0' then + return + end if base and base:sub(1, 1):match('[~\\^]') then base = 'HEAD' .. base end diff --git a/lua/gitsigns/watcher.lua b/lua/gitsigns/watcher.lua index 951a162c7..65218aa9d 100644 --- a/lua/gitsigns/watcher.lua +++ b/lua/gitsigns/watcher.lua @@ -43,7 +43,7 @@ local function handle_moved(bufnr, old_relpath) git_obj.file = git_obj.repo.toplevel .. util.path_sep .. git_obj.relpath bcache.file = git_obj.file - git_obj:update_file_info() + git_obj:update() if not manager.schedule(bufnr) then return end @@ -82,7 +82,7 @@ local watcher_handler = async.create(1, function(bufnr) local was_tracked = git_obj.object_name ~= nil local old_relpath = git_obj.relpath - git_obj:update_file_info() + git_obj:update() if not manager.schedule(bufnr) then return end diff --git a/test/gitdir_watcher_spec.lua b/test/gitdir_watcher_spec.lua index 5cf8ccfad..90a7982d6 100644 --- a/test/gitdir_watcher_spec.lua +++ b/test/gitdir_watcher_spec.lua @@ -52,7 +52,7 @@ describe('gitdir_watcher', function() ), np('run_job: git .* ls%-files .* ' .. vim.pesc(test_file)), n('watch_gitdir(1): Watching git dir'), - np('run_job: git .* show :0:dummy.txt'), + np('run_job: git .* show .*'), }) eq({ [1] = test_file }, get_bufs()) @@ -77,7 +77,7 @@ describe('gitdir_watcher', function() n('handle_moved(1): File moved to dummy.txt2'), np('run_job: git .* ls%-files .* ' .. vim.pesc(test_file2)), np('handle_moved%(1%): Renamed buffer 1 from .*/dummy.txt to .*/dummy.txt2'), - np('run_job: git .* show :0:dummy.txt2'), + np('run_job: git .* show .*'), }) eq({ [1] = test_file2 }, get_bufs()) @@ -103,7 +103,7 @@ describe('gitdir_watcher', function() n('handle_moved(1): File moved to dummy.txt3'), np('run_job: git .* ls%-files .* ' .. vim.pesc(test_file3)), np('handle_moved%(1%): Renamed buffer 1 from .*/dummy.txt2 to .*/dummy.txt3'), - np('run_job: git .* show :0:dummy.txt3'), + np('run_job: git .* show .*'), }) eq({ [1] = test_file3 }, get_bufs()) @@ -128,7 +128,7 @@ describe('gitdir_watcher', function() n('handle_moved(1): Moved file reset'), np('run_job: git .* ls%-files .* ' .. vim.pesc(test_file)), np('handle_moved%(1%): Renamed buffer 1 from .*/dummy.txt3 to .*/dummy.txt'), - np('run_job: git .* show :0:dummy.txt'), + np('run_job: git .* show .*'), }) eq({ [1] = test_file }, get_bufs()) diff --git a/test/gitsigns_spec.lua b/test/gitsigns_spec.lua index 63e6906f5..d6597c0c3 100644 --- a/test/gitsigns_spec.lua +++ b/test/gitsigns_spec.lua @@ -106,7 +106,6 @@ describe('gitsigns (with screen)', function() .. vim.pesc(test_file) ), 'watch_gitdir(1): Watching git dir', - p('run_job: git .* show :0:dummy.txt'), }) check({ @@ -487,7 +486,6 @@ describe('gitsigns (with screen)', function() ), np('run_job: git .* ls%-files .*'), n('watch_gitdir(1): Watching git dir'), - np('run_job: git .* show :0:newfile.txt'), } if not internal_diff then