|
10 | 10 | require "dependabot/go_modules/go_work_parser" |
11 | 11 | require "dependabot/go_modules/replace_stubber" |
12 | 12 | require "dependabot/go_modules/resolvability_errors" |
| 13 | +require "dependabot/go_modules/version" |
13 | 14 |
|
14 | 15 | module Dependabot |
15 | 16 | module GoModules |
@@ -222,10 +223,12 @@ def update_files |
222 | 223 | substitute_all(substitutions.invert) |
223 | 224 | end |
224 | 225 |
|
225 | | - updated_go_sum = original_go_sum ? File.read("go.sum") : nil |
| 226 | + updated_go_sum = T.let(nil, T.nilable(String)) |
| 227 | + updated_go_sum = reconcile_go_sum(original_go_sum, File.read("go.sum")) if original_go_sum |
| 228 | + |
226 | 229 | updated_go_mod = File.read("go.mod") |
227 | 230 |
|
228 | | - { go_mod: updated_go_mod, go_sum: updated_go_sum } |
| 231 | + { go_mod: updated_go_mod, go_sum: updated_go_sum }.compact |
229 | 232 | end |
230 | 233 | end |
231 | 234 |
|
@@ -417,6 +420,115 @@ def build_module_stubs(stub_paths) |
417 | 420 | end |
418 | 421 | end |
419 | 422 |
|
| 423 | + sig { params(original_go_sum: String, updated_go_sum: String).returns(String) } |
| 424 | + def reconcile_go_sum(original_go_sum, updated_go_sum) |
| 425 | + original_lines = original_go_sum.lines(chomp: true).reject(&:empty?) |
| 426 | + updated_lines = updated_go_sum.lines(chomp: true).reject(&:empty?) |
| 427 | + updated_set = updated_lines.to_set |
| 428 | + updated_module_versions = extract_module_versions(updated_lines) |
| 429 | + |
| 430 | + restored_lines = find_restorable_go_mod_lines(original_lines, updated_set, updated_module_versions) |
| 431 | + return updated_go_sum if restored_lines.empty? |
| 432 | + |
| 433 | + (updated_lines + restored_lines).sort! { |a, b| go_sum_line_compare(a, b) }.join("\n") + "\n" |
| 434 | + end |
| 435 | + |
| 436 | + sig do |
| 437 | + params( |
| 438 | + original_lines: T::Array[String], |
| 439 | + updated_set: T::Set[String], |
| 440 | + updated_module_versions: T::Set[String] |
| 441 | + ).returns(T::Array[String]) |
| 442 | + end |
| 443 | + def find_restorable_go_mod_lines(original_lines, updated_set, updated_module_versions) |
| 444 | + original_lines.filter_map do |line| |
| 445 | + next unless go_mod_checksum_line?(line) |
| 446 | + next if updated_set.include?(line) |
| 447 | + |
| 448 | + module_path = go_sum_module_path(line) |
| 449 | + next unless module_path |
| 450 | + next if updated_dependency_names.include?(module_path) |
| 451 | + |
| 452 | + module_version = extract_module_version_from_go_mod_line(line) |
| 453 | + next unless module_version |
| 454 | + next unless updated_module_versions.include?(module_version) |
| 455 | + |
| 456 | + line |
| 457 | + end |
| 458 | + end |
| 459 | + |
| 460 | + sig { params(line: String).returns(T::Boolean) } |
| 461 | + def go_mod_checksum_line?(line) |
| 462 | + line.include?("/go.mod h1:") |
| 463 | + end |
| 464 | + |
| 465 | + sig { params(line: String).returns(T.nilable(String)) } |
| 466 | + def go_sum_module_path(line) |
| 467 | + line.split(/\s+/, 2).first |
| 468 | + end |
| 469 | + |
| 470 | + # Extracts "module/path vX.Y.Z" from a /go.mod checksum line |
| 471 | + sig { params(line: String).returns(T.nilable(String)) } |
| 472 | + def extract_module_version_from_go_mod_line(line) |
| 473 | + match = line.match(%r{^(\S+)\s+(\S+)/go\.mod\s}) |
| 474 | + return nil unless match |
| 475 | + |
| 476 | + "#{match[1]} #{match[2]}" |
| 477 | + end |
| 478 | + |
| 479 | + # Builds a set of "module/path vX.Y.Z" pairs from non-/go.mod lines in go.sum |
| 480 | + sig { params(lines: T::Array[String]).returns(T::Set[String]) } |
| 481 | + def extract_module_versions(lines) |
| 482 | + lines.each_with_object(Set.new) do |line, set| |
| 483 | + next if go_mod_checksum_line?(line) |
| 484 | + |
| 485 | + parts = line.split(/\s+/, 3) |
| 486 | + next unless parts.length >= 2 |
| 487 | + |
| 488 | + set.add("#{parts[0]} #{parts[1]}") |
| 489 | + end |
| 490 | + end |
| 491 | + |
| 492 | + sig { returns(T::Set[String]) } |
| 493 | + def updated_dependency_names |
| 494 | + @updated_dependency_names ||= T.let(dependencies.to_set(&:name), T.nilable(T::Set[String])) |
| 495 | + end |
| 496 | + |
| 497 | + # Compares two go.sum lines using Go's module-aware sort order: |
| 498 | + # sort by module path, then semver version, then /go.mod suffix last. |
| 499 | + sig { params(line_a: String, line_b: String).returns(Integer) } |
| 500 | + def go_sum_line_compare(line_a, line_b) |
| 501 | + path_a, version_rest_a = line_a.split(/\s+/, 2) |
| 502 | + path_b, version_rest_b = line_b.split(/\s+/, 2) |
| 503 | + |
| 504 | + path_cmp = T.must((path_a || "") <=> (path_b || "")) |
| 505 | + return path_cmp unless path_cmp.zero? |
| 506 | + |
| 507 | + compare_go_versions(version_rest_a || "", version_rest_b || "") |
| 508 | + end |
| 509 | + |
| 510 | + # Compares version+suffix portions of go.sum lines using GoModules::Version. |
| 511 | + sig { params(ver_a: String, ver_b: String).returns(Integer) } |
| 512 | + def compare_go_versions(ver_a, ver_b) |
| 513 | + a_is_gomod = ver_a.include?("/go.mod") |
| 514 | + b_is_gomod = ver_b.include?("/go.mod") |
| 515 | + |
| 516 | + # Extract raw version token (e.g., "v0.6.0" from "v0.6.0/go.mod h1:...") |
| 517 | + raw_a = ver_a.split(%r{(/go\.mod)?\s}, 2).first || "" |
| 518 | + raw_b = ver_b.split(%r{(/go\.mod)?\s}, 2).first || "" |
| 519 | + |
| 520 | + ver_cmp = go_version_compare(raw_a, raw_b) |
| 521 | + return ver_cmp unless ver_cmp.zero? |
| 522 | + |
| 523 | + # Same version: zip hash line sorts before /go.mod line |
| 524 | + (a_is_gomod ? 1 : 0) <=> (b_is_gomod ? 1 : 0) |
| 525 | + end |
| 526 | + |
| 527 | + sig { params(ver_a: String, ver_b: String).returns(Integer) } |
| 528 | + def go_version_compare(ver_a, ver_b) |
| 529 | + T.must(Dependabot::GoModules::Version.new(ver_a) <=> Dependabot::GoModules::Version.new(ver_b)) |
| 530 | + end |
| 531 | + |
420 | 532 | # Given a go.mod file, find all `replace` directives pointing to a path |
421 | 533 | # on the local filesystem, and return an array of pairs mapping the |
422 | 534 | # original path to a hash of the path. |
|
0 commit comments