diff --git a/gems/kettle-jem/CHANGELOG.md b/gems/kettle-jem/CHANGELOG.md index df94b0716..3b0412fd0 100644 --- a/gems/kettle-jem/CHANGELOG.md +++ b/gems/kettle-jem/CHANGELOG.md @@ -42,6 +42,9 @@ Please file a bug if you notice a violation of semantic versioning. - The packaged `Gemfile` no longer overrides `git_source(:github)` with an SSH URL. The override was recorded in `Gemfile.lock` as `git@github.com` remotes, which CI runners cannot clone; Bundler's built-in HTTPS source is used again. + Templating now also removes existing overrides of Bundler's built-in git + sources (`git_source(:github)`, `:gist`, `:bitbucket`) from destination + Gemfiles, including the managed monorepo root Gemfile. - Generated Ruby 4 extracted-stdlib Gemfiles now include `ostruct` so suites that require it can run on Rubies where it is no longer bundled by default. diff --git a/gems/kettle-jem/lib/kettle/jem.rb b/gems/kettle-jem/lib/kettle/jem.rb index 212c54dc5..e64160e73 100644 --- a/gems/kettle-jem/lib/kettle/jem.rb +++ b/gems/kettle-jem/lib/kettle/jem.rb @@ -101,6 +101,10 @@ class Error < StandardError; end # turbo_tests were replaced by appraisal2 and turbo_tests2. PROHIBITED_GEMSPEC_DEPENDENCIES = %w[gem-release appraisal turbo_tests].freeze PROHIBITED_GEMFILE_DEPENDENCIES = PROHIBITED_GEMSPEC_DEPENDENCIES.freeze + # Git sources Bundler already defines. A Gemfile override (for example an + # SSH `git_source(:github)`) replaces Bundler's own HTTPS source and is + # recorded in Gemfile.lock, where CI cannot clone it: unconditional removal. + BUNDLER_BUILTIN_GIT_SOURCES = %w[github gist bitbucket].freeze # Canonical resolutions for development-dependency gems whose handling is # already known ahead of any specific destination project. Two # categories today: @@ -5530,6 +5534,7 @@ def ensure_monorepo_root_gemfile_dependencies(content) ensure_trailing_newline(content.to_s.empty? ? %(source "https://gem.coop"\n) : content.to_s).dup ) updated = remove_gemfile_dependency_blocks(updated, PROHIBITED_GEMFILE_DEPENDENCIES) + updated = remove_gemfile_builtin_git_source_overrides(updated).dup monorepo_root_gemfile_dependency_lines.each do |line| next if gemfile_declares_gem?(updated, line.fetch(:name)) @@ -8123,6 +8128,7 @@ def finalize_gemfile_template_source(recipe, content, destination_content, facts preserve_self_word_entries: local_gemfile_template_recipe?(recipe) ) end + output = remove_gemfile_builtin_git_source_overrides(output) if recipe.fetch(:target_path).to_s == "Gemfile" # A merged Gemfile retains its project-specific source; an accepted # template owns the complete source declaration. @@ -9700,6 +9706,22 @@ def remove_gemfile_dependency_blocks(content, gem_names) ensure_trailing_newline(lines.each_with_index.reject { |_line, index| remove_indexes.include?(index) }.map(&:first).join.gsub(/\n{3,}/, "\n\n")) end + # Bundler already provides these git sources; see BUNDLER_BUILTIN_GIT_SOURCES. + def remove_gemfile_builtin_git_source_overrides(content) + remove_indexes = Set.new + ruby_call_records(content, :git_source).each do |call| + argument = call.arguments&.arguments&.first + next unless argument.is_a?(::Prism::SymbolNode) + next unless BUNDLER_BUILTIN_GIT_SOURCES.include?(argument.unescaped.to_s) + + (call.location.start_line..ruby_node_source_end_line(call)).each { |line_number| remove_indexes << (line_number - 1) } + end + return content if remove_indexes.empty? + + lines = content.to_s.lines.each_with_index.reject { |_line, index| remove_indexes.include?(index) }.map(&:first) + ensure_trailing_newline(lines.join.sub(/\A\n+/, "").gsub(/\n{3,}/, "\n\n")) + end + def gemfile_gem_call_records(content) ruby_call_records(content, :gem).filter_map do |call| name = ruby_string_argument(call) diff --git a/gems/kettle-jem/spec/integration/appraisals_and_gemfile_template_spec.rb b/gems/kettle-jem/spec/integration/appraisals_and_gemfile_template_spec.rb index 2ed1230fa..d86e3d5a6 100644 --- a/gems/kettle-jem/spec/integration/appraisals_and_gemfile_template_spec.rb +++ b/gems/kettle-jem/spec/integration/appraisals_and_gemfile_template_spec.rb @@ -1450,6 +1450,86 @@ expect(updated).not_to include('gem "turbo_tests"') end + it "removes overrides of Bundler's built-in git sources and keeps other git sources" do + updated = described_class.remove_gemfile_builtin_git_source_overrides(<<~RUBY) + source "https://gem.coop" + + git_source(:github) { |repo_name| "git@github.com:\#{repo_name}.git" } + git_source(:gist) { |repo_name| "https://gist.github.com/\#{repo_name}.git" } + git_source(:bitbucket) do |repo_name| + "https://bitbucket.org/\#{repo_name}.git" + end + git_source(:codeberg) { |repo_name| "https://codeberg.org/\#{repo_name}" } + + gem "rubocop-packaging", github: "pboling/rubocop-packaging" + RUBY + + expect(updated).not_to include("git_source(:github)") + expect(updated).not_to include("git_source(:gist)") + expect(updated).not_to include("git_source(:bitbucket)") + expect(updated).not_to include("bitbucket.org") + expect(updated).to include("git_source(:codeberg)") + expect(updated).to include('gem "rubocop-packaging", github: "pboling/rubocop-packaging"') + end + + it "removes a git_source(:github) override from the managed root Gemfile" do + updated = described_class.ensure_monorepo_root_gemfile_dependencies( + "source \"https://gem.coop\"\ngit_source(:github) { |repo_name| \"https://github.com/\#{repo_name}\" }\n" + ) + + expect(updated).not_to include("git_source(:github)") + end + + it "removes a destination git_source(:github) override when templating the main Gemfile" do + tmp_root = File.expand_path("../tmp", __dir__) + FileUtils.mkdir_p(tmp_root) + Dir.mktmpdir("kettle-jem-main-gemfile-builtin-git-source", tmp_root) do |root| + write_tree(root, { + "example-gem.gemspec" => <<~RUBY, + Gem::Specification.new do |spec| + spec.name = "example-gem" + spec.summary = "Example Gem" + end + RUBY + ".kettle-jem.yml" => <<~YAML, + templates: + root: template + apply: true + entries: + - Gemfile + YAML + "Gemfile" => <<~RUBY, + # frozen_string_literal: true + + source "https://gem.coop" + + git_source(:github) { |repo_name| "git@github.com:\#{repo_name}.git" } + git_source(:codeberg) { |repo_name| "https://codeberg.org/\#{repo_name}" } + + gem "destination-only" + RUBY + "template/Gemfile.example" => <<~RUBY + # frozen_string_literal: true + + source "https://gem.coop" + + git_source(:codeberg) { |repo_name| "https://codeberg.org/\#{repo_name}" } + + gem "shared-tool" + RUBY + }) + + apply = described_class.apply_project(root, env: {}) + report = apply.fetch(:recipe_reports).find { |candidate| candidate.fetch(:relative_path) == "Gemfile" } + content = report.fetch(:final_content) + + expect(content).not_to include("git_source(:github)") + expect(content).to include("git_source(:codeberg)") + expect(content).to include('gem "shared-tool"') + expect(File.read(File.join(root, "Gemfile"))).to eq(content) + end + end + it "replaces declared kettle-dev dependencies with local paths without duplicate declarations" do template = File.read(File.expand_path("../../lib/kettle/jem/templates/gemfiles/modular/templating_local.gemfile.example", __dir__)) coverage_template = File.read(File.expand_path("../../lib/kettle/jem/templates/gemfiles/modular/coverage_local.gemfile.example", __dir__))