diff --git a/README.md b/README.md index 219ec06e..456697d1 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,11 @@
reset_hunk +
+ Stage a hunk +
+ hunk_stage +
Navigate through hunks
@@ -72,6 +77,13 @@ diff_preference
+## Supported Neovim versions: +- Neovim > 0.5 + +## Supported Opperating System: +- linux-gnu* +- Darwin + ## Prerequisites - [Git](https://git-scm.com/) - [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter) @@ -374,8 +386,9 @@ vim.api.nvim_set_keymap('n', 'gq', ':VGit hunks_quickfix_list', { | setup | Sets up the plugin for success | | toggle_buffer_hunks | Shows hunk signs on buffers/Hides hunk signs on buffers | | toggle_buffer_blames | Enables blames feature on buffers /Disables blames feature on buffers | -| hunk_preview | Opens a VGit view of a hunk, if cursor is on a line with a git change | -| hunk_reset | Removes the hunk from the buffer | +| hunk_preview | Opens a VGit view of a hunk, if cursor is on a hunk | +| hunk_reset | Removes the hunk from the buffer, if cursor is on a hunk | +| hunk_stage | Stages a hunk, if cursor is on a hunk | | hunk_down | Navigate downward through a hunk | | hunk_up | Navigate upwards through a hunk | | buffer_preview | Shows the current differences in lines in the current buffer | @@ -397,5 +410,4 @@ vim.api.nvim_set_keymap('n', 'gq', ':VGit hunks_quickfix_list', { ## Similar Git Plugins - [vim-fugitive](https://github.com/tpope/vim-fugitive) - [vim-gitgutter](https://github.com/airblade/vim-gitgutter) -- [gitsigns.nvim](https://github.com/lewis6991/gitsigns.nvim) -- [neogit](https://github.com/TimUntersberger/neogit) +- [vim-signify](https://github.com/mhinz/vim-signify) diff --git a/lua/vgit/algorithms.lua b/lua/vgit/algorithms.lua deleted file mode 100644 index a45d3b46..00000000 --- a/lua/vgit/algorithms.lua +++ /dev/null @@ -1,175 +0,0 @@ -local M = {} - -local function create_hunk(hunks, start, finish, diff, type) - if type == 'remove' then - if start < 0 then - start = 0 - end - hunks[#hunks + 1] = { - start = start, - finish = start, - type = type, - diff = diff, - } - elseif type == 'change' then - local hunk = { - start = start + 1, - finish = finish, - type = type, - diff = diff, - } - hunks[#hunks + 1] = hunk - if hunk.start < 1 then - hunk.start = 1 - hunk.finish = 1 - end - else - hunks[#hunks + 1] = { - start = start, - finish = finish - 1, - type = type, - diff = diff, - } - end -end - -M.myers_difference = function(a_lines, b_lines) - local steps = { [1] = { x = 0, history = {} } } - local a_len = #a_lines - local b_len = #b_lines - local max = a_len + b_len + 1 - for d = 0, max do - for k = -d, d, 2 do - local x, history - local go_down = (k == -d or (k ~= d and steps[k - 1].x < steps[k + 1].x)) - if go_down then - local step = steps[k + 1] - x = step.x - history = step.history - else - local step = steps[k - 1] - x = step.x + 1 - history = step.history - end - local temp_history = history - history = {} - for i = 1, #temp_history do - history[i] = temp_history[i] - end - local y = x - k - if 1 <= y and y <= b_len and go_down then - history[#history + 1] = { 1, b_lines[y] } - elseif 1 <= x and x <= a_len then - history[#history + 1] = { -1, a_lines[x] } - end - while x < a_len and y < b_len and a_lines[x + 1] == b_lines[y + 1] do - x = x + 1 - y = y + 1 - history[#history + 1] = { 0, a_lines[x] } - end - if x >= a_len and y >= b_len then - return history - else - steps[k] = { x = x, history = history } - end - end - end -end - -M.hunks = function(a_lines, b_lines) - local diffs = M.myers_difference(a_lines, b_lines) - local hunks = {} - local processing_hunk = false - local predicted_type = nil - local temp_diff = {} - local start = 0 - local temp_added_lines = 0 - local temp_removed_lines = 0 - local r_lines = {} - for line_number = 1, #diffs do - local diff = diffs[line_number] - local type = diff[1] - local line = diff[2] - if not processing_hunk then - if type == 1 then - predicted_type = 'add' - processing_hunk = true - temp_diff[#temp_diff + 1] = string.format('+%s', line) - start = line_number - temp_added_lines = temp_added_lines + 1 - elseif type == -1 then - predicted_type = 'undecided' - processing_hunk = true - temp_diff[#temp_diff + 1] = string.format('-%s', line) - r_lines[#r_lines + 1] = line_number - start = line_number - end - else - if type == 1 then - temp_diff[#temp_diff + 1] = string.format('+%s', line) - if predicted_type == 'undecided' then - predicted_type = 'change' - end - temp_added_lines = temp_added_lines + 1 - elseif type == -1 then - temp_diff[#temp_diff + 1] = string.format('-%s', line) - r_lines[#r_lines + 1] = line_number - temp_removed_lines = temp_removed_lines + 1 - else - local removed_lines = 0 - if predicted_type == 'undecided' then - predicted_type = 'remove' - for i = 1, #r_lines do - local lnum = r_lines[i] - if line_number > lnum then - removed_lines = removed_lines + 1 - end - end - removed_lines = removed_lines - temp_removed_lines - elseif predicted_type == 'change' then - for i = 1, #r_lines do - local lnum = r_lines[i] - if line_number > lnum then - removed_lines = removed_lines + 1 - end - end - removed_lines = removed_lines - temp_removed_lines - else - removed_lines = #r_lines - end - if #temp_diff ~= 0 then - create_hunk( - hunks, - start - removed_lines, - start - removed_lines + temp_added_lines, - temp_diff, - predicted_type - ) - end - temp_added_lines = 0 - temp_removed_lines = 0 - processing_hunk = false - temp_diff = {} - predicted_type = nil - start = 0 - end - end - end - if #temp_diff ~= 0 then - local removed_lines = #r_lines - if predicted_type == 'undecided' then - predicted_type = 'remove' - removed_lines = removed_lines - temp_removed_lines - end - create_hunk( - hunks, - start - removed_lines, - start - removed_lines + temp_added_lines, - temp_diff, - predicted_type - ) - end - return hunks -end - -return M diff --git a/lua/vgit/defer.lua b/lua/vgit/defer.lua index cf2b9a65..e4a7b70c 100644 --- a/lua/vgit/defer.lua +++ b/lua/vgit/defer.lua @@ -16,7 +16,7 @@ M.throttle_leading = function(fn, ms) end end -function M.debounce_trailing(fn, ms) +M.debounce_trailing = function(fn, ms) local timer = vim.loop.new_timer() return function(...) local argv = {...} diff --git a/lua/vgit/fs.lua b/lua/vgit/fs.lua index ea3dae23..858ce560 100644 --- a/lua/vgit/fs.lua +++ b/lua/vgit/fs.lua @@ -5,7 +5,7 @@ local vim = vim local M = {} -M.relative_path = function(filepath) +M.relative_filename = function(filepath) assert(type(filepath) == 'string', 'type error :: expected string') local cwd = vim.loop.cwd() if not cwd or not filepath then return filepath end @@ -19,13 +19,26 @@ M.relative_path = function(filepath) return filepath end +M.short_filename = function(filepath) + assert(type(filepath) == 'string', 'type error :: expected string') + local filename = '' + for i = #filepath, 1, -1 do + local letter = filepath:sub(i, i) + if letter == '/' then + break + end + filename = letter .. filename + end + return filename +end + M.project_relative_filename = function(filepath, project_files) assert(type(filepath) == 'string', 'type error :: expected string') assert(vim.tbl_islist(project_files), 'type error :: expected table of type list') - table.sort(project_files) if filepath == '' then - return filepath + return nil end + table.sort(project_files) for i = #filepath, 1, -1 do local letter = filepath:sub(i, i) local new_project_files = {} @@ -39,7 +52,19 @@ M.project_relative_filename = function(filepath, project_files) end project_files = new_project_files end - return project_files[1] + local project_filename = project_files[1] + if project_filename then + local short_filename = M.short_filename(filepath) + local project_short_filename = M.short_filename(project_filename) + return (short_filename == project_short_filename and project_filename) or nil + end + return nil +end + +M.filename = function(buf) + assert(type(buf) == 'number', 'type error :: expected number') + local filepath = vim.api.nvim_buf_get_name(buf) + return M.relative_filename(filepath) end M.filetype = function(buf) @@ -49,12 +74,6 @@ end M.detect_filetype = pfiletype.detect -M.filename = function(buf) - assert(type(buf) == 'number', 'type error :: expected number') - local filepath = vim.api.nvim_buf_get_name(buf) - return M.relative_path(filepath) -end - M.tmpname = function() local length = 6 local res = '' diff --git a/lua/vgit/git.lua b/lua/vgit/git.lua index 751efc2e..406cd2ad 100644 --- a/lua/vgit/git.lua +++ b/lua/vgit/git.lua @@ -1,3 +1,4 @@ +local fs = require('vgit.fs') local Job = require('plenary.job') local State = require('vgit.State') local a = require('plenary.async') @@ -111,6 +112,7 @@ end M.create_hunk = function(header) local previous, current = parse_hunk_header(header) local hunk = { + header = header, start = current[1], finish = current[1] + current[2] - 1, type = nil, @@ -128,6 +130,20 @@ M.create_hunk = function(header) return hunk end +M.create_patch = function(filename, hunk) + local patch = { + string.format('diff --git a/%s b/%s', filename, filename), + 'index 000000..000000', + string.format('--- a/%s', filename), + string.format('+++ a/%s', filename), + hunk.header, + } + for i = 1, #hunk.diff do + patch[#patch + 1] = hunk.diff[i] + end + return patch +end + M.create_blame = function(info) local function split_by_whitespace(str) return vim.split(str, ' ') @@ -516,13 +532,40 @@ M.show = wrap(function(filename, commit_hash, callback) job:start() end, 3) +M.stage_hunk = wrap(function(filename, hunk, callback) + local patch = M.create_patch(filename, hunk) + local patch_filename = fs.tmpname() + fs.write_file(patch_filename, patch) + local err = {} + local job = Job:new({ + command = 'git', + args = { + 'apply', + '--cached', + '--unidiff-zero', + patch_filename, + }, + on_stderr = function(_, data, _) + err[#err + 1] = data + end, + on_exit = function() + fs.remove_file(patch_filename) + if #err ~= 0 then + return callback(err) + end + callback(nil) + end, + }) + job:start() +end, 3) + M.reset = wrap(function(filename, callback) local err = {} local job = Job:new({ command = 'git', args = { 'checkout', - 'HEAD', + M.get_diff_base(), '--', filename, }, diff --git a/lua/vgit/init.lua b/lua/vgit/init.lua index 6bfa4af7..f098e41d 100644 --- a/lua/vgit/init.lua +++ b/lua/vgit/init.lua @@ -208,8 +208,7 @@ M._blame_line = debounce_trailing(void(function(buf) if not state:get('disabled') and buffer.is_valid(buf) and bstate:contains(buf) then - local is_buf_modified = vim.api.nvim_buf_get_option(buf, 'modified') - if not is_buf_modified then + if not vim.api.nvim_buf_get_option(buf, 'modified') then local win = vim.api.nvim_get_current_win() local last_lnum_blamed = bstate:get(buf, 'last_lnum_blamed') local lnum = vim.api.nvim_win_get_cursor(win)[1] @@ -758,6 +757,39 @@ M.show_blame = throttle_leading(void(function(buf) end end), state:get('action_delay_ms')) +M.hunk_stage = throttle_leading(void(function(buf, win) + buf = buf or buffer.current() + if not state:get('disabled') + and buffer.is_valid(buf) + and bstate:contains(buf) + and not vim.api.nvim_buf_get_option(buf, 'modified') then + win = win or vim.api.nvim_get_current_win() + local lnum = vim.api.nvim_win_get_cursor(win)[1] + local selected_hunk = nil + local hunks = bstate:get(buf, 'hunks') + for i = 1, #hunks do + local hunk = hunks[i] + if lnum == 1 and hunk.start == 0 and hunk.finish == 0 then + selected_hunk = hunk + break + end + if lnum >= hunk.start and lnum <= hunk.finish then + selected_hunk = hunk + break + end + end + if selected_hunk then + local err = git.stage_hunk(bstate:get(buf, 'project_relative_filename'), selected_hunk) + scheduler() + if not err then + M._buf_update(buf) + else + logger.debug(err, 'init.lua/hunk_stage') + end + end + end +end), state:get('action_delay_ms')) + M.enabled = function() return not state:get('disabled') end diff --git a/tests/unit/algorithms_spec.lua b/tests/unit/algorithms_spec.lua deleted file mode 100644 index 5ea6738b..00000000 --- a/tests/unit/algorithms_spec.lua +++ /dev/null @@ -1,54 +0,0 @@ -local algorithms = require('vgit.algorithms') - -local it = it -local describe = describe -local eq = assert.are.same - -describe('algorithms:', function() - - describe('myers_difference', function() - - it('should compute the difference between two list tables', function() - eq(algorithms.myers_difference({ 'a', 'b', 'c', 'd' }, { 'j', 'l', 'c', 'n' }), { - { -1, 'a' }, - { -1, 'b' }, - { 1, 'j' }, - { 1, 'l' }, - { 0, 'c' }, - { -1, 'd' }, - { 1, 'n' }, - }) - end) - - end) - - describe('hunks', function() - - it('should compute the hunks of difference between the two different lines', function() - eq(algorithms.hunks({ 'a', 'b', 'c', 'd' }, { 'j', 'l', 'c', 'n' }), { - { - diff = { - '-a', - '-b', - '+j', - '+l', - }, - finish = 2, - start = 1, - type = 'change', - }, - { - diff = { - '-d', - '+n', - }, - finish = 4, - start = 4, - type = 'change', - } - }) - end) - - end) - -end) diff --git a/tests/unit/fs_spec.lua b/tests/unit/fs_spec.lua index 2001bde4..7cc598a4 100644 --- a/tests/unit/fs_spec.lua +++ b/tests/unit/fs_spec.lua @@ -49,41 +49,72 @@ describe('fs:', function() end) - describe('relative_path', function() + describe('relative_filename', function() it('should throw error on invalid argument types', function() assert.has_error(function() - fs.relative_path(true) + fs.relative_filename(true) end) assert.has_error(function() - fs.relative_path({}) + fs.relative_filename({}) end) assert.has_error(function() - fs.relative_path(1) + fs.relative_filename(1) end) assert.has_error(function() - fs.relative_path(nil) + fs.relative_filename(nil) end) assert.has_error(function() - fs.relative_path(function() end) + fs.relative_filename(function() end) end) end) it('should convert an absolute path to a relative path', function() local current = vim.loop.cwd() local path = current .. '/lua/vgit/init.lua' - local filepath = fs.relative_path(path) + local filepath = fs.relative_filename(path) eq(filepath, 'lua/vgit/init.lua') end) it('should return the unchanged path if it is not absolute', function() local path = 'lua/vgit/init.lua' - local filepath = fs.relative_path(path) + local filepath = fs.relative_filename(path) eq(filepath, 'lua/vgit/init.lua') end) end) + describe('short_filename', function() + + it('should throw error on invalid argument types', function() + assert.has_error(function() + fs.relative_filename(true) + end) + assert.has_error(function() + fs.relative_filename({}) + end) + assert.has_error(function() + fs.relative_filename(1) + end) + assert.has_error(function() + fs.relative_filename(nil) + end) + assert.has_error(function() + fs.relative_filename(function() end) + end) + end) + + it('should take a long path and give you the filename', function() + eq(fs.short_filename('lua/vgit/init.lua'), 'init.lua') + eq(fs.short_filename('/init.lua'), 'init.lua') + eq(fs.short_filename('a/b/c/d/init.lua'), 'init.lua') + eq(fs.short_filename('init.lua'), 'init.lua') + eq(fs.short_filename(''), '') + eq(fs.short_filename('init/.lua'), '.lua') + end) + + end) + describe('filetype', function() it('should throw error on invalid argument types', function()