From e1087c1226f0a98c83842613066ba9ff48710887 Mon Sep 17 00:00:00 2001 From: eileencodes Date: Fri, 19 Dec 2025 11:27:28 -0500 Subject: [PATCH 1/4] [ruby/rubygems] Fix dependency source bug in bundler I stumbled across a bundler bug that had me scratching my head for awhile, because I hadn't experienced it before. In some cases when changing the source in a gemfile from a `Source::Gemspec` to either a `Source::Path` or `Source::Git` only the parent gem will have it's gem replaced and updated and the child components will retain the original version. This only happens if the gem version of the `Source::Gemspec` and `Source::Git` are the same. It also requires another gem to share a dependency with the one being updated. For example if I have the following gemfile: ``` gem "rails", "~> 8.1.1" gem "propshaft" ``` Rails has a component called `actionpack` which `propshaft` depends on. If I change `rails` to point at a git source (or path source), only the path for `rails` gets updated: ``` gem "rails", github: "rails/rails", branch: "8-1-stable" gem "propshaft" ``` Because `actionpack` is a dependency of `propshaft`, it will remain in the rubygems source in the lock file WHILE the other gems are correctly pointing to the git source. Gemfile.lock: ``` GIT remote: https://github.com/rails/rails.git revision: https://github.com/ruby/rubygems/commit/9439f463e0ef branch: 8-1-stable specs: actioncable (8.1.1) ... actionmailbox (8.1.1) ... actionmailer (8.1.1) ... actiontext (8.1.1) ... activejob (8.1.1) ... activemodel (8.1.1) ... activerecord (8.1.1) ... activestorage (8.1.1) ... rails (8.1.1) ... railties (8.1.1) ... GEM remote: https://rubygems.org/ specs: action_text-trix (2.1.15) railties actionpack (8.1.1) <===== incorrectly left in Rubygems source ... ``` The gemfile will contain `actionpack` in the rubygems source, but will be missing in the git source so the path will be incorrect. A bundle show on Rails will point to the correct place: ``` $ bundle show rails /Users/eileencodes/.gem/ruby/3.4.4/bundler/gems/rails-9439f463e0ef ``` but a bundle show on actionpack will be incorrect: ``` $ bundle show actionpack /Users/eileencodes/.gem/ruby/3.4.4/gems/actionpack-8.1.1 ``` This bug requires the following to reproduce: 1) A gem like Rails that contains components that are released as their own standalone gem is added to the gemfile pointing to rubygems 2) A second gem is added that depends on one of the gems in the first gem (like propshaft does on actionpack) 3) The Rails gem is updated to use a git source, pointing to the same version that is being used by rubygems (ie 8.1.1) 4) `bundle` will only update the path for Rails component gems if no other gem depends on it. This incorrectly leaves Rails (or any gem like it) using two different codepaths / gem source code. https://github.com/ruby/rubygems/commit/dff76ba4f6 --- lib/bundler/definition.rb | 17 ++- spec/bundler/install/gemfile/sources_spec.rb | 116 +++++++++++++++++++ 2 files changed, 132 insertions(+), 1 deletion(-) diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb index 2fa7d0d277943b..5ab577f504c39d 100644 --- a/lib/bundler/definition.rb +++ b/lib/bundler/definition.rb @@ -1066,7 +1066,22 @@ def converge_specs(specs) deps << dep if !replacement_source || lockfile_source.include?(replacement_source) || new_deps.include?(dep) else - replacement_source = sources.get(lockfile_source) + parent_dep = @dependencies.find do |d| + next unless d.source && d.source != lockfile_source + next if d.source.is_a?(Source::Gemspec) + + parent_locked_specs = @originally_locked_specs[d.name] + + parent_locked_specs.any? do |parent_spec| + parent_spec.runtime_dependencies.any? {|rd| rd.name == s.name } + end + end + + if parent_dep + replacement_source = parent_dep.source + else + replacement_source = sources.get(lockfile_source) + end end # Replace the locked dependency's source with the equivalent source from the Gemfile diff --git a/spec/bundler/install/gemfile/sources_spec.rb b/spec/bundler/install/gemfile/sources_spec.rb index c0b4d98f1c5cc6..90f87ed0c5daea 100644 --- a/spec/bundler/install/gemfile/sources_spec.rb +++ b/spec/bundler/install/gemfile/sources_spec.rb @@ -1079,4 +1079,120 @@ expect(lockfile).to eq original_lockfile.gsub("bigdecimal (1.0.0)", "bigdecimal (3.3.1)") end end + + context "when switching a gem with components from rubygems to git source" do + before do + build_repo2 do + build_gem "rails", "7.0.0" do |s| + s.add_dependency "actionpack", "7.0.0" + s.add_dependency "activerecord", "7.0.0" + end + build_gem "actionpack", "7.0.0" + build_gem "activerecord", "7.0.0" + # propshaft also depends on actionpack, creating the conflict + build_gem "propshaft", "1.0.0" do |s| + s.add_dependency "actionpack", ">= 7.0.0" + end + end + + build_git "rails", "7.0.0", path: lib_path("rails") do |s| + s.add_dependency "actionpack", "7.0.0" + s.add_dependency "activerecord", "7.0.0" + end + + build_git "actionpack", "7.0.0", path: lib_path("rails") + build_git "activerecord", "7.0.0", path: lib_path("rails") + + install_gemfile <<-G + source "https://gem.repo2" + gem "rails", "7.0.0" + gem "propshaft" + G + end + + it "moves component gems to the git source in the lockfile" do + expect(lockfile).to include("remote: https://gem.repo2") + expect(lockfile).to include("rails (7.0.0)") + expect(lockfile).to include("actionpack (7.0.0)") + expect(lockfile).to include("activerecord (7.0.0)") + expect(lockfile).to include("propshaft (1.0.0)") + + gemfile <<-G + source "https://gem.repo2" + gem "rails", git: "#{lib_path("rails")}" + gem "propshaft" + G + + bundle "install" + + expect(lockfile).to include("remote: #{lib_path("rails")}") + expect(lockfile).to include("rails (7.0.0)") + expect(lockfile).to include("actionpack (7.0.0)") + expect(lockfile).to include("activerecord (7.0.0)") + + # Component gems should NOT remain in the GEM section + # Extract just the GEM section by splitting on GIT first, then GEM + gem_section = lockfile.split("GEM\n").last.split(/\n(PLATFORMS|DEPENDENCIES)/)[0] + expect(gem_section).not_to include("actionpack (7.0.0)") + expect(gem_section).not_to include("activerecord (7.0.0)") + end + end + + context "when switching a gem with components from rubygems to path source" do + before do + build_repo2 do + build_gem "rails", "7.0.0" do |s| + s.add_dependency "actionpack", "7.0.0" + s.add_dependency "activerecord", "7.0.0" + end + build_gem "actionpack", "7.0.0" + build_gem "activerecord", "7.0.0" + # propshaft also depends on actionpack, creating the conflict + build_gem "propshaft", "1.0.0" do |s| + s.add_dependency "actionpack", ">= 7.0.0" + end + end + + build_lib "rails", "7.0.0", path: lib_path("rails") do |s| + s.add_dependency "actionpack", "7.0.0" + s.add_dependency "activerecord", "7.0.0" + end + + build_lib "actionpack", "7.0.0", path: lib_path("rails") + build_lib "activerecord", "7.0.0", path: lib_path("rails") + + install_gemfile <<-G + source "https://gem.repo2" + gem "rails", "7.0.0" + gem "propshaft" + G + end + + it "moves component gems to the path source in the lockfile" do + expect(lockfile).to include("remote: https://gem.repo2") + expect(lockfile).to include("rails (7.0.0)") + expect(lockfile).to include("actionpack (7.0.0)") + expect(lockfile).to include("activerecord (7.0.0)") + expect(lockfile).to include("propshaft (1.0.0)") + + gemfile <<-G + source "https://gem.repo2" + gem "rails", path: "#{lib_path("rails")}" + gem "propshaft" + G + + bundle "install" + + expect(lockfile).to include("remote: #{lib_path("rails")}") + expect(lockfile).to include("rails (7.0.0)") + expect(lockfile).to include("actionpack (7.0.0)") + expect(lockfile).to include("activerecord (7.0.0)") + + # Component gems should NOT remain in the GEM section + # Extract just the GEM section by splitting appropriately + gem_section = lockfile.split("GEM\n").last.split(/\n(PLATFORMS|DEPENDENCIES)/)[0] + expect(gem_section).not_to include("actionpack (7.0.0)") + expect(gem_section).not_to include("activerecord (7.0.0)") + end + end end From 88467996862a15f64ee05e49c9df9b18296c66d6 Mon Sep 17 00:00:00 2001 From: neimadTL Date: Thu, 13 Feb 2025 09:32:33 -0400 Subject: [PATCH 2/4] [ruby/rubygems] Update custom errors with Exception#full_message The use of `Exception#full_message` makes more sense as it shows the cause and the backstrace. https://github.com/ruby/rubygems/commit/62a92c3f5e --- lib/bundler/errors.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/bundler/errors.rb b/lib/bundler/errors.rb index 8cd1336356079e..d8df4d6ec5c553 100644 --- a/lib/bundler/errors.rb +++ b/lib/bundler/errors.rb @@ -131,7 +131,8 @@ class GemRequireError < BundlerError attr_reader :orig_exception def initialize(orig_exception, msg) - full_message = msg + "\nGem Load Error is: #{orig_exception.message}\n"\ + full_message = msg + "\nGem Load Error is: + #{orig_exception.full_message(highlight: false)}\n"\ "Backtrace for gem load error is:\n"\ "#{orig_exception.backtrace.join("\n")}\n"\ "Bundler Error Backtrace:\n" @@ -221,7 +222,9 @@ def initialize(underlying_error, message) class DirectoryRemovalError < BundlerError def initialize(orig_exception, msg) full_message = "#{msg}.\n" \ - "The underlying error was #{orig_exception.class}: #{orig_exception.message}, with backtrace:\n" \ + "The underlying error was #{orig_exception.class}: + #{orig_exception.full_message(highlight: false)}, + with backtrace:\n" \ " #{orig_exception.backtrace.join("\n ")}\n\n" \ "Bundler Error Backtrace:" super(full_message) From 3657700c4069ffd5d9e9b900e75f484f80eb8e3f Mon Sep 17 00:00:00 2001 From: Takuya Noguchi Date: Tue, 6 Sep 2022 07:37:50 +0000 Subject: [PATCH 3/4] [ruby/rubygems] Bundler: validate more options for add sub-command Signed-off-by: Takuya Noguchi https://github.com/ruby/rubygems/commit/6ca2e28680 --- lib/bundler/cli/add.rb | 10 +++++++++ spec/bundler/commands/add_spec.rb | 34 +++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/lib/bundler/cli/add.rb b/lib/bundler/cli/add.rb index 12a681a816886c..9f1760409617d2 100644 --- a/lib/bundler/cli/add.rb +++ b/lib/bundler/cli/add.rb @@ -36,6 +36,16 @@ def inject_dependencies end def validate_options! + raise InvalidOption, "You cannot specify `--git` and `--github` at the same time." if options["git"] && options["github"] + + unless options["git"] || options["github"] + raise InvalidOption, "You cannot specify `--branch` unless `--git` or `--github` is specified." if options["branch"] + + raise InvalidOption, "You cannot specify `--ref` unless `--git` or `--github` is specified." if options["ref"] + end + + raise InvalidOption, "You cannot specify `--branch` and `--ref` at the same time." if options["branch"] && options["ref"] + raise InvalidOption, "You cannot specify `--strict` and `--optimistic` at the same time." if options[:strict] && options[:optimistic] # raise error when no gems are specified diff --git a/spec/bundler/commands/add_spec.rb b/spec/bundler/commands/add_spec.rb index 00aa6415e11c97..79f036d3cd3f72 100644 --- a/spec/bundler/commands/add_spec.rb +++ b/spec/bundler/commands/add_spec.rb @@ -236,6 +236,40 @@ end end + describe "with mismatched pair in --git/--github, --branch/--ref" do + describe "with --git and --github" do + it "throws error" do + bundle "add 'foo' --git x --github y", :raise_on_error => false + + expect(err).to include("You cannot specify `--git` and `--github` at the same time.") + end + end + + describe "with --branch and --ref with --git" do + it "throws error" do + bundle "add 'foo' --branch x --ref y --git file://git", :raise_on_error => false + + expect(err).to include("You cannot specify `--branch` and `--ref` at the same time.") + end + end + + describe "with --branch but without --git or --github" do + it "throws error" do + bundle "add 'foo' --branch x", :raise_on_error => false + + expect(err).to include("You cannot specify `--branch` unless `--git` or `--github` is specified.") + end + end + + describe "with --ref but without --git or --github" do + it "throws error" do + bundle "add 'foo' --ref y", :raise_on_error => false + + expect(err).to include("You cannot specify `--ref` unless `--git` or `--github` is specified.") + end + end + end + describe "with --skip-install" do it "adds gem to Gemfile but is not installed" do bundle "add foo --skip-install --version=2.0" From 7d4983803887a45c311ab954de4527333b976500 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Wed, 7 Jan 2026 17:00:14 +0900 Subject: [PATCH 4/4] [ruby/rubygems] bin/rubocop -A https://github.com/ruby/rubygems/commit/e3f418aa11 --- spec/bundler/commands/add_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/bundler/commands/add_spec.rb b/spec/bundler/commands/add_spec.rb index 79f036d3cd3f72..ed98a914f326c4 100644 --- a/spec/bundler/commands/add_spec.rb +++ b/spec/bundler/commands/add_spec.rb @@ -239,7 +239,7 @@ describe "with mismatched pair in --git/--github, --branch/--ref" do describe "with --git and --github" do it "throws error" do - bundle "add 'foo' --git x --github y", :raise_on_error => false + bundle "add 'foo' --git x --github y", raise_on_error: false expect(err).to include("You cannot specify `--git` and `--github` at the same time.") end @@ -247,7 +247,7 @@ describe "with --branch and --ref with --git" do it "throws error" do - bundle "add 'foo' --branch x --ref y --git file://git", :raise_on_error => false + bundle "add 'foo' --branch x --ref y --git file://git", raise_on_error: false expect(err).to include("You cannot specify `--branch` and `--ref` at the same time.") end @@ -255,7 +255,7 @@ describe "with --branch but without --git or --github" do it "throws error" do - bundle "add 'foo' --branch x", :raise_on_error => false + bundle "add 'foo' --branch x", raise_on_error: false expect(err).to include("You cannot specify `--branch` unless `--git` or `--github` is specified.") end @@ -263,7 +263,7 @@ describe "with --ref but without --git or --github" do it "throws error" do - bundle "add 'foo' --ref y", :raise_on_error => false + bundle "add 'foo' --ref y", raise_on_error: false expect(err).to include("You cannot specify `--ref` unless `--git` or `--github` is specified.") end