diff options
author | Hiroshi SHIBATA <hsbt@ruby-lang.org> | 2022-12-09 14:45:51 +0900 |
---|---|---|
committer | Hiroshi SHIBATA <hsbt@ruby-lang.org> | 2022-12-09 16:36:22 +0900 |
commit | a4e14b9d9d58391fb7d7a10be8d883690860373b (patch) | |
tree | 08f9c871583bd0a0d98b9cac3389ad52631400be | |
parent | d928ebacb23639cbf3f28201304f0451e5bd45a7 (diff) | |
download | ruby-a4e14b9d9d58391fb7d7a10be8d883690860373b.tar.gz |
Merge RubyGems/Bundler master
Pick from https://github.com/rubygems/rubygems/commit/823c776d951f3c35094611473ec77f94e8bf6610
22 files changed, 420 insertions, 158 deletions
diff --git a/lib/bundler/definition.rb b/lib/bundler/definition.rb index 9043efe408..fe807605e0 100644 --- a/lib/bundler/definition.rb +++ b/lib/bundler/definition.rb @@ -524,13 +524,22 @@ module Bundler raise GemNotFound, "Could not find #{missing_specs_list.join(" nor ")}" end + incomplete_specs = specs.incomplete_specs loop do - incomplete_specs = specs.incomplete_specs break if incomplete_specs.empty? Bundler.ui.debug("The lockfile does not have all gems needed for the current platform though, Bundler will still re-resolve dependencies") @resolve = start_resolution(:exclude_specs => incomplete_specs) specs = resolve.materialize(dependencies) + + still_incomplete_specs = specs.incomplete_specs + + if still_incomplete_specs == incomplete_specs + package = resolution_packages[incomplete_specs.first.name] + resolver.raise_not_found! package + end + + incomplete_specs = still_incomplete_specs end bundler = sources.metadata_source.specs.search(["bundler", Bundler.gem_version]).last diff --git a/lib/bundler/env.rb b/lib/bundler/env.rb index 1763035a8a..8b2e292fd6 100644 --- a/lib/bundler/env.rb +++ b/lib/bundler/env.rb @@ -75,7 +75,7 @@ module Bundler end def self.git_version - Bundler::Source::Git::GitProxy.new(nil, nil, nil).full_version + Bundler::Source::Git::GitProxy.new(nil, nil).full_version rescue Bundler::Source::Git::GitNotInstalledError "not installed" end diff --git a/lib/bundler/fetcher.rb b/lib/bundler/fetcher.rb index 61241b6523..a073bae278 100644 --- a/lib/bundler/fetcher.rb +++ b/lib/bundler/fetcher.rb @@ -29,9 +29,7 @@ module Bundler " is a chance you are experiencing a man-in-the-middle attack, but" \ " most likely your system doesn't have the CA certificates needed" \ " for verification. For information about OpenSSL certificates, see" \ - " https://railsapps.github.io/openssl-certificate-verify-failed.html." \ - " To connect without using SSL, edit your Gemfile" \ - " sources and change 'https' to 'http'." + " https://railsapps.github.io/openssl-certificate-verify-failed.html." end end @@ -39,8 +37,7 @@ module Bundler class SSLError < HTTPError def initialize(msg = nil) super msg || "Could not load OpenSSL.\n" \ - "You must recompile Ruby with OpenSSL support or change the sources in your " \ - "Gemfile from 'https' to 'http'." + "You must recompile Ruby with OpenSSL support." end end diff --git a/lib/bundler/man/bundle-platform.1 b/lib/bundler/man/bundle-platform.1 index 2d2450780a..41db8f72f0 100644 --- a/lib/bundler/man/bundle-platform.1 +++ b/lib/bundler/man/bundle-platform.1 @@ -65,7 +65,7 @@ It will display the ruby directive information, so you don\'t have to parse it f .SH "SEE ALSO" . .IP "\(bu" 4 -bundle\-lock(1) \fIbundle\-lock\.1\.ronn\fR +bundle\-lock(1) \fIbundle\-lock\.1\.html\fR . .IP "" 0 diff --git a/lib/bundler/man/bundle-platform.1.ronn b/lib/bundler/man/bundle-platform.1.ronn index eb9baa1c62..744acd1b43 100644 --- a/lib/bundler/man/bundle-platform.1.ronn +++ b/lib/bundler/man/bundle-platform.1.ronn @@ -46,4 +46,4 @@ match the running Ruby VM, it tells you what part does not. ## SEE ALSO -* [bundle-lock(1)](bundle-lock.1.ronn) +* [bundle-lock(1)](bundle-lock.1.html) diff --git a/lib/bundler/resolver.rb b/lib/bundler/resolver.rb index 07607813ec..c175ea4354 100644 --- a/lib/bundler/resolver.rb +++ b/lib/bundler/resolver.rb @@ -11,6 +11,7 @@ module Bundler require_relative "resolver/base" require_relative "resolver/package" require_relative "resolver/candidate" + require_relative "resolver/incompatibility" require_relative "resolver/root" include GemHelpers @@ -29,6 +30,10 @@ module Bundler root = Resolver::Root.new(name_for_explicit_dependency_source) root_version = Resolver::Candidate.new(0) + @all_specs = Hash.new do |specs, name| + specs[name] = source_for(name).specs.search(name).sort_by {|s| [s.version, s.platform.to_s] } + end + @sorted_versions = Hash.new do |candidates, package| candidates[package] = if package.root? [root_version] @@ -60,7 +65,7 @@ module Bundler incompatibility = e.incompatibility names_to_unlock = [] - conflict_on_bundler = nil + extended_explanation = nil while incompatibility.conflict? cause = incompatibility.cause @@ -69,12 +74,11 @@ module Bundler incompatibility.terms.each do |term| name = term.package.name names_to_unlock << name if base_requirements[name] - next unless name == "bundler" no_versions_incompat = [cause.incompatibility, cause.satisfier].find {|incompat| incompat.cause.is_a?(PubGrub::Incompatibility::NoVersions) } next unless no_versions_incompat - conflict_on_bundler ||= Gem::Requirement.new(no_versions_incompat.cause.constraint.constraint.constraint_string.split(",")) + extended_explanation = no_versions_incompat.extended_explanation end end @@ -85,9 +89,9 @@ module Bundler explanation = e.message - if conflict_on_bundler + if extended_explanation explanation << "\n\n" - explanation << bundler_not_found_message(conflict_on_bundler) + explanation << extended_explanation end raise SolveFailure.new(explanation) @@ -111,14 +115,25 @@ module Bundler def no_versions_incompatibility_for(package, unsatisfied_term) cause = PubGrub::Incompatibility::NoVersions.new(unsatisfied_term) + name = package.name + constraint = unsatisfied_term.constraint + requirement = Gem::Requirement.new(constraint.constraint_string.split(",")) - custom_explanation = if package.name == "bundler" - "the current Bundler version (#{Bundler::VERSION}) does not satisfy #{cause.constraint}" + if name == "bundler" + custom_explanation = "the current Bundler version (#{Bundler::VERSION}) does not satisfy #{constraint}" + extended_explanation = bundler_not_found_message(requirement) else - "#{cause.constraint} could not be found in #{repository_for(package)}" + specs_matching_other_platforms = filter_matching_specs(@all_specs[name], requirement) + + platforms_explanation = specs_matching_other_platforms.any? ? " for any resolution platforms (#{package.platforms.join(", ")})" : "" + custom_explanation = "#{constraint} could not be found in #{repository_for(package)}#{platforms_explanation}" + + dependency = Dependency.new(name, requirement) + label = SharedHelpers.pretty_dependency(dependency) + extended_explanation = other_specs_matching_message(specs_matching_other_platforms, label) if specs_matching_other_platforms.any? end - PubGrub::Incompatibility.new([unsatisfied_term], :cause => cause, :custom_explanation => custom_explanation) + Incompatibility.new([unsatisfied_term], :cause => cause, :custom_explanation => custom_explanation, :extended_explanation => extended_explanation) end def debug? @@ -187,9 +202,9 @@ module Bundler def all_versions_for(package) name = package.name - results = @base[name] + results_for(name) + results = @base[name] + @all_specs[name] locked_requirement = base_requirements[name] - results = results.select {|spec| requirement_satisfied_by?(locked_requirement, spec) } if locked_requirement + results = filter_matching_specs(results, locked_requirement) if locked_requirement versions = results.group_by(&:version).reduce([]) do |groups, (version, specs)| platform_specs = package.platforms.flat_map {|platform| select_best_platform_match(specs, platform) } @@ -208,30 +223,56 @@ module Bundler sort_versions(package, versions) end - def index_for(name) - source_for(name).specs - end - def source_for(name) @source_requirements[name] || @source_requirements[:default] end - def results_for(name) - index_for(name).search(name) - end - def name_for_explicit_dependency_source Bundler.default_gemfile.basename.to_s rescue StandardError "Gemfile" end - def requirement_satisfied_by?(requirement, spec) - requirement.satisfied_by?(spec.version) || spec.source.is_a?(Source::Gemspec) + def raise_not_found!(package) + name = package.name + source = source_for(name) + specs = @all_specs[name] + matching_part = name + requirement_label = SharedHelpers.pretty_dependency(package.dependency) + cache_message = begin + " or in gems cached in #{Bundler.settings.app_cache_path}" if Bundler.app_cache.exist? + rescue GemfileNotFound + nil + end + specs_matching_requirement = filter_matching_specs(specs, package.dependency.requirement) + + if specs_matching_requirement.any? + specs = specs_matching_requirement + matching_part = requirement_label + platforms = package.platforms + platform_label = platforms.size == 1 ? "platform '#{platforms.first}" : "platforms '#{platforms.join("', '")}" + requirement_label = "#{requirement_label}' with #{platform_label}" + end + + message = String.new("Could not find gem '#{requirement_label}' in #{source}#{cache_message}.\n") + + if specs.any? + message << "\n#{other_specs_matching_message(specs, matching_part)}" + end + + raise GemNotFound, message end private + def filter_matching_specs(specs, requirement) + specs.select {| spec| requirement_satisfied_by?(requirement, spec) } + end + + def requirement_satisfied_by?(requirement, spec) + requirement.satisfied_by?(spec.version) || spec.source.is_a?(Source::Gemspec) + end + def sort_versions(package, versions) if versions.size > 1 @gem_version_promoter.sort_versions(package, versions).reverse @@ -260,38 +301,13 @@ module Bundler next [dep_package, dep_constraint] unless versions_for(dep_package, dep_constraint.range).empty? next unless dep_package.current_platform? - raise GemNotFound, gem_not_found_message(dep_package, dep_constraint) + raise_not_found!(dep_package) end.compact.to_h end - def gem_not_found_message(package, requirement) - name = package.name - source = source_for(name) - specs = source.specs.search(name).sort_by {|s| [s.version, s.platform.to_s] } - matching_part = name - requirement_label = SharedHelpers.pretty_dependency(package.dependency) - cache_message = begin - " or in gems cached in #{Bundler.settings.app_cache_path}" if Bundler.app_cache.exist? - rescue GemfileNotFound - nil - end - specs_matching_requirement = specs.select {| spec| requirement_satisfied_by?(package.dependency.requirement, spec) } - - if specs_matching_requirement.any? - specs = specs_matching_requirement - matching_part = requirement_label - platforms = package.platforms - platform_label = platforms.size == 1 ? "platform '#{platforms.first}" : "platforms '#{platforms.join("', '")}" - requirement_label = "#{requirement_label}' with #{platform_label}" - end - - message = String.new("Could not find gem '#{requirement_label}' in #{source}#{cache_message}.\n") - - if specs.any? - message << "\nThe source contains the following gems matching '#{matching_part}':\n" - message << specs.map {|s| " * #{s.full_name}" }.join("\n") - end - + def other_specs_matching_message(specs, requirement) + message = String.new("The source contains the following gems matching '#{requirement}':\n") + message << specs.map {|s| " * #{s.full_name}" }.join("\n") message end @@ -342,7 +358,7 @@ module Bundler end def bundler_not_found_message(conflict_dependency) - candidate_specs = source_for(:default_bundler).specs.search("bundler").select {|spec| requirement_satisfied_by?(conflict_dependency, spec) } + candidate_specs = filter_matching_specs(source_for(:default_bundler).specs.search("bundler"), conflict_dependency) if candidate_specs.any? target_version = candidate_specs.last.version new_command = [File.basename($PROGRAM_NAME), "_#{target_version}_", *ARGV].join(" ") diff --git a/lib/bundler/resolver/incompatibility.rb b/lib/bundler/resolver/incompatibility.rb new file mode 100644 index 0000000000..c61151fbeb --- /dev/null +++ b/lib/bundler/resolver/incompatibility.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Bundler + class Resolver + class Incompatibility < PubGrub::Incompatibility + attr_reader :extended_explanation + + def initialize(terms, cause:, custom_explanation: nil, extended_explanation: nil) + @extended_explanation = extended_explanation + + super(terms, :cause => cause, :custom_explanation => custom_explanation) + end + end + end +end diff --git a/lib/bundler/resolver/package.rb b/lib/bundler/resolver/package.rb index fa283baca8..7d64632860 100644 --- a/lib/bundler/resolver/package.rb +++ b/lib/bundler/resolver/package.rb @@ -13,14 +13,14 @@ module Bundler # * The dependency explicit set in the Gemfile for this gem (if any). # class Package - attr_reader :name, :platforms, :dependency + attr_reader :name, :platforms, :dependency, :locked_version def initialize(name, platforms, locked_specs, unlock, dependency: nil) @name = name @platforms = platforms - @locked_specs = locked_specs + @locked_version = locked_specs[name].first&.version @unlock = unlock - @dependency = dependency + @dependency = dependency || Dependency.new(name, @locked_version) end def to_s @@ -43,24 +43,20 @@ module Bundler @name.hash end - def locked_version - @locked_specs[name].first&.version - end - def unlock? @unlock.empty? || @unlock.include?(name) end def prerelease_specified? - @dependency&.prerelease? + @dependency.prerelease? end def force_ruby_platform? - @dependency&.force_ruby_platform + @dependency.force_ruby_platform end def current_platform? - @dependency&.current_platform? + @dependency.current_platform? end end end diff --git a/lib/bundler/source/git.rb b/lib/bundler/source/git.rb index fd34edffb7..745ae05c5c 100644 --- a/lib/bundler/source/git.rb +++ b/lib/bundler/source/git.rb @@ -72,7 +72,7 @@ module Bundler elsif ref ref else - git_proxy.branch + current_branch end rev = "at #{at}@#{shortref_for_display(revision)}" @@ -126,7 +126,7 @@ module Bundler path = Pathname.new(path) path = path.expand_path(Bundler.root) unless path.relative? - unless options["branch"] || Bundler.settings[:disable_local_branch_check] + unless branch || Bundler.settings[:disable_local_branch_check] raise GitError, "Cannot use local override for #{name} at #{path} because " \ ":branch is not specified in Gemfile. Specify a branch or run " \ "`bundle config unset local.#{override_for(original_path)}` to remove the local override" @@ -141,11 +141,11 @@ module Bundler # Create a new git proxy without the cached revision # so the Gemfile.lock always picks up the new revision. - @git_proxy = GitProxy.new(path, uri, ref) + @git_proxy = GitProxy.new(path, uri, options) - if git_proxy.branch != options["branch"] && !Bundler.settings[:disable_local_branch_check] + if current_branch != branch && !Bundler.settings[:disable_local_branch_check] raise GitError, "Local override for #{name} at #{path} is using branch " \ - "#{git_proxy.branch} but Gemfile specifies #{options["branch"]}" + "#{current_branch} but Gemfile specifies #{branch}" end changed = cached_revision && cached_revision != git_proxy.revision @@ -228,6 +228,10 @@ module Bundler git_proxy.revision end + def current_branch + git_proxy.current_branch + end + def allow_git_ops? @allow_remote || @allow_cached end @@ -313,7 +317,7 @@ module Bundler end def git_proxy - @git_proxy ||= GitProxy.new(cache_path, uri, ref, cached_revision, self) + @git_proxy ||= GitProxy.new(cache_path, uri, options, cached_revision, self) end def fetch diff --git a/lib/bundler/source/git/git_proxy.rb b/lib/bundler/source/git/git_proxy.rb index 745a7fe118..5468536f86 100644 --- a/lib/bundler/source/git/git_proxy.rb +++ b/lib/bundler/source/git/git_proxy.rb @@ -47,13 +47,15 @@ module Bundler # All actions required by the Git source is encapsulated in this # object. class GitProxy - attr_accessor :path, :uri, :ref + attr_accessor :path, :uri, :branch, :tag, :ref attr_writer :revision - def initialize(path, uri, ref, revision = nil, git = nil) + def initialize(path, uri, options = {}, revision = nil, git = nil) @path = path @uri = uri - @ref = ref + @branch = options["branch"] + @tag = options["tag"] + @ref = options["ref"] @revision = revision @git = git end @@ -62,8 +64,8 @@ module Bundler @revision ||= find_local_revision end - def branch - @branch ||= allowed_with_path do + def current_branch + @current_branch ||= allowed_with_path do git("rev-parse", "--abbrev-ref", "HEAD", :dir => path).strip end end @@ -76,36 +78,33 @@ module Bundler end def version - git("--version").match(/(git version\s*)?((\.?\d+)+).*/)[2] + @version ||= full_version.match(/((\.?\d+)+).*/)[1] end def full_version - git("--version").sub("git version", "").strip + @full_version ||= git("--version").sub(/git version\s*/, "").strip end def checkout - return if path.exist? && has_revision_cached? - extra_ref = "#{ref}:#{ref}" if ref && ref.start_with?("refs/") + return if has_revision_cached? - Bundler.ui.info "Fetching #{URICredentialsFilter.credential_filtered_uri(uri)}" - - configured_uri = configured_uri_for(uri).to_s + Bundler.ui.info "Fetching #{credential_filtered_uri}" unless path.exist? SharedHelpers.filesystem_access(path.dirname) do |p| FileUtils.mkdir_p(p) end - git_retry "clone", "--bare", "--no-hardlinks", "--quiet", "--", configured_uri, path.to_s + git_retry "clone", "--bare", "--no-hardlinks", "--quiet", *extra_clone_args, "--", configured_uri, path.to_s return unless extra_ref end - with_path do - git_retry(*["fetch", "--force", "--quiet", "--tags", "--", configured_uri, "refs/heads/*:refs/heads/*", extra_ref].compact, :dir => path) - end + fetch_args = extra_fetch_args + fetch_args.unshift("--unshallow") if path.join("shallow").exist? && full_clone? + + git_retry(*["fetch", "--force", "--quiet", "--no-tags", *fetch_args, "--", configured_uri, refspec].compact, :dir => path) end def copy_to(destination, submodules = false) - # method 1 unless File.exist?(destination.join(".git")) begin SharedHelpers.filesystem_access(destination.dirname) do |p| @@ -114,7 +113,7 @@ module Bundler SharedHelpers.filesystem_access(destination) do |p| FileUtils.rm_rf(p) end - git_retry "clone", "--no-checkout", "--quiet", path.to_s, destination.to_s + git "clone", "--no-checkout", "--quiet", path.to_s, destination.to_s File.chmod(((File.stat(destination).mode | 0o777) & ~File.umask), destination) rescue Errno::EEXIST => e file_path = e.message[%r{.*?((?:[a-zA-Z]:)?/.*)}, 1] @@ -123,14 +122,10 @@ module Bundler "this file and try again." end end - # method 2 - git_retry "fetch", "--force", "--quiet", "--tags", path.to_s, :dir => destination - begin - git "reset", "--hard", @revision, :dir => destination - rescue GitCommandError => e - raise MissingGitRevisionError.new(e.command, destination, @revision, URICredentialsFilter.credential_filtered_uri(uri)) - end + git(*["fetch", "--force", "--quiet", *extra_fetch_args, path.to_s, revision_refspec].compact, :dir => destination) + + git "reset", "--hard", @revision, :dir => destination if submodules git_retry "submodule", "update", "--init", "--recursive", :dir => destination @@ -142,6 +137,69 @@ module Bundler private + def extra_ref + return false if not_pinned? + return true unless full_clone? + + ref.start_with?("refs/") + end + + def depth + return @depth if defined?(@depth) + + @depth = if legacy_locked_revision? || !supports_fetching_unreachable_refs? + nil + elsif not_pinned? + 1 + elsif ref.include?("~") + parsed_depth = ref.split("~").last + parsed_depth.to_i + 1 + elsif abbreviated_ref? + nil + else + 1 + end + end + + def refspec + if fully_qualified_ref + "#{fully_qualified_ref}:#{fully_qualified_ref}" + elsif ref.include?("~") + parsed_ref = ref.split("~").first + "#{parsed_ref}:#{parsed_ref}" + elsif ref.start_with?("refs/") + "#{ref}:#{ref}" + elsif abbreviated_ref? + nil + else + ref + end + end + + def fully_qualified_ref + return @fully_qualified_ref if defined?(@fully_qualified_ref) + + @fully_qualified_ref = if branch + "refs/heads/#{branch}" + elsif tag + "refs/tags/#{tag}" + elsif ref.nil? + "refs/heads/#{current_branch}" + end + end + + def not_pinned? + branch || tag || ref.nil? + end + + def abbreviated_ref? + ref =~ /\A\h+\z/ && ref !~ /\A\h{40}\z/ + end + + def legacy_locked_revision? + !@revision.nil? && @revision =~ /\A\h{7}\z/ + end + def git_null(*command, dir: nil) check_allowed(command) @@ -175,37 +233,40 @@ module Bundler end def has_revision_cached? - return unless @revision - with_path { git("cat-file", "-e", @revision, :dir => path) } + return unless @revision && path.exist? + git("cat-file", "-e", @revision, :dir => path) true rescue GitError false end - def remove_cache - FileUtils.rm_rf(path) - end - def find_local_revision allowed_with_path do - git("rev-parse", "--verify", ref || "HEAD", :dir => path).strip + git("rev-parse", "--verify", branch || tag || ref || "HEAD", :dir => path).strip end rescue GitCommandError => e - raise MissingGitRevisionError.new(e.command, path, ref, URICredentialsFilter.credential_filtered_uri(uri)) + raise MissingGitRevisionError.new(e.command, path, branch || tag || ref, credential_filtered_uri) end - # Adds credentials to the URI as Fetcher#configured_uri_for does - def configured_uri_for(uri) + # Adds credentials to the URI + def configured_uri if /https?:/ =~ uri remote = Bundler::URI(uri) config_auth = Bundler.settings[remote.to_s] || Bundler.settings[remote.host] remote.userinfo ||= config_auth remote.to_s + elsif File.exist?(uri) + "file://#{uri}" else - uri + uri.to_s end end + # Removes credentials from the URI + def credential_filtered_uri + URICredentialsFilter.credential_filtered_uri(uri) + end + def allow? allowed = @git ? @git.allow_git_ops? : true @@ -254,9 +315,43 @@ module Bundler end end + def extra_clone_args + return [] if full_clone? + + args = ["--depth", depth.to_s, "--single-branch"] + args.unshift("--no-tags") if supports_cloning_with_no_tags? + + args += ["--branch", branch || tag] if branch || tag + args + end + + def extra_fetch_args + return [] if full_clone? + + ["--depth", depth.to_s] + end + + def revision_refspec + return if legacy_locked_revision? + + revision + end + + def full_clone? + depth.nil? + end + def supports_minus_c? @supports_minus_c ||= Gem::Version.new(version) >= Gem::Version.new("1.8.5") end + + def supports_fetching_unreachable_refs? + @supports_fetching_unreachable_refs ||= Gem::Version.new(version) >= Gem::Version.new("2.5.0") + end + + def supports_cloning_with_no_tags? + @supports_cloning_with_no_tags ||= Gem::Version.new(version) >= Gem::Version.new("2.14.0-rc0") + end end end end diff --git a/spec/bundler/bundler/env_spec.rb b/spec/bundler/bundler/env_spec.rb index a6f4b2ba85..fb950c3c60 100644 --- a/spec/bundler/bundler/env_spec.rb +++ b/spec/bundler/bundler/env_spec.rb @@ -4,7 +4,7 @@ require "bundler/settings" require "openssl" RSpec.describe Bundler::Env do - let(:git_proxy_stub) { Bundler::Source::Git::GitProxy.new(nil, nil, nil) } + let(:git_proxy_stub) { Bundler::Source::Git::GitProxy.new(nil, nil) } describe "#report" do it "prints the environment" do diff --git a/spec/bundler/bundler/source/git/git_proxy_spec.rb b/spec/bundler/bundler/source/git/git_proxy_spec.rb index cffd72cc3f..841b8651e4 100644 --- a/spec/bundler/bundler/source/git/git_proxy_spec.rb +++ b/spec/bundler/bundler/source/git/git_proxy_spec.rb @@ -11,21 +11,24 @@ RSpec.describe Bundler::Source::Git::GitProxy do context "with configured credentials" do it "adds username and password to URI" do Bundler.settings.temporary(uri => "u:p") do - expect(subject).to receive(:git_retry).with("clone", "--bare", "--no-hardlinks", "--quiet", "--", "https://u:p@github.com/rubygems/rubygems.git", path.to_s) + allow(subject).to receive(:git).with("--version").and_return("git version 2.14.0") + expect(subject).to receive(:git_retry).with("clone", "--bare", "--no-hardlinks", "--quiet", "--no-tags", "--depth", "1", "--single-branch", "--", "https://u:p@github.com/rubygems/rubygems.git", path.to_s) subject.checkout end end it "adds username and password to URI for host" do Bundler.settings.temporary("github.com" => "u:p") do - expect(subject).to receive(:git_retry).with("clone", "--bare", "--no-hardlinks", "--quiet", "--", "https://u:p@github.com/rubygems/rubygems.git", path.to_s) + allow(subject).to receive(:git).with("--version").and_return("git version 2.14.0") + expect(subject).to receive(:git_retry).with("clone", "--bare", "--no-hardlinks", "--quiet", "--no-tags", "--depth", "1", "--single-branch", "--", "https://u:p@github.com/rubygems/rubygems.git", path.to_s) subject.checkout end end it "does not add username and password to mismatched URI" do Bundler.settings.temporary("https://u:p@github.com/rubygems/rubygems-mismatch.git" => "u:p") do - expect(subject).to receive(:git_retry).with("clone", "--bare", "--no-hardlinks", "--quiet", "--", uri, path.to_s) + allow(subject).to receive(:git).with("--version").and_return("git version 2.14.0") + expect(subject).to receive(:git_retry).with("clone", "--bare", "--no-hardlinks", "--quiet", "--no-tags", "--depth", "1", "--single-branch", "--", uri, path.to_s) subject.checkout end end @@ -34,7 +37,8 @@ RSpec.describe Bundler::Source::Git::GitProxy do Bundler.settings.temporary("github.com" => "u:p") do original = "https://orig:info@github.com/rubygems/rubygems.git" subject = described_class.new(Pathname("path"), original, "HEAD") - expect(subject).to receive(:git_retry).with("clone", "--bare", "--no-hardlinks", "--quiet", "--", original, path.to_s) + allow(subject).to receive(:git).with("--version").and_return("git version 2.14.0") + expect(subject).to receive(:git_retry).with("clone", "--bare", "--no-hardlinks", "--quiet", "--no-tags", "--depth", "1", "--single-branch", "--", original, path.to_s) subject.checkout end end @@ -122,33 +126,6 @@ RSpec.describe Bundler::Source::Git::GitProxy do end end - describe "#copy_to" do - let(:cache) { tmpdir("cache_path") } - let(:destination) { tmpdir("copy_to_path") } - let(:submodules) { false } - - context "when given a SHA as a revision" do - let(:revision) { "abcd" * 10 } - let(:command) { ["reset", "--hard", revision] } - let(:command_for_display) { "git #{command.shelljoin}" } - - it "fails gracefully when resetting to the revision fails" do - expect(subject).to receive(:git_retry).with("clone", any_args) { destination.mkpath } - expect(subject).to receive(:git_retry).with("fetch", any_args, :dir => destination) - expect(subject).to receive(:git).with(*command, :dir => destination).and_raise(Bundler::Source::Git::GitCommandError.new(command_for_display, destination)) - expect(subject).not_to receive(:git) - - expect { subject.copy_to(destination, submodules) }. - to raise_error( - Bundler::Source::Git::MissingGitRevisionError, - "Git error: command `#{command_for_display}` in directory #{destination} has failed.\n" \ - "Revision #{revision} does not exist in the repository #{uri}. Maybe you misspelled it?\n" \ - "If this error persists you could try removing the cache directory '#{destination}'" - ) - end - end - end - it "doesn't allow arbitrary code execution through Gemfile uris with a leading dash" do gemfile <<~G gem "poc", git: "-u./pay:load.sh" diff --git a/spec/bundler/cache/git_spec.rb b/spec/bundler/cache/git_spec.rb index fed8ba43f4..d2671b5f15 100644 --- a/spec/bundler/cache/git_spec.rb +++ b/spec/bundler/cache/git_spec.rb @@ -90,7 +90,6 @@ RSpec.describe "bundle cache with git" do expect(ref).not_to eq(old_ref) bundle "update", :all => true - bundle "config set cache_all true" bundle :cache expect(bundled_app("vendor/cache/foo-1.0-#{ref}")).to exist diff --git a/spec/bundler/commands/lock_spec.rb b/spec/bundler/commands/lock_spec.rb index 007e53f4e2..fd331d2f1b 100644 --- a/spec/bundler/commands/lock_spec.rb +++ b/spec/bundler/commands/lock_spec.rb @@ -595,4 +595,74 @@ RSpec.describe "bundle lock" do expect(read_lockfile).to eq(@lockfile.sub("foo (1.0)", "foo (2.0)").sub(/foo$/, "foo (= 2.0)")) end end + + context "when a system gem has incorrect dependencies, different from the lockfile" do + before do + build_repo4 do + build_gem "debug", "1.6.3" do |s| + s.add_dependency "irb", ">= 1.3.6" + end + + build_gem "irb", "1.5.0" + end + + system_gems "irb-1.5.0", :gem_repo => gem_repo4 + system_gems "debug-1.6.3", :gem_repo => gem_repo4 + + # simulate gemspec with wrong empty dependencies + debug_gemspec_path = system_gem_path("specifications/debug-1.6.3.gemspec") + debug_gemspec = Gem::Specification.load(debug_gemspec_path.to_s) + debug_gemspec.dependencies.clear + File.write(debug_gemspec_path, debug_gemspec.to_ruby) + end + + it "respects the existing lockfile, even when reresolving" do + gemfile <<~G + source "#{file_uri_for(gem_repo4)}" + + gem "debug" + G + + lockfile <<~L + GEM + remote: #{file_uri_for(gem_repo4)}/ + specs: + debug (1.6.3) + irb (>= 1.3.6) + irb (1.5.0) + + PLATFORMS + x86_64-linux + + DEPENDENCIES + debug + + BUNDLED WITH + #{Bundler::VERSION} + L + + simulate_platform "arm64-darwin-22" do + bundle "lock" + end + + expect(lockfile).to eq <<~L + GEM + remote: #{file_uri_for(gem_repo4)}/ + specs: + debug (1.6.3) + irb (>= 1.3.6) + irb (1.5.0) + + PLATFORMS + arm64-darwin-22 + x86_64-linux + + DEPENDENCIES + debug + + BUNDLED WITH + #{Bundler::VERSION} + L + end + end end diff --git a/spec/bundler/install/allow_offline_install_spec.rb b/spec/bundler/install/allow_offline_install_spec.rb index 524363fde5..4c6c77a61e 100644 --- a/spec/bundler/install/allow_offline_install_spec.rb +++ b/spec/bundler/install/allow_offline_install_spec.rb @@ -53,7 +53,10 @@ RSpec.describe "bundle install with :allow_offline_install" do File.open(tmp("broken_path/git"), "w", 0o755) do |f| f.puts strip_whitespace(<<-RUBY) #!/usr/bin/env ruby - if %w(fetch --force --quiet --tags refs/heads/*:refs/heads/*).-(ARGV).empty? || %w(clone --bare --no-hardlinks --quiet).-(ARGV).empty? + fetch_args = %w(fetch --force --quiet) + clone_args = %w(clone --bare --no-hardlinks --quiet) + + if (fetch_args.-(ARGV).empty? || clone_args.-(ARGV).empty?) && ARGV.any? {|arg| arg.start_with?("file://") } warn "git remote ops have been disabled" exit 1 end diff --git a/spec/bundler/install/gemfile/git_spec.rb b/spec/bundler/install/gemfile/git_spec.rb index 636a3daaad..4ac9f186ec 100644 --- a/spec/bundler/install/gemfile/git_spec.rb +++ b/spec/bundler/install/gemfile/git_spec.rb @@ -52,8 +52,9 @@ RSpec.describe "bundle install with git sources" do bundle "update foo" sha = git.ref_for("main", 11) - spec_file = default_bundle_path.join("bundler/gems/foo-1.0-#{sha}/foo.gemspec").to_s - ruby_code = Gem::Specification.load(spec_file).to_ruby + spec_file = default_bundle_path.join("bundler/gems/foo-1.0-#{sha}/foo.gemspec") + expect(spec_file).to exist + ruby_code = Gem::Specification.load(spec_file.to_s).to_ruby file_code = File.read(spec_file) expect(file_code).to eq(ruby_code) end @@ -218,6 +219,22 @@ RSpec.describe "bundle install with git sources" do expect(out).to eq("WIN") end + it "works when an abbreviated revision is added after an initial, potentially shallow clone" do + install_gemfile <<-G + source "#{file_uri_for(gem_repo1)}" + git "#{lib_path("foo-1.0")}" do + gem "foo" + end + G + + install_gemfile <<-G + source "#{file_uri_for(gem_repo1)}" + git "#{lib_path("foo-1.0")}", :ref => #{@revision[0..7].inspect} do + gem "foo" + end + G + end + it "works when the revision is a non-head ref" do # want to ensure we don't fallback to main update_git "foo", :path => lib_path("foo-1.0") do |s| diff --git a/spec/bundler/install/gemfile/specific_platform_spec.rb b/spec/bundler/install/gemfile/specific_platform_spec.rb index 1f615ec8a5..628ae89431 100644 --- a/spec/bundler/install/gemfile/specific_platform_spec.rb +++ b/spec/bundler/install/gemfile/specific_platform_spec.rb @@ -363,21 +363,17 @@ RSpec.describe "bundle install with specific platforms" do G error_message = <<~ERROR.strip - Could not find gem 'sorbet-static (= 0.5.6433)' with platform 'arm64-darwin-21', which is required by gem 'sorbet (= 0.5.6433)', in rubygems repository #{file_uri_for(gem_repo4)}/ or installed locally. - - The source contains the following gems matching 'sorbet-static (= 0.5.6433)': - * sorbet-static-0.5.6433-universal-darwin-20 - * sorbet-static-0.5.6433-x86_64-linux - ERROR - - error_message = <<~ERROR.strip Could not find compatible versions Because every version of sorbet depends on sorbet-static = 0.5.6433 - and sorbet-static = 0.5.6433 could not be found in rubygems repository #{file_uri_for(gem_repo4)}/ or installed locally, + and sorbet-static = 0.5.6433 could not be found in rubygems repository #{file_uri_for(gem_repo4)}/ or installed locally for any resolution platforms (arm64-darwin-21), every version of sorbet is forbidden. So, because Gemfile depends on sorbet = 0.5.6433, version solving has failed. + + The source contains the following gems matching 'sorbet-static (= 0.5.6433)': + * sorbet-static-0.5.6433-universal-darwin-20 + * sorbet-static-0.5.6433-x86_64-linux ERROR simulate_platform "arm64-darwin-21" do diff --git a/spec/bundler/install/gems/resolving_spec.rb b/spec/bundler/install/gems/resolving_spec.rb index 3977122568..279d3422c1 100644 --- a/spec/bundler/install/gems/resolving_spec.rb +++ b/spec/bundler/install/gems/resolving_spec.rb @@ -374,6 +374,57 @@ RSpec.describe "bundle install with install-time dependencies" do end end + context "with a Gemfile and lock file that don't resolve under the current platform" do + before do + build_repo4 do + build_gem "sorbet", "0.5.10554" do |s| + s.add_dependency "sorbet-static", "0.5.10554" + end + + build_gem "sorbet-static", "0.5.10554" do |s| + s.platform = "universal-darwin-21" + end + end + + gemfile <<~G + source "#{file_uri_for(gem_repo4)}" + gem 'sorbet', '= 0.5.10554' + G + + lockfile <<~L + GEM + remote: #{file_uri_for(gem_repo4)}/ + specs: + sorbet (0.5.10554) + sorbet-static (= 0.5.10554) + sorbet-static (0.5.10554-universal-darwin-21) + + PLATFORMS + arm64-darwin-21 + + DEPENDENCIES + sorbet (= 0.5.10554) + + BUNDLED WITH + #{Bundler::VERSION} + L + end + + it "raises a proper error" do + simulate_platform "aarch64-linux" do + bundle "install", :raise_on_error => false + end + + nice_error = strip_whitespace(<<-E).strip + Could not find gem 'sorbet-static (= 0.5.10554)' with platforms 'arm64-darwin-21', 'aarch64-linux' in rubygems repository #{file_uri_for(gem_repo4)}/ or installed locally. + + The source contains the following gems matching 'sorbet-static (= 0.5.10554)': + * sorbet-static-0.5.10554-universal-darwin-21 + E + expect(err).to end_with(nice_error) + end + end + it "gives a meaningful error on ruby version mismatches between dependencies" do build_repo4 do build_gem "requires-old-ruby" do |s| diff --git a/spec/bundler/lock/git_spec.rb b/spec/bundler/lock/git_spec.rb index 56db5d8305..df1564d614 100644 --- a/spec/bundler/lock/git_spec.rb +++ b/spec/bundler/lock/git_spec.rb @@ -14,6 +14,14 @@ RSpec.describe "bundle lock with git gems" do expect(the_bundle).to include_gems "foo 1.0.0" end + it "doesn't print errors even if running lock after removing the cache" do + FileUtils.rm_rf(Dir[default_cache_path("git/foo-1.0-*")].first) + + bundle "lock --verbose" + + expect(err).to be_empty + end + it "locks a git source to the current ref" do update_git "foo" bundle :install diff --git a/spec/bundler/support/filters.rb b/spec/bundler/support/filters.rb index 523f7c742c..78545d2e64 100644 --- a/spec/bundler/support/filters.rb +++ b/spec/bundler/support/filters.rb @@ -21,7 +21,7 @@ end RSpec.configure do |config| config.filter_run_excluding :realworld => true - git_version = Bundler::Source::Git::GitProxy.new(nil, nil, nil).version + git_version = Bundler::Source::Git::GitProxy.new(nil, nil).version config.filter_run_excluding :git => RequirementChecker.against(git_version) config.filter_run_excluding :bundler => RequirementChecker.against(Bundler::VERSION.split(".")[0]) diff --git a/spec/bundler/support/path.rb b/spec/bundler/support/path.rb index 7443e78d52..c9538f6b36 100644 --- a/spec/bundler/support/path.rb +++ b/spec/bundler/support/path.rb @@ -118,6 +118,14 @@ module Spec end end + def default_cache_path(*path) + if Bundler.feature_flag.global_gem_cache? + home(".bundle/cache", *path) + else + default_bundle_path("cache/bundler", *path) + end + end + def bundled_app(*path) root = tmp.join("bundled_app") FileUtils.mkdir_p(root) diff --git a/spec/bundler/update/git_spec.rb b/spec/bundler/update/git_spec.rb index 427a0bb713..59e3d2f5fb 100644 --- a/spec/bundler/update/git_spec.rb +++ b/spec/bundler/update/git_spec.rb @@ -120,6 +120,7 @@ RSpec.describe "bundle update" do G bundle "update", :all => true + expect(err).to be_empty end describe "with submodules" do |