Revert "Refactor git commands to be more OOP"

This reverts commit 718d61b712.
This commit is contained in:
Lewis Russell 2021-04-10 09:34:15 +01:00
parent dbd4c8a87d
commit f11dc80ec4
6 changed files with 951 additions and 670 deletions

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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<boolean>
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<M.BlameInfo>
file_info : function(Obj): a.future4<string, string, string, boolean>
ensure_file_in_index : function(Obj): a.future0
stage_hunks : function(Obj, {Hunk}, boolean): a.future0
new : function(Obj, string): a.future1<Obj>
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<string, string, string, boolean>
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<M.BlameInfo>
get_repo_info : function(string): a.future3<string,string,string>
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

View File

@ -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=[[

View File

@ -32,43 +32,37 @@ local record Async
type async_fun4_3 = function <A1,A2,A3,A4,R1,R2,R3> (A1,A2,A3,A4) : future3<R1,R2,R3>
type async_fun4_4 = function <A1,A2,A3,A4,R1,R2,R3,R4> (A1,A2,A3,A4) : future4<R1,R2,R3,R4>
type async_fun5_0 = function <A1,A2,A3,A4,A5> (A1,A2,A3,A4,A5) : future0
type async_fun5_1 = function <A1,A2,A3,A4,A5,R1> (A1,A2,A3,A4,A5) : future1<R1>
type async_fun5_4 = function <A1,A2,A3,A4,A5,R1,R2,R3,R4> (A1,A2,A3,A4,A5) : future4<R1,R2,R3,R4>
await: function (future0 ): ()
await: function<A1> (future1<A1> ): A1
await: function<A1,A2> (future2<A1,A2> ): A1,A2
await: function<A1,A2,A3> (future3<A1,A2,A3> ): A1,A2,A3
await: function<A1,A2,A3,A4> (future4<A1,A2,A3,A4>): A1,A2,A3,A4
async: function<R1,R2,R3> (function() : R1,R2,R3 ): async_fun0_3 <R1,R2,R3>
async: function<R1,R2> (function() : R1,R2 ): async_fun0_2 <R1,R2>
async: function<R1> (function() : R1 ): async_fun0_1 <R1>
async: function (function() : () ): async_fun0
async: function<A1,R1,R2,R3,R4> (function(A1) : R1,R2,R3,R4): async_fun1_4 <A1,R1,R2,R3,R4>
async: function<A1,R1,R2,R3> (function(A1) : R1,R2,R3 ): async_fun1_3 <A1,R1,R2,R3>
async: function<A1,R1,R2> (function(A1) : R1,R2 ): async_fun1_2 <A1,R1,R2>
async: function<A1,R1> (function(A1) : R1 ): async_fun1_1 <A1,R1>
async: function<R1> (function() : R1 ): async_fun0_1 <R1>
async: function<R1,R2> (function() : R1,R2 ): async_fun0_2 <R1,R2>
async: function<R1,R2,R3> (function() : R1,R2,R3 ): async_fun0_3 <R1,R2,R3>
async: function<R1,R2,R3,R4> (function() : R1,R2,R3,R4): async_fun0_4 <R1,R2,R3,R4>
async: function<A1> (function(A1) : () ): async_fun1 <A1>
async: function<A1,A2,R1,R2,R3,R4> (function(A1,A2) : R1,R2,R3,R4): async_fun2_4 <A1,A2,R1,R2,R3,R4>
async: function<A1,A2,R1,R2,R3> (function(A1,A2) : R1,R2,R3 ): async_fun2_3 <A1,A2,R1,R2,R3>
async: function<A1,A2,R1,R2> (function(A1,A2) : R1,R2 ): async_fun2_2 <A1,A2,R1,R2>
async: function<A1,A2,R1> (function(A1,A2) : R1 ): async_fun2_1 <A1,A2,R1>
async: function<A1,R1> (function(A1) : R1 ): async_fun1_1 <A1,R1>
async: function<A1,R1,R2> (function(A1) : R1,R2 ): async_fun1_2 <A1,R1,R2>
async: function<A1,R1,R2,R3> (function(A1) : R1,R2,R3 ): async_fun1_3 <A1,R1,R2,R3>
async: function<A1,R1,R2,R3,R4> (function(A1) : R1,R2,R3,R4): async_fun1_4 <A1,R1,R2,R3,R4>
async: function<A1,A2> (function(A1,A2) : () ): async_fun2 <A1,A2>
async: function<A1,A2,A3,R1,R2,R3,R4> (function(A1,A2,A3) : R1,R2,R3,R4): async_fun3_4 <A1,A2,A3,R1,R2,R3,R4>
async: function<A1,A2,A3,R1,R2,R3> (function(A1,A2,A3) : R1,R2,R3 ): async_fun3_3 <A1,A2,A3,R1,R2,R3>
async: function<A1,A2,A3,R1,R2> (function(A1,A2,A3) : R1,R2 ): async_fun3_2 <A1,A2,A3,R1,R2>
async: function<A1,A2,A3,R1> (function(A1,A2,A3) : R1 ): async_fun3_1 <A1,A2,A3,R1>
async: function<A1,A2,R1> (function(A1,A2) : R1 ): async_fun2_1 <A1,A2,R1>
async: function<A1,A2,R1,R2> (function(A1,A2) : R1,R2 ): async_fun2_2 <A1,A2,R1,R2>
async: function<A1,A2,R1,R2,R3> (function(A1,A2) : R1,R2,R3 ): async_fun2_3 <A1,A2,R1,R2,R3>
async: function<A1,A2,R1,R2,R3,R4> (function(A1,A2) : R1,R2,R3,R4): async_fun2_4 <A1,A2,R1,R2,R3,R4>
async: function<A1,A2,A3> (function(A1,A2,A3) : () ): async_fun3 <A1,A2,A3>
async: function<A1,A2,A3,A4,R1,R2,R3,R4>(function(A1,A2,A3,A4) : R1,R2,R3,R4): async_fun4_4 <A1,A2,A3,A4,R1,R2,R3,R4>
async: function<A1,A2,A3,A4,R1,R2,R3> (function(A1,A2,A3,A4) : R1,R2,R3 ): async_fun4_3 <A1,A2,A3,A4,R1,R2,R3>
async: function<A1,A2,A3,A4,R1,R2> (function(A1,A2,A3,A4) : R1,R2 ): async_fun4_2 <A1,A2,A3,A4,R1,R2>
async: function<A1,A2,A3,A4,R1> (function(A1,A2,A3,A4) : R1 ): async_fun4_1 <A1,A2,A3,A4,R1>
async: function<A1,A2,A3,R1> (function(A1,A2,A3) : R1 ): async_fun3_1 <A1,A2,A3,R1>
async: function<A1,A2,A3,R1,R2> (function(A1,A2,A3) : R1,R2 ): async_fun3_2 <A1,A2,A3,R1,R2>
async: function<A1,A2,A3,R1,R2,R3> (function(A1,A2,A3) : R1,R2,R3 ): async_fun3_3 <A1,A2,A3,R1,R2,R3>
async: function<A1,A2,A3,R1,R2,R3,R4> (function(A1,A2,A3) : R1,R2,R3,R4): async_fun3_4 <A1,A2,A3,R1,R2,R3,R4>
async: function<A1,A2,A3,A4> (function(A1,A2,A3,A4) : () ): async_fun4 <A1,A2,A3,A4>
async: function<A1,A2,A3,A4,A5,R1,R2,R3,R4>(function(A1,A2,A3,A4,A5) : R1,R2,R3,R4): async_fun5_4 <A1,A2,A3,A4,A5,R1,R2,R3,R4>
async: function<A1,A2,A3,A4,A5,R1> (function(A1,A2,A3,A4,A5) : R1 ): async_fun5_1 <A1,A2,A3,A4,A5,R1>
async: function<A1,A2,A3,A4,A5> (function(A1,A2,A3,A4,A5) : () ): async_fun5_0 <A1,A2,A3,A4,A5>
async: function<A1,A2,A3,A4,R1> (function(A1,A2,A3,A4) : R1 ): async_fun4_1 <A1,A2,A3,A4,R1>
async: function<A1,A2,A3,A4,R1,R2> (function(A1,A2,A3,A4) : R1,R2 ): async_fun4_2 <A1,A2,A3,A4,R1,R2>
async: function<A1,A2,A3,A4,R1,R2,R3> (function(A1,A2,A3,A4) : R1,R2,R3 ): async_fun4_3 <A1,A2,A3,A4,R1,R2,R3>
async: function<A1,A2,A3,A4,R1,R2,R3,R4>(function(A1,A2,A3,A4) : R1,R2,R3,R4): async_fun4_4 <A1,A2,A3,A4,R1,R2,R3,R4>
wrap: function (function( function()) , integer): async_fun0
wrap: function<R1> (function( function(R1)) , integer): async_fun0_1 <R1>
@ -94,8 +88,7 @@ local record Async
wrap: function<A1,A2,A3,A4,R1> (function(A1,A2,A3,A4,function(R1)) , integer): async_fun4_1 <A1,A2,A3,A4,R1>
wrap: function<A1,A2,A3,A4,R1,R2> (function(A1,A2,A3,A4,function(R1,R2)) , integer): async_fun4_2 <A1,A2,A3,A4,R1,R2>
wrap: function<A1,A2,A3,A4,R1,R2,R3> (function(A1,A2,A3,A4,function(R1,R2,R3)) , integer): async_fun4_3 <A1,A2,A3,A4,R1,R2,R3>
wrap: function<A1,A2,A3,A4,A5,R1> (function(A1,A2,A3,A4,A5,function(R1)), integer): async_fun5_1 <A1,A2,A3,A4,A5,R1>
wrap: function<A1,A2,A3,A4,A5,R1,R2,R3,R4>(function(A1,A2,A3,A4,A5,function(R1,R2,R3,R4)), integer): async_fun5_4 <A1,A2,A3,A4,A5,R1,R2,R3,R4>
wrap: function<A1,A2,A3,A4,R1,R2,R3,R4>(function(A1,A2,A3,A4,function(R1,R2,R3,R4)), integer): async_fun4_4 <A1,A2,A3,A4,R1,R2,R3,R4>
scheduler: future
execute: function