Common library code for other vc*.nvim projects.
at main 189 lines 5.1 kB view raw
1--- Utilities for working with git-format patches. 2local M = {} 3 4---@class PatchLine 5---@field type "context"|"add"|"remove" The type of line in the patch. 6---@field content string The actual line content (without the leading +/- marker). 7 8---@class Hunk 9---@field old_start integer Starting line number in the old file. 10---@field old_count integer Number of lines in the old file. 11---@field new_start integer Starting line number in the new file. 12---@field new_count integer Number of lines in the new file. 13---@field lines PatchLine[] The lines in this hunk. 14 15---@class Patch 16---@field hunks Hunk[] The hunks in this patch. 17 18--- Parse a single file git-format diff into a structured patch. 19--- (single file to keep things simple, and was sufficient for our use cases) 20---@param patch_text string The git diff output. 21---@return Patch|nil The parsed patch, or nil if no hunks found. 22function M.parse_single_file_patch(patch_text) 23 local lines = vim.split(patch_text, "\n", { plain = true }) 24 25 -- Skip metadata until we hit the first @@. 26 local i = 1 27 while i <= #lines and not lines[i]:match "^@@" do 28 i = i + 1 29 end 30 31 if i > #lines then 32 -- No hunks found. 33 return nil 34 end 35 36 local hunks = {} 37 local current_hunk = nil 38 39 while i <= #lines do 40 local line = lines[i] 41 42 if line:match "^@@" then 43 -- Save previous hunk if any. 44 if current_hunk then 45 table.insert(hunks, current_hunk) 46 end 47 48 -- Parse hunk header: @@ -old_start,old_count +new_start,new_count @@. 49 local old_start, old_count, new_start, new_count = 50 line:match "^@@ %-(%d+),?(%d*) %+(%d+),?(%d*) @@" 51 52 if old_start and new_start then 53 current_hunk = { 54 old_start = tonumber(old_start), 55 old_count = tonumber(old_count) or 1, 56 new_start = tonumber(new_start), 57 new_count = tonumber(new_count) or 1, 58 lines = {}, 59 } 60 end 61 i = i + 1 62 else 63 local first_char = line:sub(1, 1) 64 if first_char == " " then 65 -- Context line. 66 if current_hunk then 67 table.insert(current_hunk.lines, { 68 type = "context", 69 content = line:sub(2), 70 }) 71 end 72 i = i + 1 73 elseif first_char == "-" then 74 -- Removed line. 75 if current_hunk then 76 table.insert(current_hunk.lines, { 77 type = "remove", 78 content = line:sub(2), 79 }) 80 end 81 i = i + 1 82 elseif first_char == "+" then 83 -- Added line. 84 if current_hunk then 85 table.insert(current_hunk.lines, { 86 type = "add", 87 content = line:sub(2), 88 }) 89 end 90 i = i + 1 91 elseif first_char == "\\" then 92 -- "\ No newline at end of file" marker - ignore for now. 93 i = i + 1 94 else 95 -- End of hunks or unknown line. 96 break 97 end 98 end 99 end 100 101 -- Save last hunk. 102 if current_hunk then 103 table.insert(hunks, current_hunk) 104 end 105 106 if #hunks == 0 then 107 return nil 108 end 109 110 return { hunks = hunks } 111end 112 113--- Invert a patch (swap add/remove operations and old/new positions). 114---@param patch Patch The patch to invert. 115---@return Patch The inverted patch. 116function M.invert_patch(patch) 117 local inverted_hunks = {} 118 119 for _, hunk in ipairs(patch.hunks) do 120 local inverted_lines = {} 121 122 for _, line in ipairs(hunk.lines) do 123 local inverted_type = line.type 124 if line.type == "add" then 125 inverted_type = "remove" 126 elseif line.type == "remove" then 127 inverted_type = "add" 128 end 129 130 table.insert(inverted_lines, { 131 type = inverted_type, 132 content = line.content, 133 }) 134 end 135 136 -- Swap old and new positions. 137 table.insert(inverted_hunks, { 138 old_start = hunk.new_start, 139 old_count = hunk.new_count, 140 new_start = hunk.old_start, 141 new_count = hunk.old_count, 142 lines = inverted_lines, 143 }) 144 end 145 146 return { hunks = inverted_hunks } 147end 148 149--- Apply a patch to file contents. 150---@param file_lines string[] The current file contents. 151---@param patch Patch The patch to apply. 152---@return string[] The file contents after applying the patch. 153function M.apply_patch(file_lines, patch) 154 local result = {} 155 local i = 1 156 157 for _, hunk in ipairs(patch.hunks) do 158 -- Copy unchanged lines before this hunk. 159 while i < hunk.old_start do 160 table.insert(result, file_lines[i]) 161 i = i + 1 162 end 163 164 -- Process hunk lines. 165 for _, line in ipairs(hunk.lines) do 166 if line.type == "context" then 167 -- Context line - should match current file. 168 table.insert(result, line.content) 169 i = i + 1 170 elseif line.type == "remove" then 171 -- Line removed - skip it in the result, advance in current file. 172 i = i + 1 173 elseif line.type == "add" then 174 -- Line added - add to result, don't advance in current file. 175 table.insert(result, line.content) 176 end 177 end 178 end 179 180 -- Copy any remaining unchanged lines. 181 while i <= #file_lines do 182 table.insert(result, file_lines[i]) 183 i = i + 1 184 end 185 186 return result 187end 188 189return M