From f11dc80ec4835dade2eef8d4c1a8c1cbbc831db6 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Sat, 10 Apr 2021 09:34:15 +0100 Subject: [PATCH] Revert "Refactor git commands to be more OOP" This reverts commit 718d61b712240afe873e795b2c6cca2590eef5d8. --- lua/gitsigns.lua | 151 ++++++-- lua/gitsigns/git.lua | 603 ++++++++++++++++------------- teal/gitsigns.tl | 173 ++++++--- teal/gitsigns/git.tl | 601 +++++++++++++++------------- test/gitsigns_spec.lua | 44 +-- types/plenary/async_lib/async.d.tl | 49 +-- 6 files changed, 951 insertions(+), 670 deletions(-) diff --git a/lua/gitsigns.lua b/lua/gitsigns.lua index cab03d9..d5af8eb 100644 --- a/lua/gitsigns.lua +++ b/lua/gitsigns.lua @@ -28,6 +28,7 @@ local git = require('gitsigns/git') local util = require('gitsigns/util') local gs_hunks = require("gitsigns/hunks") +local create_patch = gs_hunks.create_patch local process_hunks = gs_hunks.process_hunks local Hunk = gs_hunks.Hunk @@ -59,8 +60,32 @@ local CacheEntry = {} + + + + + + + local cache = {} +local ensure_file_in_index = async(function(bcache) + if not bcache.object_name or bcache.has_conflicts then + if not bcache.object_name then + + await(git.add_file(bcache.toplevel, bcache.relpath)) + else + + + await(git.update_index(bcache.toplevel, bcache.mode_bits, bcache.object_name, bcache.relpath)) + end + + + _, bcache.object_name, bcache.mode_bits, bcache.has_conflicts = + await(git.file_info(bcache.relpath, bcache.toplevel)) + end +end) + local function get_cursor_hunk(bufnr, hunks) bufnr = bufnr or current_buf() hunks = hunks or cache[bufnr].hunks @@ -122,15 +147,15 @@ local update = async(function(bufnr, bcache) await(scheduler()) local buftext = api.nvim_buf_get_lines(bufnr, 0, -1, false) - local git_obj = bcache.git_obj + local stage = bcache.has_conflicts and 1 or 0 if config.use_internal_diff then if not bcache.staged_text or config._refresh_staged_on_update then - bcache.staged_text = await(git_obj:get_staged_text()) + bcache.staged_text = await(git.get_staged_text(bcache.toplevel, bcache.relpath, stage)) end bcache.hunks = diff.run_diff(bcache.staged_text, buftext, config.diff_algorithm) else - await(git_obj:get_staged(bcache.staged)) + await(git.get_staged(bcache.toplevel, bcache.relpath, stage, bcache.staged)) bcache.hunks = await(git.run_diff(bcache.staged, buftext, config.diff_algorithm)) end bcache.pending_signs = process_hunks(bcache.hunks) @@ -141,7 +166,7 @@ local update = async(function(bufnr, bcache) apply_win_signs(bufnr, bcache.pending_signs) - Status:update(bufnr, gs_hunks.get_summary(bcache.hunks, git_obj.abbrev_head)) + Status:update(bufnr, gs_hunks.get_summary(bcache.hunks, bcache.abbrev_head)) update_cnt = update_cnt + 1 dprint(string.format('updates: %s, jobs: %s', update_cnt, util.job_cnt), bufnr, 'update') @@ -165,17 +190,21 @@ local stage_hunk = void_async(function() return end - if not util.path_exists(bcache.file) then - print("Error: Cannot stage lines. Please add the file to the working tree.") - return - end - local hunk = get_cursor_hunk(bufnr, bcache.hunks) if not hunk then return end - await(bcache.git_obj:stage_hunks({ hunk })) + if not util.path_exists(bcache.file) then + print("Error: Cannot stage lines. Please add the file to the working tree.") + return + end + + await(ensure_file_in_index(bcache)) + + local lines = create_patch(bcache.relpath, { hunk }, bcache.mode_bits) + + await(git.stage_lines(bcache.toplevel, lines)) table.insert(bcache.staged_diffs, hunk) @@ -234,15 +263,23 @@ local undo_stage_hunk = void_async(function() return end - local hunk = table.remove(bcache.staged_diffs) + local hunk = bcache.staged_diffs[#bcache.staged_diffs] + if not hunk then print("No hunks to undo") return end - await(bcache.git_obj:stage_hunks({ hunk }, true)) + local lines = create_patch(bcache.relpath, { hunk }, bcache.mode_bits, true) + + await(git.stage_lines(bcache.toplevel, lines)) + + table.remove(bcache.staged_diffs) + + local hunk_signs = process_hunks({ hunk }) + await(scheduler()) - signs.add(config, bufnr, process_hunks({ hunk })) + signs.add(config, bufnr, hunk_signs) end) local stage_buffer = void_async(function() @@ -260,19 +297,25 @@ local stage_buffer = void_async(function() return end - if not util.path_exists(bcache.git_obj.file) then + if not util.path_exists(bcache.file) then print("Error: Cannot stage file. Please add it to the working tree.") return end - await(bcache.git_obj:stage_hunks(hunks)) + await(ensure_file_in_index(bcache)) + + local lines = create_patch(bcache.relpath, hunks, bcache.mode_bits) + + await(git.stage_lines(bcache.toplevel, lines)) for _, hunk in ipairs(hunks) do table.insert(bcache.staged_diffs, hunk) end await(scheduler()) + signs.remove(bufnr) + Status:clear_diff(bufnr) end) @@ -290,12 +333,17 @@ local reset_buffer_index = void_async(function() local hunks = bcache.staged_diffs - bcache.staged_diffs = {} - await(bcache.git_obj:unstage_file()) + await(git.unstage_file(bcache.toplevel, bcache.file)) + + + local hunk_signs = process_hunks(hunks) + + table.remove(bcache.staged_diffs) await(scheduler()) - signs.add(config, bufnr, process_hunks(hunks)) + + signs.add(config, bufnr, hunk_signs) end) local NavHunkOpts = {} @@ -354,7 +402,8 @@ local function detach(bufnr, keep_signs) end if not keep_signs then - signs.remove(bufnr) + + vim.fn.sign_unplace('gitsigns_ns', { buffer = bufnr }) end @@ -391,7 +440,7 @@ uv.fs_realpath(api.nvim_buf_get_name(bufnr)) or end) end -local function index_handler(cbuf) +local function index_update_handler(cbuf) return void_async(function(err) if err then dprint('Index update error: ' .. err, cbuf, 'watcher_cb') @@ -399,18 +448,22 @@ local function index_handler(cbuf) end dprint('Index update', cbuf, 'watcher_cb') local bcache = cache[cbuf] - local git_obj = bcache.git_obj - await(git_obj:update_abbrev_head()) + _, _, bcache.abbrev_head = await(git.get_repo_info(bcache.toplevel)) - await(scheduler()) - Status:update_head(cbuf, git_obj.abbrev_head) + Status:update_head(cbuf, bcache.abbrev_head) - if not await(git_obj:update_file_info()) then + local _, object_name0, mode_bits0, has_conflicts = + await(git.file_info(bcache.file, bcache.toplevel)) + + if object_name0 == bcache.object_name then dprint('File not changed', cbuf, 'watcher_cb') return end + bcache.object_name = object_name0 + bcache.mode_bits = mode_bits0 + bcache.has_conflicts = has_conflicts bcache.staged_text = nil await(update(cbuf, bcache)) @@ -446,13 +499,14 @@ end local attach = async(function(cbuf) await(scheduler()) cbuf = cbuf or current_buf() - if cache[cbuf] then + if cache[cbuf] ~= nil then dprint('Already attached', cbuf, 'attach') return end dprint('Attaching', cbuf, 'attach') - if api.nvim_buf_line_count(cbuf) > config.max_file_length then + local lc = api.nvim_buf_line_count(cbuf) + if lc > config.max_file_length then dprint('Exceeds max_file_length', cbuf, 'attach') return end @@ -476,38 +530,48 @@ local attach = async(function(cbuf) return end - local git_obj = await(git.obj:new(file)) + local toplevel, gitdir, abbrev_head = await(git.get_repo_info(file_dir)) - if not git_obj.gitdir then + if not gitdir then dprint('Not in git repo', cbuf, 'attach') return end - await(scheduler()) - Status:update_head(cbuf, git_obj.abbrev_head) + Status:update_head(cbuf, abbrev_head) if not util.path_exists(file) or uv.fs_stat(file).type == 'directory' then dprint('Not a file', cbuf, 'attach') return end - if not git_obj.relpath then + + + await(scheduler()) + local staged = os.tmpname() + + local relpath, object_name, mode_bits, has_conflicts = + await(git.file_info(file, toplevel)) + + if not relpath then dprint('Cannot resolve file in repo', cbuf, 'attach') return end - - - await(scheduler()) - cache[cbuf] = { file = file, - staged = os.tmpname(), + relpath = relpath, + object_name = object_name, + mode_bits = mode_bits, + toplevel = toplevel, + gitdir = gitdir, + abbrev_head = abbrev_head, + username = await(git.command({ 'config', 'user.name' }))[1], + has_conflicts = has_conflicts, + staged = staged, staged_text = nil, hunks = {}, staged_diffs = {}, - index_watcher = watch_index(cbuf, git_obj.gitdir, index_handler(cbuf)), - git_obj = git_obj, + index_watcher = watch_index(cbuf, gitdir, index_update_handler(cbuf)), } @@ -592,6 +656,7 @@ function gitsigns_complete(arglead, line) return matches end + local function setup_command() vim.cmd(table.concat({ 'command!', @@ -711,7 +776,7 @@ local blame_line = void_async(function() local buftext = api.nvim_buf_get_lines(bufnr, 0, -1, false) local lnum = api.nvim_win_get_cursor(0)[1] - local result = await(bcache.git_obj:run_blame(buftext, lnum)) + local result = await(git.run_blame(bcache.file, bcache.toplevel, buftext, lnum)) local date = os.date('%Y-%m-%d %H:%M', tonumber(result['author_time'])) local lines = { @@ -744,20 +809,20 @@ end local _current_line_blame = void_async(function() local bufnr = current_buf() local bcache = cache[bufnr] - if not bcache or not bcache.git_obj.object_name then + if not bcache or not bcache.object_name then return end local buftext = api.nvim_buf_get_lines(bufnr, 0, -1, false) local lnum = api.nvim_win_get_cursor(0)[1] - local result = await(bcache.git_obj:run_blame(buftext, lnum)) + local result = await(git.run_blame(bcache.file, bcache.toplevel, buftext, lnum)) await(scheduler()) _current_line_blame_reset(bufnr) api.nvim_buf_set_extmark(bufnr, namespace, lnum - 1, 0, { id = 1, - virt_text = config.current_line_blame_formatter(bcache.git_obj.username, result), + virt_text = config.current_line_blame_formatter(bcache.username, result), }) end) diff --git a/lua/gitsigns/git.lua b/lua/gitsigns/git.lua index fe12346..2e78255 100644 --- a/lua/gitsigns/git.lua +++ b/lua/gitsigns/git.lua @@ -1,53 +1,12 @@ local a = require('plenary/async_lib/async') -local JobSpec = require('plenary/job').JobSpec -local await = a.await -local async = a.async - local gsd = require("gitsigns/debug") local util = require('gitsigns/util') -local gs_hunks = require("gitsigns/hunks") -local Hunk = gs_hunks.Hunk +local hunks = require("gitsigns/hunks") +local Hunk = hunks.Hunk local uv = vim.loop local startswith = vim.startswith - -local GJobSpec = {} - - - - - - - - - - - - -local Obj = {} - - - - - - - - - - - - - - - - - - - - - - local M = {BlameInfo = {}, Version = {}, } @@ -81,6 +40,11 @@ local M = {BlameInfo = {}, Version = {}, } + + + + + @@ -111,30 +75,148 @@ local function check_version(version) return true end -local command = a.wrap(function(args, spec, callback) - local result = {} - spec = spec or {} - spec.command = 'git' - spec.args = { '--no-pager', unpack(args) } - spec.on_stdout = spec.on_stdout or function(_, line) - table.insert(result, line) - end - if not spec.supress_stderr then - spec.on_stderr = spec.on_stderr or function(err, line) - if err then gsd.eprint(err) end - if line then gsd.eprint(line) end - end - end - local old_on_exit = spec.on_exit - spec.on_exit = function() - if old_on_exit then - old_on_exit() - end - callback(result) - end - util.run_job(spec) +M.file_info = a.wrap(function( + file, + toplevel, + callback) + + local relpath + local object_name + local mode_bits + local stage + local has_conflict = false + util.run_job({ + command = 'git', + args = { + '--no-pager', + 'ls-files', + '--stage', + '--others', + '--exclude-standard', + file, + }, + cwd = toplevel, + on_stdout = function(_, line) + local parts = vim.split(line, '\t') + if #parts > 1 then + relpath = parts[2] + local attrs = vim.split(parts[1], '%s+') + stage = tonumber(attrs[3]) + if stage <= 1 then + mode_bits = attrs[1] + object_name = attrs[2] + else + has_conflict = true + end + else + relpath = parts[1] + end + end, + on_exit = function() + callback(relpath, object_name, mode_bits, has_conflict) + end, + }) end, 3) +M.get_staged = a.wrap(function( + toplevel, + relpath, + stage, + output, + callback) + + + + local outf = io.open(output, 'wb') + util.run_job({ + command = 'git', + args = { + '--no-pager', + 'show', + ':' .. tostring(stage) .. ':' .. relpath, + }, + cwd = toplevel, + on_stdout = function(_, line) + outf:write(line) + outf:write('\n') + end, + on_exit = function() + outf:close() + callback() + end, + }) +end, 5) + +M.get_staged_text = a.wrap(function( + toplevel, + relpath, + stage, + callback) + + local result = {} + util.run_job({ + command = 'git', + args = { + '--no-pager', + 'show', + ':' .. tostring(stage) .. ':' .. relpath, + }, + cwd = toplevel, + on_stdout = function(_, line) + table.insert(result, line) + end, + on_exit = function() + callback(result) + end, + }) +end, 4) + +M.run_blame = a.wrap(function( + file, + toplevel, + lines, + lnum, + callback) + + local results = {} + util.run_job({ + command = 'git', + args = { + '--no-pager', + 'blame', + '--contents', '-', + '-L', lnum .. ',+1', + '--line-porcelain', + file, + }, + writer = lines, + cwd = toplevel, + on_stdout = function(_, line) + table.insert(results, line) + end, + on_exit = function() + local ret = {} + if #results == 0 then + callback({}) + return + end + local header = vim.split(table.remove(results, 1), ' ') + ret.sha = header[1] + ret.abbrev_sha = string.sub(ret.sha, 1, 8) + ret.orig_lnum = tonumber(header[2]) + ret.final_lnum = tonumber(header[3]) + for _, l in ipairs(results) do + if not startswith(l, '\t') then + local cols = vim.split(l, ' ') + local key = table.remove(cols, 1):gsub('-', '_') + ret[key] = table.concat(cols, ' ') + end + end + callback(ret) + end, + }) +end, 5) + local function process_abbrev_head(gitdir, head_str) if not gitdir then return head_str @@ -152,27 +234,132 @@ local function process_abbrev_head(gitdir, head_str) return head_str end -local get_repo_info = async(function(path) +M.get_repo_info = a.wrap(function( + path, callback) + local out = {} + local has_abs_gd = check_version({ 2, 13 }) local git_dir_opt = has_abs_gd and '--absolute-git-dir' or '--git-dir' - local results = await(command({ - 'rev-parse', '--show-toplevel', git_dir_opt, '--abbrev-ref', 'HEAD', - }, { - supress_stderr = true, + util.run_job({ + command = 'git', + args = { 'rev-parse', +'--show-toplevel', +git_dir_opt, +'--abbrev-ref', 'HEAD', + }, cwd = path, - })) + on_stdout = function(_, line) + if not has_abs_gd and #out == 1 then + line = uv.fs_realpath(line) + end + table.insert(out, line) + end, + on_exit = vim.schedule_wrap(function() + local toplevel = out[1] + local gitdir = out[2] + local abbrev_head = process_abbrev_head(gitdir, out[3]) + callback(toplevel, gitdir, abbrev_head) + end), + }) +end, 2) - local toplevel = results[1] - local gitdir = results[2] - if not has_abs_gd then - gitdir = uv.fs_realpath(gitdir) - end - local abbrev_head = process_abbrev_head(gitdir, results[3]) - return toplevel, gitdir, abbrev_head -end) +M.stage_lines = a.wrap(function( + toplevel, lines, callback) + local status = true + local err = {} + util.run_job({ + command = 'git', + args = { 'apply', '--cached', '--unidiff-zero', '-' }, + cwd = toplevel, + writer = lines, + on_stderr = function(_, line) + status = false + table.insert(err, line) + end, + on_exit = function() + if not status then + local s = table.concat(err, '\n') + error('Cannot stage lines. Command stderr:\n\n' .. s) + end + callback() + end, + }) +end, 3) + +M.add_file = a.wrap(function( + toplevel, file, callback) + local status = true + local err = {} + util.run_job({ + command = 'git', + args = { 'add', '--intent-to-add', file }, + cwd = toplevel, + on_stderr = function(_, line) + status = false + table.insert(err, line) + end, + on_exit = function() + if not status then + local s = table.concat(err, '\n') + error('Cannot add file. Command stderr:\n\n' .. s) + end + callback() + end, + }) +end, 3) + +M.unstage_file = a.wrap(function( + toplevel, file, callback) + local status = true + local err = {} + util.run_job({ + command = 'git', + args = { 'reset', file }, + cwd = toplevel, + on_stderr = function(_, line) + status = false + table.insert(err, line) + end, + on_exit = function() + if not status then + local s = table.concat(err, '\n') + error('Cannot unstage file. Command stderr:\n\n' .. s) + end + callback() + end, + }) +end, 3) + +M.update_index = a.wrap(function( + toplevel, + mode_bits, + object_name, + file, + callback) + + local status = true + local err = {} + local cacheinfo = table.concat({ mode_bits, object_name, file }, ',') + util.run_job({ + command = 'git', + args = { 'update-index', '--add', '--cacheinfo', cacheinfo }, + cwd = toplevel, + on_stderr = function(_, line) + status = false + table.insert(err, line) + end, + on_exit = function() + if not status then + local s = table.concat(err, '\n') + error('Cannot update index. Command stderr:\n\n' .. s) + end + callback() + end, + }) +end, 5) local function write_to_file(path, text) local f = io.open(path, 'wb') @@ -183,14 +370,15 @@ local function write_to_file(path, text) f:close() end -M.run_diff = async(function( +M.run_diff = a.wrap(function( staged, text, - diff_algo) + diff_algo, + callback) local results = {} - local buffile = os.tmpname() .. '_buf' + local buffile = staged .. '_buf' write_to_file(buffile, text) @@ -209,196 +397,89 @@ M.run_diff = async(function( - await(command({ - '-c', 'core.safecrlf=false', - 'diff', - '--color=never', - '--diff-algorithm=' .. diff_algo, - '--patch-with-raw', - '--unified=0', - staged, - buffile, - }, { + util.run_job({ + command = 'git', + args = { + '--no-pager', + '-c', 'core.safecrlf=false', + 'diff', + '--color=never', + '--diff-algorithm=' .. diff_algo, + '--patch-with-raw', + '--unified=0', + staged, + buffile, + }, on_stdout = function(_, line) if startswith(line, '@@') then - table.insert(results, gs_hunks.parse_diff_line(line)) - elseif #results > 0 then - table.insert(results[#results].lines, line) + table.insert(results, hunks.parse_diff_line(line)) + else + if #results > 0 then + table.insert(results[#results].lines, line) + end end end, - })) - os.remove(buffile) - return results -end) + on_stderr = function(err, line) + if err then + gsd.eprint(err) + end + if line then + gsd.eprint(line) + end + end, + on_exit = function() + os.remove(buffile) + callback(results) + end, + }) +end, 4) -M.set_version = async(function(version) +M.set_version = a.wrap(function(version, callback) if version ~= 'auto' then M.version = parse_version(version) + callback() return end - local results = await(command({ '--version' })) - local line = results[1] - assert(startswith(line, 'git version'), 'Unexpected output: ' .. line) - local parts = vim.split(line, '%s+') - M.version = parse_version(parts[3]) -end) - - - - - -local O = {} - - -O.command = async(function(self, args, spec) - spec = spec or {} - spec.cwd = self.toplevel - return await(command({ '--git-dir=' .. self.gitdir, unpack(args) }, spec)) -end) - -O.update_abbrev_head = async(function(self) - _, _, self.abbrev_head = await(get_repo_info(self.toplevel)) -end) - -O.update_file_info = async(function(self) - local old_object_name = self.object_name - _, self.object_name, self.mode_bits, self.has_conflicts = await(self:file_info()) - - return old_object_name ~= self.object_name -end) - -O.file_info = async(function(self) - local results = await(self:command({ - 'ls-files', - '--stage', - '--others', - '--exclude-standard', - self.file, - })) - - local relpath - local object_name - local mode_bits - local stage - local has_conflict = false - for _, line in ipairs(results) do - local parts = vim.split(line, '\t') - if #parts > 1 then - relpath = parts[2] - local attrs = vim.split(parts[1], '%s+') - stage = tonumber(attrs[3]) - if stage <= 1 then - mode_bits = attrs[1] - object_name = attrs[2] - else - has_conflict = true - end - else - relpath = parts[1] - end - end - return relpath, object_name, mode_bits, has_conflict -end) - -O.unstage_file = async(function(self) - await(self:command({ 'reset', self.file })) -end) - - -O.get_staged_text = async(function(self) - local stage = self.has_conflicts and 1 or 0 - return await(self:command({ 'show', ':' .. tostring(stage) .. ':' .. self.relpath }, { - supress_stderr = true, - })) -end) - - -O.get_staged = async(function(self, output_file) - local stage = self.has_conflicts and 1 or 0 - - - local outf = io.open(output_file, 'wb') - await(self:command({ - 'show', ':' .. tostring(stage) .. ':' .. self.relpath, - }, { - supress_stderr = true, + util.run_job({ + command = 'git', args = { '--version' }, on_stdout = function(_, line) - outf:write(line) - outf:write('\n') + assert(startswith(line, 'git version'), 'Unexpected output: ' .. line) + local parts = vim.split(line, '%s+') + M.version = parse_version(parts[3]) end, - })) - outf:close() -end) + on_stderr = function(err, line) + if err then + gsd.eprint(err) + end + if line then + gsd.eprint(line) + end + end, + on_exit = function() + callback() + end, + }) +end, 2) -O.run_blame = async(function(self, lines, lnum) - local results = await(self:command({ - 'blame', - '--contents', '-', - '-L', lnum .. ',+1', - '--line-porcelain', - self.file, - }, { - writer = lines, - })) - if #results == 0 then - return {} - end - local header = vim.split(table.remove(results, 1), ' ') - - local ret = {} - ret.sha = header[1] - ret.orig_lnum = tonumber(header[2]) - ret.final_lnum = tonumber(header[3]) - 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, ' ') - local key = table.remove(cols, 1):gsub('-', '_') - ret[key] = table.concat(cols, ' ') - end - end - return ret -end) - -O.ensure_file_in_index = async(function(self) - if not self.object_name or self.has_conflicts then - if not self.object_name then - - await(self:command({ 'add', '--intent-to-add', self.file })) - else - - - local info = table.concat({ self.mode_bits, self.object_name, self.relpath }, ',') - await(self:command({ 'update-index', '--add', '--cacheinfo', info })) - end - - - _, self.object_name, self.mode_bits, self.has_conflicts = await(self:file_info()) - end -end) - -O.stage_hunks = async(function(self, hunks, invert) - await(self:ensure_file_in_index()) - await(self:command({ - 'apply', '--cached', '--unidiff-zero', '-', - }, { - writer = gs_hunks.create_patch(self.relpath, hunks, self.mode_bits, invert), - })) -end) - -O.new = a.async(function(self, file) - self.file = file - self.toplevel, self.gitdir, self.abbrev_head = - await(get_repo_info(util.dirname(file))) - - self.relpath, self.object_name, self.mode_bits, self.has_conflicts = - await(self:file_info()) - - self.username = await(command({ 'config', 'user.name' }))[1] - - return self -end) - -M.obj = O +M.command = a.wrap(function(args, callback) + local result = {} + util.run_job({ + command = 'git', args = args, + on_stdout = function(_, line) + table.insert(result, line) + end, + on_stderr = function(err, line) + if err then + gsd.eprint(err) + end + if line then + gsd.eprint(line) + end + end, + on_exit = function() + callback(result) + end, + }) +end, 2) return M diff --git a/teal/gitsigns.tl b/teal/gitsigns.tl index 1bfefb5..4be0b50 100644 --- a/teal/gitsigns.tl +++ b/teal/gitsigns.tl @@ -28,6 +28,7 @@ local git = require('gitsigns/git') local util = require('gitsigns/util') local gs_hunks = require("gitsigns/hunks") +local create_patch = gs_hunks.create_patch local process_hunks = gs_hunks.process_hunks local Hunk = gs_hunks.Hunk @@ -50,17 +51,41 @@ local namespace: integer local record CacheEntry file : string + relpath : string + object_name : string + mode_bits : string + toplevel : string + gitdir : string + username : string staged : string staged_text : {string} + abbrev_head : string + has_conflicts : boolean hunks : {Hunk} staged_diffs : {Hunk} pending_signs : {integer:Sign} index_watcher : vim.loop.FSPollObj -- Timer object watching the files index - git_obj : git.Obj end local cache: {integer:CacheEntry} = {} +local ensure_file_in_index = async(function(bcache: CacheEntry) + if not bcache.object_name or bcache.has_conflicts then + if not bcache.object_name then + -- If there is no object_name then it is not yet in the index so add it + await(git.add_file(bcache.toplevel, bcache.relpath)) + else + -- Update the index with the common ancestor (stage 1) which is what bcache + -- stores + await(git.update_index(bcache.toplevel, bcache.mode_bits, bcache.object_name, bcache.relpath)) + end + + -- Update the cache + _, bcache.object_name, bcache.mode_bits, bcache.has_conflicts = + await(git.file_info(bcache.relpath, bcache.toplevel)) + end +end) + local function get_cursor_hunk(bufnr: integer, hunks: {Hunk}): Hunk bufnr = bufnr or current_buf() hunks = hunks or cache[bufnr].hunks @@ -122,15 +147,15 @@ local update = async(function(bufnr: integer, bcache: CacheEntry) await(scheduler()) local buftext = api.nvim_buf_get_lines(bufnr, 0, -1, false) - local git_obj = bcache.git_obj + local stage = bcache.has_conflicts and 1 or 0 if config.use_internal_diff then if not bcache.staged_text or config._refresh_staged_on_update then - bcache.staged_text = await(git_obj:get_staged_text()) + bcache.staged_text = await(git.get_staged_text(bcache.toplevel, bcache.relpath, stage)) end bcache.hunks = diff.run_diff(bcache.staged_text, buftext, config.diff_algorithm) else - await(git_obj:get_staged(bcache.staged)) + await(git.get_staged(bcache.toplevel, bcache.relpath, stage, bcache.staged)) bcache.hunks = await(git.run_diff(bcache.staged, buftext, config.diff_algorithm)) end bcache.pending_signs = process_hunks(bcache.hunks) @@ -141,7 +166,7 @@ local update = async(function(bufnr: integer, bcache: CacheEntry) -- provider as they are drawn. apply_win_signs(bufnr, bcache.pending_signs) - Status:update(bufnr, gs_hunks.get_summary(bcache.hunks, git_obj.abbrev_head)) + Status:update(bufnr, gs_hunks.get_summary(bcache.hunks, bcache.abbrev_head)) update_cnt = update_cnt + 1 dprint(string.format('updates: %s, jobs: %s', update_cnt, util.job_cnt), bufnr, 'update') @@ -165,17 +190,21 @@ local stage_hunk = void_async(function() return end - if not util.path_exists(bcache.file) then - print("Error: Cannot stage lines. Please add the file to the working tree.") - return - end - local hunk = get_cursor_hunk(bufnr, bcache.hunks) if not hunk then return end - await(bcache.git_obj:stage_hunks({hunk})) + if not util.path_exists(bcache.file) then + print("Error: Cannot stage lines. Please add the file to the working tree.") + return + end + + await(ensure_file_in_index(bcache)) + + local lines = create_patch(bcache.relpath, {hunk}, bcache.mode_bits) + + await(git.stage_lines(bcache.toplevel, lines)) table.insert(bcache.staged_diffs, hunk) @@ -234,15 +263,23 @@ local undo_stage_hunk = void_async(function() return end - local hunk = table.remove(bcache.staged_diffs) + local hunk = bcache.staged_diffs[#bcache.staged_diffs] + if not hunk then print("No hunks to undo") return end - await(bcache.git_obj:stage_hunks({hunk}, true)) + local lines = create_patch(bcache.relpath, {hunk}, bcache.mode_bits, true) + + await(git.stage_lines(bcache.toplevel, lines)) + + table.remove(bcache.staged_diffs) + + local hunk_signs = process_hunks({hunk}) + await(scheduler()) - signs.add(config, bufnr, process_hunks({hunk})) + signs.add(config, bufnr, hunk_signs) end) local stage_buffer = void_async(function() @@ -257,22 +294,28 @@ local stage_buffer = void_async(function() local hunks = bcache.hunks if #hunks == 0 then print("No unstaged changes in file to stage") - return + return end - if not util.path_exists(bcache.git_obj.file) then + if not util.path_exists(bcache.file) then print("Error: Cannot stage file. Please add it to the working tree.") return end - await(bcache.git_obj:stage_hunks(hunks)) + await(ensure_file_in_index(bcache)) - for _, hunk in ipairs(hunks) do + local lines = create_patch(bcache.relpath, hunks, bcache.mode_bits) + + await(git.stage_lines(bcache.toplevel, lines)) + + for _,hunk in ipairs(hunks) do table.insert(bcache.staged_diffs, hunk) end await(scheduler()) + signs.remove(bufnr) + Status:clear_diff(bufnr) end) @@ -283,19 +326,24 @@ local reset_buffer_index = void_async(function() return end - -- `bcache.staged_diffs` won't contain staged changes outside of current - -- neovim session so signs added from this unstage won't be complete They will - -- however be fixed by index watcher and properly updated We should implement - -- some sort of initial population from git diff, after that this function can - -- be improved to check if any staged hunks exists and it can undo changes - -- using git apply line by line instead of reseting whole file + -- `bcache.staged_diffs` won't contain staged changes outside of current neovim session + -- so signs added from this unstage won't be complete + -- They will however be fixed by index watcher and properly updated + -- We should implement some sort of initial population from git diff, + -- after that this function can be improved to check if any staged hunks exists + -- and it can undo changes using git apply line by line instead of reseting whole file local hunks = bcache.staged_diffs - bcache.staged_diffs = {} - await(bcache.git_obj:unstage_file()) + await(git.unstage_file(bcache.toplevel, bcache.file)) + + -- Signs need to be generated before the `table.remove` below as it modifies the hunks array + local hunk_signs = process_hunks(hunks) + + table.remove(bcache.staged_diffs) await(scheduler()) - signs.add(config, bufnr, process_hunks(hunks)) + + signs.add(config, bufnr, hunk_signs) end) local record NavHunkOpts @@ -341,7 +389,7 @@ end -- 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 +-- to the file externally changeing. 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. local function detach(bufnr: integer, keep_signs: boolean) @@ -354,7 +402,8 @@ local function detach(bufnr: integer, keep_signs: boolean) end if not keep_signs then - signs.remove(bufnr) -- Remove all signs + -- Remove all the signs + vim.fn.sign_unplace('gitsigns_ns', {buffer = bufnr}) end -- Clear status variables @@ -391,7 +440,7 @@ local function get_buf_path(bufnr: integer): string end) end -local function index_handler(cbuf: integer): function +local function index_update_handler(cbuf: integer): function return void_async(function(err: string) if err then dprint('Index update error: '..err, cbuf, 'watcher_cb') @@ -399,19 +448,23 @@ local function index_handler(cbuf: integer): function end dprint('Index update', cbuf, 'watcher_cb') local bcache = cache[cbuf] - local git_obj = bcache.git_obj - await(git_obj:update_abbrev_head()) + _, _, bcache.abbrev_head = await(git.get_repo_info(bcache.toplevel)) - await(scheduler()) - Status:update_head(cbuf, git_obj.abbrev_head) + Status:update_head(cbuf, bcache.abbrev_head) - if not await(git_obj:update_file_info()) then + local _, object_name0, mode_bits0, has_conflicts = + await(git.file_info(bcache.file, bcache.toplevel)) + + if object_name0 == bcache.object_name then dprint('File not changed', cbuf, 'watcher_cb') return end - bcache.staged_text = nil -- Invalidate + bcache.object_name = object_name0 + bcache.mode_bits = mode_bits0 + bcache.has_conflicts = has_conflicts + bcache.staged_text = nil -- Invalidate await(update(cbuf, bcache)) end) @@ -446,13 +499,14 @@ end local attach = async(function(cbuf: integer) await(scheduler()) cbuf = cbuf or current_buf() - if cache[cbuf] then + if cache[cbuf] ~= nil then dprint('Already attached', cbuf, 'attach') return end dprint('Attaching', cbuf, 'attach') - if api.nvim_buf_line_count(cbuf) > config.max_file_length then + local lc = api.nvim_buf_line_count(cbuf) + if lc > config.max_file_length then dprint('Exceeds max_file_length', cbuf, 'attach') return end @@ -476,38 +530,48 @@ local attach = async(function(cbuf: integer) return end - local git_obj = await(git.obj:new(file)) + local toplevel, gitdir, abbrev_head = await(git.get_repo_info(file_dir)) - if not git_obj.gitdir then + if not gitdir then dprint('Not in git repo', cbuf, 'attach') return end - await(scheduler()) - Status:update_head(cbuf, git_obj.abbrev_head) + Status:update_head(cbuf, abbrev_head) if not util.path_exists(file) or uv.fs_stat(file).type == 'directory' then dprint('Not a file', cbuf, 'attach') return end - if not git_obj.relpath then + -- On windows os.tmpname() crashes in callback threads so initialise this + -- variable on the main thread. + await(scheduler()) + local staged = os.tmpname() -- Temp filename of staged file + + local relpath, object_name, mode_bits, has_conflicts = + await(git.file_info(file, toplevel)) + + if not relpath then dprint('Cannot resolve file in repo', cbuf, 'attach') return end - -- On windows os.tmpname() crashes in callback threads so initialise this - -- variable on the main thread. - await(scheduler()) - cache[cbuf] = { file = file, - staged = os.tmpname(), + relpath = relpath, + object_name = object_name, + mode_bits = mode_bits, + toplevel = toplevel, + gitdir = gitdir, + abbrev_head = abbrev_head, + username = await(git.command{'config', 'user.name'})[1], + has_conflicts = has_conflicts, + staged = staged, staged_text = nil, hunks = {}, staged_diffs = {}, - index_watcher = watch_index(cbuf, git_obj.gitdir, index_handler(cbuf)), - git_obj = git_obj + index_watcher = watch_index(cbuf, gitdir, index_update_handler(cbuf)) } -- Initial update @@ -592,6 +656,7 @@ function gitsigns_complete(arglead: string, line: string): {string} return matches end + local function setup_command() vim.cmd(table.concat({ 'command!', @@ -711,7 +776,7 @@ local blame_line = void_async(function() local buftext = api.nvim_buf_get_lines(bufnr, 0, -1, false) local lnum = api.nvim_win_get_cursor(0)[1] - local result = await(bcache.git_obj:run_blame(buftext, lnum)) + local result = await(git.run_blame(bcache.file, bcache.toplevel, buftext, lnum)) local date = os.date('%Y-%m-%d %H:%M', tonumber(result['author_time'])) local lines = { @@ -744,20 +809,20 @@ end local _current_line_blame = void_async(function() local bufnr = current_buf() local bcache = cache[bufnr] - if not bcache or not bcache.git_obj.object_name then + if not bcache or not bcache.object_name then return end local buftext = api.nvim_buf_get_lines(bufnr, 0, -1, false) local lnum = api.nvim_win_get_cursor(0)[1] - local result = await(bcache.git_obj:run_blame(buftext, lnum)) + local result = await(git.run_blame(bcache.file, bcache.toplevel, buftext, lnum)) await(scheduler()) _current_line_blame_reset(bufnr) api.nvim_buf_set_extmark(bufnr, namespace, lnum-1, 0, { id = 1, - virt_text = config.current_line_blame_formatter(bcache.git_obj.username, result), + virt_text = config.current_line_blame_formatter(bcache.username, result), }) end) diff --git a/teal/gitsigns/git.tl b/teal/gitsigns/git.tl index 0bb775c..be21536 100644 --- a/teal/gitsigns/git.tl +++ b/teal/gitsigns/git.tl @@ -1,54 +1,13 @@ local a = require('plenary/async_lib/async') -local JobSpec = require('plenary/job').JobSpec -local await = a.await -local async = a.async - local gsd = require("gitsigns/debug") local util = require('gitsigns/util') -local gs_hunks = require("gitsigns/hunks") -local Hunk = gs_hunks.Hunk +local hunks = require("gitsigns/hunks") +local Hunk = hunks.Hunk local uv = vim.loop local startswith = vim.startswith -local record GJobSpec - command: string - args: {string} - cwd: string - on_stdout: function - on_stderr: function - on_exit: function - writer: {string} - - -- gitsigns extensions - supress_stderr: boolean -end - -local record Obj - toplevel : string - gitdir : string - file : string - abbrev_head : string - username : string - relpath : string - object_name : string - mode_bits : string - has_conflicts : boolean - - command : function(Obj, {string}, GJobSpec): a.future1<{string}> - update_abbrev_head : function(Obj): a.future0 - update_file_info : function(Obj): a.future1 - unstage_file : function(Obj, string, string): a.future0 - get_staged : function(Obj, string): a.future0 - get_staged_text : function(Obj): a.future1<{string}> - run_blame : function(Obj, {string}, number): a.future1 - file_info : function(Obj): a.future4 - ensure_file_in_index : function(Obj): a.future0 - stage_hunks : function(Obj, {Hunk}, boolean): a.future0 - new : function(Obj, string): a.future1 -end - local record M record BlameInfo -- Info in header @@ -78,13 +37,18 @@ local record M end version: Version - set_version: function(string): a.future0 - run_diff : function(string, {string}, string): a.future1<{Hunk}> - - type Obj = Obj - - obj: Obj - + file_info : function(string, string): a.future4 + get_staged : function(string, string, number, string): a.future0 + get_staged_text : function(string, string, number): a.future1<{string}> + run_blame : function(string, string, {string}, number): a.future1 + get_repo_info : function(string): a.future3 + stage_lines : function(string, {string}): a.future0 + add_file : function(string, string, string): a.future0 + unstage_file : function(string, string, string): a.future0 + update_index : function(string, string, string, string): a.future0 + run_diff : function(string, {string}, string): a.future1<{Hunk}> + set_version : function(string): a.future0 + command : function({string}): a.future1<{string}> end local function parse_version(version: string): M.Version @@ -111,30 +75,148 @@ local function check_version(version: {number,number,number}): boolean return true end -local command = a.wrap(function(args: {string}, spec: GJobSpec, callback: function({string})) - local result: {string} = {} - spec = spec or {} - spec.command = 'git' - spec.args = {'--no-pager', unpack(args) } - spec.on_stdout = spec.on_stdout or function(_, line: string) - table.insert(result, line) - end - if not spec.supress_stderr then - spec.on_stderr = spec.on_stderr or function(err: string, line: string) - if err then gsd.eprint(err) end - if line then gsd.eprint(line) end +M.file_info = a.wrap(function( + file: string, + toplevel: string, + callback: function(string, string, string, boolean) +) + local relpath: string + local object_name: string + local mode_bits: string + local stage: number + local has_conflict: boolean = false + util.run_job { + command = 'git', + args = { + '--no-pager', + 'ls-files', + '--stage', + '--others', + '--exclude-standard', + file + }, + cwd = toplevel, + on_stdout = function(_, line: string) + local parts = vim.split(line, '\t') + if #parts > 1 then -- tracked file + relpath = parts[2] + local attrs = vim.split(parts[1], '%s+') + stage = tonumber(attrs[3]) + if stage <= 1 then + mode_bits = attrs[1] + object_name = attrs[2] + else + has_conflict = true + end + else -- untracked file + relpath = parts[1] + end + end, + on_exit = function() + callback(relpath, object_name, mode_bits, has_conflict) end - end - local old_on_exit = spec.on_exit - spec.on_exit = function() - if old_on_exit then - old_on_exit() - end - callback(result) - end - util.run_job(spec as JobSpec) + } end, 3) +M.get_staged = a.wrap(function( + toplevel: string, + relpath: string, + stage: number, + output: string, + callback: function() +) + -- On windows 'w' mode use \r\n instead of \n, see: + -- https://stackoverflow.com/a/43967013 + local outf = io.open(output , 'wb') + util.run_job { + command = 'git', + args = { + '--no-pager', + 'show', + ':'..tostring(stage)..':'..relpath, + }, + cwd = toplevel, + on_stdout = function(_, line: string) + outf:write(line) + outf:write('\n') + end, + on_exit = function() + outf:close() + callback() + end + } +end, 5) + +M.get_staged_text = a.wrap(function( + toplevel: string, + relpath: string, + stage: number, + callback: function({string}) +) + local result = {} + util.run_job { + command = 'git', + args = { + '--no-pager', + 'show', + ':'..tostring(stage)..':'..relpath, + }, + cwd = toplevel, + on_stdout = function(_, line: string) + table.insert(result, line) + end, + on_exit = function() + callback(result) + end + } +end, 4) + +M.run_blame = a.wrap(function( + file: string, + toplevel: string, + lines: {string}, + lnum: number, + callback: function(M.BlameInfo) +) + local results: {string} = {} + util.run_job { + command = 'git', + args = { + '--no-pager', + 'blame', + '--contents', '-', + '-L', lnum..',+1', + '--line-porcelain', + file + }, + writer = lines, + cwd = toplevel, + on_stdout = function(_, line: string) + table.insert(results, line) + end, + on_exit = function() + local ret: {string:any} = {} + if #results == 0 then + callback({}) + return + end + local header = vim.split(table.remove(results, 1), ' ') + ret.sha = header[1] + ret.abbrev_sha = string.sub(ret.sha as string, 1, 8) + ret.orig_lnum = tonumber(header[2]) as integer + ret.final_lnum = tonumber(header[3]) as integer + for _, l in ipairs(results) do + if not startswith(l, '\t') then + local cols = vim.split(l, ' ') + local key = table.remove(cols, 1):gsub('-', '_') + ret[key] = table.concat(cols, ' ') + end + end + callback(ret as M.BlameInfo) + end + } +end, 5) + local function process_abbrev_head(gitdir: string, head_str: string): string if not gitdir then return head_str @@ -152,27 +234,132 @@ local function process_abbrev_head(gitdir: string, head_str: string): string return head_str end -local get_repo_info = async(function(path: string): string,string,string +M.get_repo_info = a.wrap(function( + path: string, callback: function(string,string,string)) + local out = {} + -- Does git rev-parse have --absolute-git-dir, added in 2.13: -- https://public-inbox.org/git/20170203024829.8071-16-szeder.dev@gmail.com/ local has_abs_gd = check_version{2,13} local git_dir_opt = has_abs_gd and '--absolute-git-dir' or '--git-dir' - local results = await(command({ - 'rev-parse', '--show-toplevel', git_dir_opt, '--abbrev-ref', 'HEAD', - }, { - supress_stderr = true, - cwd = path - })) + util.run_job { + command = 'git', + args = {'rev-parse', + '--show-toplevel', + git_dir_opt, + '--abbrev-ref', 'HEAD', + }, + cwd = path, + on_stdout = function(_, line: string) + if not has_abs_gd and #out == 1 then + line = uv.fs_realpath(line) + end + table.insert(out, line) + end, + on_exit = vim.schedule_wrap(function() + local toplevel = out[1] + local gitdir = out[2] + local abbrev_head = process_abbrev_head(gitdir, out[3]) + callback(toplevel, gitdir, abbrev_head) + end) + } +end, 2) - local toplevel = results[1] - local gitdir = results[2] - if not has_abs_gd then - gitdir = uv.fs_realpath(gitdir) - end - local abbrev_head = process_abbrev_head(gitdir, results[3]) - return toplevel, gitdir, abbrev_head -end) +M.stage_lines = a.wrap(function( + toplevel: string, lines: {string}, callback: function()) + local status = true + local err = {} + util.run_job { + command = 'git', + args = {'apply', '--cached', '--unidiff-zero', '-'}, + cwd = toplevel, + writer = lines, + on_stderr = function(_, line: string) + status = false + table.insert(err, line) + end, + on_exit = function() + if not status then + local s = table.concat(err, '\n') + error('Cannot stage lines. Command stderr:\n\n'..s) + end + callback() + end + } +end, 3) + +M.add_file = a.wrap(function( + toplevel: string, file: string, callback: function()) + local status = true + local err = {} + util.run_job { + command = 'git', + args = {'add', '--intent-to-add', file}, + cwd = toplevel, + on_stderr = function(_, line: string) + status = false + table.insert(err, line) + end, + on_exit = function() + if not status then + local s = table.concat(err, '\n') + error('Cannot add file. Command stderr:\n\n'..s) + end + callback() + end + } +end, 3) + +M.unstage_file = a.wrap(function( + toplevel: string, file: string, callback: function()) + local status = true + local err = {} + util.run_job { + command = 'git', + args = {'reset', file}, + cwd = toplevel, + on_stderr = function(_, line: string) + status = false + table.insert(err, line) + end, + on_exit = function() + if not status then + local s = table.concat(err, '\n') + error('Cannot unstage file. Command stderr:\n\n'..s) + end + callback() + end + } +end, 3) + +M.update_index = a.wrap(function( + toplevel: string, + mode_bits: string, + object_name: string, + file: string, + callback: function() +) + local status = true + local err = {} + local cacheinfo = table.concat({mode_bits, object_name, file}, ',') + util.run_job { + command = 'git', + args = {'update-index', '--add', '--cacheinfo', cacheinfo}, + cwd = toplevel, + on_stderr = function(_, line: string) + status = false + table.insert(err, line) + end, + on_exit = function() + if not status then + local s = table.concat(err, '\n') + error('Cannot update index. Command stderr:\n\n'..s) + end + callback() + end + } +end, 5) local function write_to_file(path: string, text: {string}) local f = io.open(path, 'wb') @@ -183,14 +370,15 @@ local function write_to_file(path: string, text: {string}) f:close() end -M.run_diff = async(function( +M.run_diff = a.wrap(function( staged: string, text: {string}, - diff_algo: string -): {Hunk} + diff_algo: string, + callback: function({Hunk}) +) local results: {Hunk} = {} - local buffile = os.tmpname()..'_buf' + local buffile = staged..'_buf' write_to_file(buffile, text) -- Taken from gitgutter, diff.vim: @@ -209,7 +397,10 @@ M.run_diff = async(function( -- We can safely ignore the warning, we turn it off by passing the '-c -- "core.safecrlf=false"' argument to git-diff. - await(command({ + util.run_job { + command = 'git', + args = { + '--no-pager', '-c', 'core.safecrlf=false', 'diff', '--color=never', @@ -218,187 +409,77 @@ M.run_diff = async(function( '--unified=0', staged, buffile, - }, { + }, on_stdout = function(_, line: string) if startswith(line, '@@') then - table.insert(results, gs_hunks.parse_diff_line(line)) - elseif #results > 0 then - table.insert(results[#results].lines, line) + table.insert(results, hunks.parse_diff_line(line)) + else + if #results > 0 then + table.insert(results[#results].lines, line) + end end + end, + on_stderr = function(err: string, line: string) + if err then + gsd.eprint(err) + end + if line then + gsd.eprint(line) + end + end, + on_exit = function() + os.remove(buffile) + callback(results) end - })) - os.remove(buffile) - return results -end) + } +end, 4) -M.set_version = async(function(version: string) +M.set_version = a.wrap(function(version: string, callback: function()) if version ~= 'auto' then M.version = parse_version(version) + callback() return end - local results = await(command{'--version'}) - local line = results[1] - assert(startswith(line, 'git version'), 'Unexpected output: '..line) - local parts = vim.split(line, '%s+') - M.version = parse_version(parts[3]) -end) - --------------------------------------------------------------------------------- --- Git object methods --------------------------------------------------------------------------------- - -local O: Obj = {} - ---- Run git command the with the objects gitdir and toplevel -O.command = async(function(self: Obj, args: {string}, spec: GJobSpec): {string} - spec = spec or {} - spec.cwd = self.toplevel - return await(command({'--git-dir='..self.gitdir, unpack(args)}, spec)) -end) - -O.update_abbrev_head = async(function(self: Obj) - _, _, self.abbrev_head = await(get_repo_info(self.toplevel)) -end) - -O.update_file_info = async(function(self: Obj): boolean - local old_object_name = self.object_name - _, self.object_name, self.mode_bits, self.has_conflicts = await(self:file_info()) - - return old_object_name ~= self.object_name -end) - -O.file_info = async(function(self: Obj): string, string, string, boolean - local results = await(self:command({ - 'ls-files', - '--stage', - '--others', - '--exclude-standard', - self.file - })) - - local relpath: string - local object_name: string - local mode_bits: string - local stage: number - local has_conflict: boolean = false - for _, line in ipairs(results) do - local parts = vim.split(line, '\t') - if #parts > 1 then -- tracked file - relpath = parts[2] - local attrs = vim.split(parts[1], '%s+') - stage = tonumber(attrs[3]) - if stage <= 1 then - mode_bits = attrs[1] - object_name = attrs[2] - else - has_conflict = true - end - else -- untracked file - relpath = parts[1] - end - end - return relpath, object_name, mode_bits, has_conflict -end) - -O.unstage_file = async(function(self: Obj) - await(self:command{'reset', self.file }) -end) - ---- Get version of file in the index, return array lines -O.get_staged_text = async(function(self: Obj): {string} - local stage = self.has_conflicts and 1 or 0 - return await(self:command({'show', ':'..tostring(stage)..':'..self.relpath}, { - supress_stderr = true - })) -end) - ---- Get version of file in the index, write lines to file -O.get_staged = async(function(self: Obj, output_file: string) - local stage = self.has_conflicts and 1 or 0 - -- On windows 'w' mode use \r\n instead of \n, see: - -- https://stackoverflow.com/a/43967013 - local outf = io.open(output_file, 'wb') - await(self:command({ - 'show', ':'..tostring(stage)..':'..self.relpath - }, { - supress_stderr = true, + util.run_job { + command = 'git', args = {'--version'}, on_stdout = function(_, line: string) - outf:write(line) - outf:write('\n') + assert(startswith(line, 'git version'), 'Unexpected output: '..line) + local parts = vim.split(line, '%s+') + M.version = parse_version(parts[3]) + end, + on_stderr = function(err: string, line: string) + if err then + gsd.eprint(err) + end + if line then + gsd.eprint(line) + end + end, + on_exit = function() + callback() end - })) - outf:close() -end) + } +end, 2) -O.run_blame = async(function(self: Obj, lines: {string}, lnum: number): M.BlameInfo - local results = await(self:command({ - 'blame', - '--contents', '-', - '-L', lnum..',+1', - '--line-porcelain', - self.file - }, { - writer = lines, - })) - if #results == 0 then - return {} - end - local header = vim.split(table.remove(results, 1), ' ') - - local ret: {string:any} = {} - 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 as string, 1, 8) - for _, l in ipairs(results) do - if not startswith(l, '\t') then - local cols = vim.split(l, ' ') - local key = table.remove(cols, 1):gsub('-', '_') - ret[key] = table.concat(cols, ' ') +M.command = a.wrap(function(args: {string}, callback: function({string})) + local result: {string} = {} + util.run_job { + command = 'git', args = args, + on_stdout = function(_, line: string) + table.insert(result, line) + end, + on_stderr = function(err: string, line: string) + if err then + gsd.eprint(err) + end + if line then + gsd.eprint(line) + end + end, + on_exit = function() + callback(result) end - end - return ret as M.BlameInfo -end) - -O.ensure_file_in_index = async(function(self: Obj) - if not self.object_name or self.has_conflicts then - if not self.object_name then - -- If there is no object_name then it is not yet in the index so add it - await(self:command{'add', '--intent-to-add', self.file}) - else - -- Update the index with the common ancestor (stage 1) which is what bcache - -- stores - local info = table.concat({self.mode_bits, self.object_name, self.relpath}, ',') - await(self:command{'update-index', '--add', '--cacheinfo', info}) - end - - -- Update file info - _, self.object_name, self.mode_bits, self.has_conflicts = await(self:file_info()) - end -end) - -O.stage_hunks = async(function(self: Obj, hunks: {Hunk}, invert: boolean) - await(self:ensure_file_in_index()) - await(self:command({ - 'apply', '--cached', '--unidiff-zero', '-' - }, { - writer = gs_hunks.create_patch(self.relpath, hunks, self.mode_bits, invert) - })) -end) - -O.new = a.async(function(self: Obj, file: string): Obj - self.file = file - self.toplevel, self.gitdir, self.abbrev_head = - await(get_repo_info(util.dirname(file))) - - self.relpath, self.object_name, self.mode_bits, self.has_conflicts = - await(self:file_info()) - - self.username = await(command({'config', 'user.name'}))[1] - - return self -end) - -M.obj = O + } +end, 2) return M diff --git a/test/gitsigns_spec.lua b/test/gitsigns_spec.lua index 6dfa1ad..708b375 100644 --- a/test/gitsigns_spec.lua +++ b/test/gitsigns_spec.lua @@ -21,7 +21,6 @@ local function check_status(status) end local scratch = os.getenv('PJ_ROOT')..'/scratch' -local gitdir = scratch..'/.git' local test_file = scratch..'/dummy.txt' local newfile = scratch.."/newfile.txt" @@ -231,14 +230,14 @@ describe('gitsigns', function() edit(test_file) sleep(10) match_debug_messages { - "run_job: git --no-pager --version", + "run_job: git --version", 'attach(1): Attaching', - 'run_job: git --no-pager rev-parse --show-toplevel --absolute-git-dir --abbrev-ref HEAD', - p('run_job: git .* ls%-files %-%-stage %-%-others %-%-exclude%-standard '..test_file), - p'run_job: git .* config user.name', + 'run_job: git rev-parse --show-toplevel --absolute-git-dir --abbrev-ref HEAD', + 'run_job: git --no-pager ls-files --stage --others --exclude-standard '..test_file, + "run_job: git config user.name", 'watch_index(1): Watching index', + 'run_job: git --no-pager show :0:dummy.txt', 'watcher_cb(1): Index update error: ENOENT', - p'run_job: git .* show :0:dummy.txt', 'update(1): updates: 1, jobs: 5' } @@ -295,7 +294,7 @@ describe('gitsigns', function() sleep(20) match_debug_messages { - "run_job: git --no-pager --version", + "run_job: git --version", 'attach(1): Attaching', 'attach(1): In git dir' } @@ -311,11 +310,10 @@ describe('gitsigns', function() sleep(20) match_debug_messages { - "run_job: git --no-pager --version", + "run_job: git --version", "attach(1): Attaching", - "run_job: git --no-pager rev-parse --show-toplevel --absolute-git-dir --abbrev-ref HEAD", + "run_job: git rev-parse --show-toplevel --absolute-git-dir --abbrev-ref HEAD", p"run_job: git .* ls%-files .*/dummy_ignored.txt", - p"run_job: git .* config user.name", "attach(1): Cannot resolve file in repo", } @@ -327,11 +325,9 @@ describe('gitsigns', function() sleep(10) match_debug_messages { - "run_job: git --no-pager --version", + "run_job: git --version", "attach(1): Attaching", - "run_job: git --no-pager rev-parse --show-toplevel --absolute-git-dir --abbrev-ref HEAD", - p("run_job: git .* ls%-files %-%-stage %-%-others %-%-exclude%-standard "..newfile), - p"run_job: git .* config user.name", + "run_job: git rev-parse --show-toplevel --absolute-git-dir --abbrev-ref HEAD", "attach(1): Not a file", } @@ -343,7 +339,7 @@ describe('gitsigns', function() edit(scratch..'/does/not/exist') match_debug_messages { - "run_job: git --no-pager --version", + "run_job: git --version", "attach(1): Attaching", "attach(1): Not a path", } @@ -356,7 +352,7 @@ describe('gitsigns', function() it('can run copen', function() command("copen") match_debug_messages { - "run_job: git --no-pager --version", + "run_job: git --version", "attach(2): Attaching", "attach(2): Non-normal buffer", } @@ -597,6 +593,7 @@ describe('gitsigns', function() | ]]} + -- Reset feed("mhr") sleep(10) @@ -662,20 +659,19 @@ describe('gitsigns', function() command("write") sleep(40) + local messages = { "attach(1): Attaching", - "run_job: git --no-pager rev-parse --show-toplevel --absolute-git-dir --abbrev-ref HEAD", - p"run_job: git .* ls%-files .*", - p"run_job: git .* config user.name", + "run_job: git rev-parse --show-toplevel --absolute-git-dir --abbrev-ref HEAD", + p"run_job: git .* ls%-files .*/newfile.txt", + "run_job: git config user.name", "watch_index(1): Watching index", - p"run_job: git .* show :0:newfile.txt" + "run_job: git --no-pager show :0:newfile.txt", } - if not advanced_features then table.insert(messages, p'run_job: git .* diff .* /tmp/lua_.* /tmp/lua_.*') end - - local jobs = advanced_features and 8 or 9 + local jobs = advanced_features and 6 or 7 table.insert(messages, "update(1): updates: 1, jobs: "..jobs) match_debug_messages(messages) @@ -746,7 +742,7 @@ describe('gitsigns', function() <5C written | ]]} - -- Reset + -- -- Reset git{"reset"} screen:expect{grid=[[ diff --git a/types/plenary/async_lib/async.d.tl b/types/plenary/async_lib/async.d.tl index 683090a..6302375 100644 --- a/types/plenary/async_lib/async.d.tl +++ b/types/plenary/async_lib/async.d.tl @@ -32,43 +32,37 @@ local record Async type async_fun4_3 = function (A1,A2,A3,A4) : future3 type async_fun4_4 = function (A1,A2,A3,A4) : future4 - type async_fun5_0 = function (A1,A2,A3,A4,A5) : future0 - type async_fun5_1 = function (A1,A2,A3,A4,A5) : future1 - type async_fun5_4 = function (A1,A2,A3,A4,A5) : future4 - await: function (future0 ): () await: function (future1 ): A1 await: function (future2 ): A1,A2 await: function (future3 ): A1,A2,A3 await: function (future4): A1,A2,A3,A4 - async: function (function() : R1,R2,R3 ): async_fun0_3 - async: function (function() : R1,R2 ): async_fun0_2 - async: function (function() : R1 ): async_fun0_1 async: function (function() : () ): async_fun0 - async: function (function(A1) : R1,R2,R3,R4): async_fun1_4 - async: function (function(A1) : R1,R2,R3 ): async_fun1_3 - async: function (function(A1) : R1,R2 ): async_fun1_2 - async: function (function(A1) : R1 ): async_fun1_1 + async: function (function() : R1 ): async_fun0_1 + async: function (function() : R1,R2 ): async_fun0_2 + async: function (function() : R1,R2,R3 ): async_fun0_3 + async: function (function() : R1,R2,R3,R4): async_fun0_4 async: function (function(A1) : () ): async_fun1 - async: function (function(A1,A2) : R1,R2,R3,R4): async_fun2_4 - async: function (function(A1,A2) : R1,R2,R3 ): async_fun2_3 - async: function (function(A1,A2) : R1,R2 ): async_fun2_2 - async: function (function(A1,A2) : R1 ): async_fun2_1 + async: function (function(A1) : R1 ): async_fun1_1 + async: function (function(A1) : R1,R2 ): async_fun1_2 + async: function (function(A1) : R1,R2,R3 ): async_fun1_3 + async: function (function(A1) : R1,R2,R3,R4): async_fun1_4 async: function (function(A1,A2) : () ): async_fun2 - async: function (function(A1,A2,A3) : R1,R2,R3,R4): async_fun3_4 - async: function (function(A1,A2,A3) : R1,R2,R3 ): async_fun3_3 - async: function (function(A1,A2,A3) : R1,R2 ): async_fun3_2 - async: function (function(A1,A2,A3) : R1 ): async_fun3_1 + async: function (function(A1,A2) : R1 ): async_fun2_1 + async: function (function(A1,A2) : R1,R2 ): async_fun2_2 + async: function (function(A1,A2) : R1,R2,R3 ): async_fun2_3 + async: function (function(A1,A2) : R1,R2,R3,R4): async_fun2_4 async: function (function(A1,A2,A3) : () ): async_fun3 - async: function(function(A1,A2,A3,A4) : R1,R2,R3,R4): async_fun4_4 - async: function (function(A1,A2,A3,A4) : R1,R2,R3 ): async_fun4_3 - async: function (function(A1,A2,A3,A4) : R1,R2 ): async_fun4_2 - async: function (function(A1,A2,A3,A4) : R1 ): async_fun4_1 + async: function (function(A1,A2,A3) : R1 ): async_fun3_1 + async: function (function(A1,A2,A3) : R1,R2 ): async_fun3_2 + async: function (function(A1,A2,A3) : R1,R2,R3 ): async_fun3_3 + async: function (function(A1,A2,A3) : R1,R2,R3,R4): async_fun3_4 async: function (function(A1,A2,A3,A4) : () ): async_fun4 - async: function(function(A1,A2,A3,A4,A5) : R1,R2,R3,R4): async_fun5_4 - async: function (function(A1,A2,A3,A4,A5) : R1 ): async_fun5_1 - async: function (function(A1,A2,A3,A4,A5) : () ): async_fun5_0 + async: function (function(A1,A2,A3,A4) : R1 ): async_fun4_1 + async: function (function(A1,A2,A3,A4) : R1,R2 ): async_fun4_2 + async: function (function(A1,A2,A3,A4) : R1,R2,R3 ): async_fun4_3 + async: function(function(A1,A2,A3,A4) : R1,R2,R3,R4): async_fun4_4 wrap: function (function( function()) , integer): async_fun0 wrap: function (function( function(R1)) , integer): async_fun0_1 @@ -94,8 +88,7 @@ local record Async wrap: function (function(A1,A2,A3,A4,function(R1)) , integer): async_fun4_1 wrap: function (function(A1,A2,A3,A4,function(R1,R2)) , integer): async_fun4_2 wrap: function (function(A1,A2,A3,A4,function(R1,R2,R3)) , integer): async_fun4_3 - wrap: function (function(A1,A2,A3,A4,A5,function(R1)), integer): async_fun5_1 - wrap: function(function(A1,A2,A3,A4,A5,function(R1,R2,R3,R4)), integer): async_fun5_4 + wrap: function(function(A1,A2,A3,A4,function(R1,R2,R3,R4)), integer): async_fun4_4 scheduler: future execute: function