feat(hunks): stage selected hunks
This commit is contained in:
@@ -53,7 +53,7 @@ vim.pack.add({ "https://git.owall.se/oscar/git.nvim" })
|
||||
| `<Plug>(git-blame-popup)` | Cursor-line blame popup |
|
||||
| `<Plug>(git-blame-view)` | Toggle full-file blame overlay |
|
||||
| `<Plug>(git-hunk-select)` | Visually select the hunk under cursor |
|
||||
| `<Plug>(git-hunk-stage-toggle)` | Stage / unstage hunk |
|
||||
| `<Plug>(git-hunk-stage-toggle)` | Stage / unstage hunk or visual hunks |
|
||||
| `<Plug>(git-hunk-reset)` | Reset hunk |
|
||||
| `<Plug>(git-hunk-overlay-toggle)` | Toggle the in-buffer diff overlay |
|
||||
| `<Plug>(git-hunk-next)` | Jump to next hunk |
|
||||
|
||||
+112
-35
@@ -621,20 +621,6 @@ local function hunk_at(hunks, row)
|
||||
return nil
|
||||
end
|
||||
|
||||
---@param state ow.Git.Hunks.BufState
|
||||
---@param buf integer
|
||||
---@param row integer 1-indexed cursor line
|
||||
---@return ow.Git.Hunks.Hunk?
|
||||
local function staged_hunk_at(state, buf, row)
|
||||
local line_count = vim.api.nvim_buf_line_count(buf)
|
||||
for _, s in ipairs(staged_signs(state, line_count)) do
|
||||
if s.row == row - 1 then
|
||||
return s.hunk
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
---@param buf integer?
|
||||
---@return integer buf
|
||||
---@return ow.Git.Hunks.BufState? state
|
||||
@@ -748,17 +734,20 @@ end
|
||||
---@param h ow.Git.Hunks.Hunk
|
||||
---@param old_lines string[]
|
||||
---@param rel string
|
||||
---@return string patch
|
||||
---@param include_header boolean?
|
||||
---@param context integer?
|
||||
---@return string[] patch lines
|
||||
---@return boolean zero_context
|
||||
local function build_patch(h, old_lines, rel)
|
||||
local function build_patch_lines(h, old_lines, rel, include_header, context)
|
||||
context = context or PATCH_CONTEXT
|
||||
local old_before, new_before = hunk_offsets(h)
|
||||
local pre = {}
|
||||
for i = math.max(old_before - PATCH_CONTEXT + 1, 1), old_before do
|
||||
for i = math.max(old_before - context + 1, 1), old_before do
|
||||
pre[#pre + 1] = old_lines[i] or ""
|
||||
end
|
||||
local post = {}
|
||||
local after = old_before + h.old_count
|
||||
for i = after + 1, math.min(after + PATCH_CONTEXT, #old_lines) do
|
||||
for i = after + 1, math.min(after + context, #old_lines) do
|
||||
post[#post + 1] = old_lines[i] or ""
|
||||
end
|
||||
local old_n = #pre + h.old_count + #post
|
||||
@@ -786,9 +775,29 @@ local function build_patch(h, old_lines, rel)
|
||||
for _, l in ipairs(post) do
|
||||
body[#body + 1] = " " .. l
|
||||
end
|
||||
local lines = { "--- a/" .. rel, "+++ b/" .. rel }
|
||||
local lines = {}
|
||||
if include_header ~= false then
|
||||
lines = { "--- a/" .. rel, "+++ b/" .. rel }
|
||||
end
|
||||
vim.list_extend(lines, body)
|
||||
return table.concat(lines, "\n") .. "\n", #pre == 0 and #post == 0
|
||||
return lines, #pre == 0 and #post == 0
|
||||
end
|
||||
|
||||
---@param hunks ow.Git.Hunks.Hunk[]
|
||||
---@param old_lines string[]
|
||||
---@param rel string
|
||||
---@param context integer?
|
||||
---@return string patch
|
||||
---@return boolean zero_context
|
||||
local function build_patch(hunks, old_lines, rel, context)
|
||||
local lines = { "--- a/" .. rel, "+++ b/" .. rel }
|
||||
local zero_context = false
|
||||
for _, h in ipairs(hunks) do
|
||||
local body, zero = build_patch_lines(h, old_lines, rel, false, context)
|
||||
vim.list_extend(lines, body)
|
||||
zero_context = zero_context or zero
|
||||
end
|
||||
return table.concat(lines, "\n") .. "\n", zero_context
|
||||
end
|
||||
|
||||
---@param state ow.Git.Hunks.BufState
|
||||
@@ -818,27 +827,95 @@ local function apply_patch(state, buf, patch, zero_context)
|
||||
})
|
||||
end
|
||||
|
||||
---@param h ow.Git.Hunks.Hunk
|
||||
---@return integer first 1-indexed buffer line
|
||||
---@return integer last 1-indexed buffer line
|
||||
local function hunk_range(h)
|
||||
if h.type == "delete" then
|
||||
local line = math.max(h.new_start, 1)
|
||||
return line, line
|
||||
end
|
||||
return h.new_start, h.new_start + h.new_count - 1
|
||||
end
|
||||
|
||||
---@param h ow.Git.Hunks.Hunk
|
||||
---@param first integer
|
||||
---@param last integer
|
||||
---@return boolean
|
||||
local function hunk_overlaps(h, first, last)
|
||||
local h_first, h_last = hunk_range(h)
|
||||
return h_first <= last and h_last >= first
|
||||
end
|
||||
|
||||
---@param state ow.Git.Hunks.BufState
|
||||
---@param buf integer
|
||||
---@param first integer
|
||||
---@param last integer
|
||||
---@return ow.Git.Hunks.Hunk[]
|
||||
local function staged_hunks_in_range(state, buf, first, last)
|
||||
local out = {}
|
||||
local seen = {}
|
||||
local line_count = vim.api.nvim_buf_line_count(buf)
|
||||
for _, s in ipairs(staged_signs(state, line_count)) do
|
||||
local line = s.row + 1
|
||||
if line >= first and line <= last and not seen[s.hunk] then
|
||||
seen[s.hunk] = true
|
||||
table.insert(out, s.hunk)
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
---@param first integer
|
||||
---@param last integer
|
||||
---@param buf? integer
|
||||
function M.toggle_stage(buf)
|
||||
function M.toggle_stage_range(first, last, buf)
|
||||
buf = resolve_buf(buf)
|
||||
local state = states[buf]
|
||||
if not state then
|
||||
if not state or not state.index then
|
||||
return
|
||||
end
|
||||
if first > last then
|
||||
first, last = last, first
|
||||
end
|
||||
local unstaged = {}
|
||||
for _, h in ipairs(state.hunks) do
|
||||
if hunk_overlaps(h, first, last) then
|
||||
table.insert(unstaged, h)
|
||||
end
|
||||
end
|
||||
if #unstaged > 0 then
|
||||
local context = #unstaged > 1 and 0 or nil
|
||||
local patch, zero =
|
||||
build_patch(unstaged, state.index, state.rel, context)
|
||||
apply_patch(state, buf, patch, zero)
|
||||
return
|
||||
end
|
||||
local staged = staged_hunks_in_range(state, buf, first, last)
|
||||
if #staged > 0 then
|
||||
local inverted = {}
|
||||
for _, h in ipairs(staged) do
|
||||
table.insert(inverted, invert(h))
|
||||
end
|
||||
local context = #inverted > 1 and 0 or nil
|
||||
local patch, zero =
|
||||
build_patch(inverted, state.index, state.rel, context)
|
||||
apply_patch(state, buf, patch, zero)
|
||||
return
|
||||
end
|
||||
util.warning("git hunks: no hunk in selection")
|
||||
end
|
||||
|
||||
---@param buf? integer
|
||||
function M.toggle_stage(buf)
|
||||
local row = vim.api.nvim_win_get_cursor(0)[1]
|
||||
local unstaged = hunk_at(state.hunks, row)
|
||||
if unstaged and state.index then
|
||||
local patch, zero = build_patch(unstaged, state.index, state.rel)
|
||||
apply_patch(state, buf, patch, zero)
|
||||
return
|
||||
end
|
||||
local staged = staged_hunk_at(state, buf, row)
|
||||
if staged and state.index then
|
||||
local patch, zero = build_patch(invert(staged), state.index, state.rel)
|
||||
apply_patch(state, buf, patch, zero)
|
||||
return
|
||||
end
|
||||
util.warning("git hunks: no hunk at cursor")
|
||||
M.toggle_stage_range(row, row, buf)
|
||||
end
|
||||
|
||||
---@param buf? integer
|
||||
function M.toggle_stage_selection(buf)
|
||||
local cursor_line = vim.api.nvim_win_get_cursor(0)[1]
|
||||
M.toggle_stage_range(vim.fn.line("v"), cursor_line, buf)
|
||||
end
|
||||
|
||||
---@param buf? integer
|
||||
|
||||
@@ -308,6 +308,9 @@ end, { silent = true, desc = "Jump to previous git hunk" })
|
||||
vim.keymap.set("n", "<Plug>(git-hunk-stage-toggle)", function()
|
||||
require("git.hunks").toggle_stage()
|
||||
end, { silent = true, desc = "Stage or unstage the hunk under cursor" })
|
||||
vim.keymap.set("x", "<Plug>(git-hunk-stage-toggle)", function()
|
||||
require("git.hunks").toggle_stage_selection()
|
||||
end, { silent = true, desc = "Stage or unstage selected git hunks" })
|
||||
vim.keymap.set("n", "<Plug>(git-hunk-reset)", function()
|
||||
require("git.hunks").reset_hunk()
|
||||
end, { silent = true, desc = "Reset hunk under cursor" })
|
||||
|
||||
@@ -359,6 +359,49 @@ t.test("toggle_stage stages only the hunk under the cursor", function()
|
||||
)
|
||||
end)
|
||||
|
||||
t.test("toggle_stage_range stages every hunk in the selected lines", function()
|
||||
local dir, buf = setup("a\nb\nc\nd\ne\n", "A\nb\nC\nd\nE\n")
|
||||
hunks.toggle_stage_range(1, 3, buf)
|
||||
t.wait_for(function()
|
||||
return #assert(hunks.state(buf)).staged == 2
|
||||
end, "the selected hunks to land in the index")
|
||||
t.eq(
|
||||
h.git(dir, "show", ":0:a.txt").stdout,
|
||||
"A\nb\nC\nd\ne",
|
||||
"only the hunks touched by the selection are staged"
|
||||
)
|
||||
end)
|
||||
|
||||
t.test(
|
||||
"toggle_stage_range stages a hunk when selection starts inside it",
|
||||
function()
|
||||
local dir, buf = setup("a\nb\nc\n", "A\nB\nc\n")
|
||||
hunks.toggle_stage_range(2, 2, buf)
|
||||
t.wait_for(function()
|
||||
return h.git(dir, "diff", "--cached", "--name-only").stdout ~= ""
|
||||
end, "the touched hunk to land in the index")
|
||||
t.eq(h.git(dir, "show", ":0:a.txt").stdout, "A\nB\nc")
|
||||
end
|
||||
)
|
||||
|
||||
t.test("toggle_stage_range unstages selected staged hunks", function()
|
||||
local dir, buf = setup("a\nb\nc\nd\ne\n", "A\nb\nC\nd\nE\n")
|
||||
hunks.toggle_stage_range(1, 5, buf)
|
||||
t.wait_for(function()
|
||||
return #assert(hunks.state(buf)).staged == 3
|
||||
end, "all hunks to land in the index")
|
||||
|
||||
hunks.toggle_stage_range(1, 3, buf)
|
||||
t.wait_for(function()
|
||||
return #assert(hunks.state(buf)).staged == 1
|
||||
end, "the selected staged hunks to be unstaged")
|
||||
t.eq(
|
||||
h.git(dir, "show", ":0:a.txt").stdout,
|
||||
"a\nb\nc\nd\nE",
|
||||
"only the unselected staged hunk remains in the index"
|
||||
)
|
||||
end)
|
||||
|
||||
t.test("toggle_stage stages a whole-file change with no context", function()
|
||||
local dir, buf = setup("a\nb\nc\n", "x\ny\nz\n")
|
||||
vim.api.nvim_win_set_cursor(0, { 2, 0 })
|
||||
|
||||
Reference in New Issue
Block a user