Common library code for other vc*.nvim projects.
at main 356 lines 8.6 kB view raw
1local M = {} 2 3local patch = require "vclib.patch" 4local testing = require "vclib.testing" 5 6-- Helper function to create a git diff output. 7local function make_git_diff_from_string(hunks) 8 local lines = { 9 "diff --git a/test.txt b/test.txt", 10 "index abc123..def456 100644", 11 "--- a/test.txt", 12 "+++ b/test.txt", 13 } 14 for _, line in ipairs(testing.dedent_into_lines(hunks)) do 15 lines[#lines + 1] = line 16 end 17 return table.concat(lines, "\n") 18end 19 20-- Helper to compare patch structures. 21local function assert_patch_eq(actual, expected) 22 assert(actual ~= nil, "actual patch is nil") 23 assert(expected ~= nil, "expected patch is nil") 24 assert( 25 #actual.hunks == #expected.hunks, 26 string.format("Expected %d hunks, got %d", #expected.hunks, #actual.hunks) 27 ) 28 29 for h = 1, #expected.hunks do 30 local ah = actual.hunks[h] 31 local eh = expected.hunks[h] 32 33 assert( 34 ah.old_start == eh.old_start, 35 string.format( 36 "Hunk %d: old_start mismatch: %d vs %d", 37 h, 38 ah.old_start, 39 eh.old_start 40 ) 41 ) 42 assert( 43 ah.old_count == eh.old_count, 44 string.format( 45 "Hunk %d: old_count mismatch: %d vs %d", 46 h, 47 ah.old_count, 48 eh.old_count 49 ) 50 ) 51 assert( 52 ah.new_start == eh.new_start, 53 string.format( 54 "Hunk %d: new_start mismatch: %d vs %d", 55 h, 56 ah.new_start, 57 eh.new_start 58 ) 59 ) 60 assert( 61 ah.new_count == eh.new_count, 62 string.format( 63 "Hunk %d: new_count mismatch: %d vs %d", 64 h, 65 ah.new_count, 66 eh.new_count 67 ) 68 ) 69 assert( 70 #ah.lines == #eh.lines, 71 string.format( 72 "Hunk %d: Expected %d lines, got %d", 73 h, 74 #eh.lines, 75 #ah.lines 76 ) 77 ) 78 79 for l = 1, #eh.lines do 80 local al = ah.lines[l] 81 local el = eh.lines[l] 82 assert( 83 al.type == el.type, 84 string.format( 85 "Hunk %d, line %d: type mismatch: %s vs %s", 86 h, 87 l, 88 al.type, 89 el.type 90 ) 91 ) 92 assert( 93 al.content == el.content, 94 string.format( 95 "Hunk %d, line %d: content mismatch: '%s' vs '%s'", 96 h, 97 l, 98 al.content, 99 el.content 100 ) 101 ) 102 end 103 end 104end 105 106M.parse_single_file_patch = { 107 test_cases = { 108 single_hunk = { 109 patch_text = make_git_diff_from_string [[ 110 @@ -1,3 +1,4 @@ 111 line1 112 line2 113 +NEW LINE 114 line3 115 ]], 116 expected = { 117 hunks = { 118 { 119 old_start = 1, 120 old_count = 3, 121 new_start = 1, 122 new_count = 4, 123 lines = { 124 { type = "context", content = "line1" }, 125 { type = "context", content = "line2" }, 126 { type = "add", content = "NEW LINE" }, 127 { type = "context", content = "line3" }, 128 }, 129 }, 130 }, 131 }, 132 }, 133 multiple_hunks = { 134 patch_text = make_git_diff_from_string [[ 135 @@ -1,2 +1,3 @@ 136 line1 137 +NEW1 138 line2 139 @@ -5,2 +6,3 @@ 140 line5 141 +NEW2 142 line6 143 ]], 144 expected = { 145 hunks = { 146 { 147 old_start = 1, 148 old_count = 2, 149 new_start = 1, 150 new_count = 3, 151 lines = { 152 { type = "context", content = "line1" }, 153 { type = "add", content = "NEW1" }, 154 { type = "context", content = "line2" }, 155 }, 156 }, 157 { 158 old_start = 5, 159 old_count = 2, 160 new_start = 6, 161 new_count = 3, 162 lines = { 163 { type = "context", content = "line5" }, 164 { type = "add", content = "NEW2" }, 165 { type = "context", content = "line6" }, 166 }, 167 }, 168 }, 169 }, 170 }, 171 no_hunks = { 172 patch_text = "diff --git a/test.txt b/test.txt\nindex abc123..abc123 100644\n", 173 expected = nil, 174 }, 175 file_header_no_hunks = { 176 expected = nil, 177 patch_text = "diff --git a/test.txt b/test.txt\nindex abc123..abc123 100644\n--- a/test.txt\n+++ b/test.txt", 178 }, 179 mixed_operations = { 180 patch_text = make_git_diff_from_string [[ 181 @@ -1,4 +1,4 @@ 182 context1 183 -removed 184 +added 185 context2 186 ]], 187 expected = { 188 hunks = { 189 { 190 old_start = 1, 191 old_count = 4, 192 new_start = 1, 193 new_count = 4, 194 lines = { 195 { type = "context", content = "context1" }, 196 { type = "remove", content = "removed" }, 197 { type = "add", content = "added" }, 198 { type = "context", content = "context2" }, 199 }, 200 }, 201 }, 202 }, 203 }, 204 }, 205 test = function(case) 206 local result = patch.parse_single_file_patch(case.patch_text) 207 if case.expected == nil then 208 assert(result == nil, "Expected nil for patch with no hunks") 209 else 210 assert_patch_eq(result, case.expected) 211 end 212 end, 213} 214 215M.roundtrip = { 216 test_cases = { 217 simple_addition = { 218 old_file = { "line1", "line2", "line3" }, 219 new_file = { "line1", "line2", "NEW LINE", "line3" }, 220 patch_text = make_git_diff_from_string [[ 221 @@ -1,3 +1,4 @@ 222 line1 223 line2 224 +NEW LINE 225 line3 226 ]], 227 }, 228 simple_deletion = { 229 old_file = { "line1", "line2", "line3", "line4" }, 230 new_file = { "line1", "line2", "line4" }, 231 patch_text = make_git_diff_from_string [[ 232 @@ -1,4 +1,3 @@ 233 line1 234 line2 235 -line3 236 line4 237 ]], 238 }, 239 simple_modification = { 240 old_file = { "line1", "original line", "line3" }, 241 new_file = { "line1", "modified line", "line3" }, 242 patch_text = make_git_diff_from_string [[ 243 @@ -1,3 +1,3 @@ 244 line1 245 -original line 246 +modified line 247 line3 248 ]], 249 }, 250 multiple_hunks = { 251 old_file = { "line1", "line2", "unchanged", "line3", "line4" }, 252 new_file = { 253 "line1", 254 "NEW1", 255 "line2", 256 "unchanged", 257 "line3", 258 "NEW2", 259 "line4", 260 }, 261 patch_text = make_git_diff_from_string [[ 262 @@ -1,2 +1,3 @@ 263 line1 264 +NEW1 265 line2 266 @@ -4,2 +5,3 @@ 267 line3 268 +NEW2 269 line4 270 ]], 271 }, 272 complex_changes = { 273 old_file = { 274 "header", 275 "old line 1", 276 "old line 2", 277 "more content", 278 "old footer", 279 }, 280 new_file = { "header", "new line 1", "content", "more content", "footer" }, 281 patch_text = make_git_diff_from_string [[ 282 @@ -1,5 +1,5 @@ 283 header 284 -old line 1 285 -old line 2 286 +new line 1 287 +content 288 more content 289 -old footer 290 +footer 291 ]], 292 }, 293 add_to_empty_file = { 294 old_file = {}, 295 new_file = { "line1", "line2" }, 296 patch_text = make_git_diff_from_string [[ 297 @@ -0,0 +1,2 @@ 298 +line1 299 +line2 300 ]], 301 }, 302 delete_entire_file = { 303 old_file = { "line1", "line2" }, 304 new_file = {}, 305 patch_text = make_git_diff_from_string [[ 306 @@ -1,2 +0,0 @@ 307 -line1 308 -line2 309 ]], 310 }, 311 addition_at_beginning = { 312 old_file = { "line1", "line2" }, 313 new_file = { "NEW", "line1", "line2" }, 314 patch_text = make_git_diff_from_string [[ 315 @@ -1,2 +1,3 @@ 316 +NEW 317 line1 318 line2 319 ]], 320 }, 321 addition_at_end = { 322 old_file = { "line1", "line2" }, 323 new_file = { "line1", "line2", "NEW" }, 324 patch_text = make_git_diff_from_string [[ 325 @@ -1,2 +1,3 @@ 326 line1 327 line2 328 +NEW 329 ]], 330 }, 331 }, 332 test = function(case) 333 -- Validate parsing. 334 local parsed = patch.parse_single_file_patch(case.patch_text) 335 assert(parsed ~= nil, "Failed to parse patch") 336 337 -- Validate patch application. 338 local forward_result = patch.apply_patch(case.old_file, parsed) 339 testing.assert_list_eq( 340 forward_result, 341 case.new_file, 342 "Forward application failed: " 343 ) 344 345 -- Validate patch inversion and reverse application. 346 local inverted = patch.invert_patch(parsed) 347 local reverse_result = patch.apply_patch(case.new_file, inverted) 348 testing.assert_list_eq( 349 reverse_result, 350 case.old_file, 351 "Reverse application failed: " 352 ) 353 end, 354} 355 356return M