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-popup)` | Cursor-line blame popup |
|
||||||
| `<Plug>(git-blame-view)` | Toggle full-file blame overlay |
|
| `<Plug>(git-blame-view)` | Toggle full-file blame overlay |
|
||||||
| `<Plug>(git-hunk-select)` | Visually select the hunk under cursor |
|
| `<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-reset)` | Reset hunk |
|
||||||
| `<Plug>(git-hunk-overlay-toggle)` | Toggle the in-buffer diff overlay |
|
| `<Plug>(git-hunk-overlay-toggle)` | Toggle the in-buffer diff overlay |
|
||||||
| `<Plug>(git-hunk-next)` | Jump to next hunk |
|
| `<Plug>(git-hunk-next)` | Jump to next hunk |
|
||||||
|
|||||||
+112
-35
@@ -621,20 +621,6 @@ local function hunk_at(hunks, row)
|
|||||||
return nil
|
return nil
|
||||||
end
|
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?
|
---@param buf integer?
|
||||||
---@return integer buf
|
---@return integer buf
|
||||||
---@return ow.Git.Hunks.BufState? state
|
---@return ow.Git.Hunks.BufState? state
|
||||||
@@ -748,17 +734,20 @@ end
|
|||||||
---@param h ow.Git.Hunks.Hunk
|
---@param h ow.Git.Hunks.Hunk
|
||||||
---@param old_lines string[]
|
---@param old_lines string[]
|
||||||
---@param rel string
|
---@param rel string
|
||||||
---@return string patch
|
---@param include_header boolean?
|
||||||
|
---@param context integer?
|
||||||
|
---@return string[] patch lines
|
||||||
---@return boolean zero_context
|
---@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 old_before, new_before = hunk_offsets(h)
|
||||||
local pre = {}
|
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 ""
|
pre[#pre + 1] = old_lines[i] or ""
|
||||||
end
|
end
|
||||||
local post = {}
|
local post = {}
|
||||||
local after = old_before + h.old_count
|
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 ""
|
post[#post + 1] = old_lines[i] or ""
|
||||||
end
|
end
|
||||||
local old_n = #pre + h.old_count + #post
|
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
|
for _, l in ipairs(post) do
|
||||||
body[#body + 1] = " " .. l
|
body[#body + 1] = " " .. l
|
||||||
end
|
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)
|
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
|
end
|
||||||
|
|
||||||
---@param state ow.Git.Hunks.BufState
|
---@param state ow.Git.Hunks.BufState
|
||||||
@@ -818,27 +827,95 @@ local function apply_patch(state, buf, patch, zero_context)
|
|||||||
})
|
})
|
||||||
end
|
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
|
---@param buf? integer
|
||||||
function M.toggle_stage(buf)
|
function M.toggle_stage_range(first, last, buf)
|
||||||
buf = resolve_buf(buf)
|
buf = resolve_buf(buf)
|
||||||
local state = states[buf]
|
local state = states[buf]
|
||||||
if not state then
|
if not state or not state.index then
|
||||||
return
|
return
|
||||||
end
|
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 row = vim.api.nvim_win_get_cursor(0)[1]
|
||||||
local unstaged = hunk_at(state.hunks, row)
|
M.toggle_stage_range(row, row, buf)
|
||||||
if unstaged and state.index then
|
end
|
||||||
local patch, zero = build_patch(unstaged, state.index, state.rel)
|
|
||||||
apply_patch(state, buf, patch, zero)
|
---@param buf? integer
|
||||||
return
|
function M.toggle_stage_selection(buf)
|
||||||
end
|
local cursor_line = vim.api.nvim_win_get_cursor(0)[1]
|
||||||
local staged = staged_hunk_at(state, buf, row)
|
M.toggle_stage_range(vim.fn.line("v"), cursor_line, buf)
|
||||||
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")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param buf? integer
|
---@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()
|
vim.keymap.set("n", "<Plug>(git-hunk-stage-toggle)", function()
|
||||||
require("git.hunks").toggle_stage()
|
require("git.hunks").toggle_stage()
|
||||||
end, { silent = true, desc = "Stage or unstage the hunk under cursor" })
|
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()
|
vim.keymap.set("n", "<Plug>(git-hunk-reset)", function()
|
||||||
require("git.hunks").reset_hunk()
|
require("git.hunks").reset_hunk()
|
||||||
end, { silent = true, desc = "Reset hunk under cursor" })
|
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)
|
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()
|
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")
|
local dir, buf = setup("a\nb\nc\n", "x\ny\nz\n")
|
||||||
vim.api.nvim_win_set_cursor(0, { 2, 0 })
|
vim.api.nvim_win_set_cursor(0, { 2, 0 })
|
||||||
|
|||||||
Reference in New Issue
Block a user