diff --git a/Manifest.txt b/Manifest.txt index 09b2ff09487d..7e413102301d 100644 --- a/Manifest.txt +++ b/Manifest.txt @@ -32,6 +32,7 @@ bundler/lib/bundler/cli/config.rb bundler/lib/bundler/cli/console.rb bundler/lib/bundler/cli/doctor.rb bundler/lib/bundler/cli/exec.rb +bundler/lib/bundler/cli/fund.rb bundler/lib/bundler/cli/gem.rb bundler/lib/bundler/cli/info.rb bundler/lib/bundler/cli/init.rb diff --git a/bundler/lib/bundler/cli.rb b/bundler/lib/bundler/cli.rb index 8d30001bd480..b4196621e5f7 100644 --- a/bundler/lib/bundler/cli.rb +++ b/bundler/lib/bundler/cli.rb @@ -439,6 +439,14 @@ def outdated(*gems) Outdated.new(options, gems).run end + desc "fund [OPTIONS]", "Lists information about gems seeking funding assistance" + method_option "group", :aliases => "-g", :type => :array, :banner => + "Fetch funding information for a specific group" + def fund + require_relative "cli/fund" + Fund.new(options).run + end + desc "cache [OPTIONS]", "Locks and then caches all of the gems into vendor/cache" method_option "all", :type => :boolean, :default => Bundler.feature_flag.cache_all?, diff --git a/bundler/lib/bundler/cli/common.rb b/bundler/lib/bundler/cli/common.rb index cec7bcadb433..23ac78a103b8 100644 --- a/bundler/lib/bundler/cli/common.rb +++ b/bundler/lib/bundler/cli/common.rb @@ -14,6 +14,20 @@ def self.print_post_install_message(name, msg) Bundler.ui.info msg end + def self.output_fund_metadata_summary + definition = Bundler.definition + current_dependencies = definition.requested_dependencies + current_specs = definition.specs + + count = current_dependencies.count {|dep| current_specs[dep.name].first.metadata.key?("funding_uri") } + + return if count.zero? + + intro = count > 1 ? "#{count} installed gems you directly depend on are" : "#{count} installed gem you directly depend on is" + message = "#{intro} looking for funding.\n Run `bundle fund` for details" + Bundler.ui.info message + end + def self.output_without_groups_message(command) return if Bundler.settings[:without].empty? Bundler.ui.confirm without_groups_message(command) diff --git a/bundler/lib/bundler/cli/fund.rb b/bundler/lib/bundler/cli/fund.rb new file mode 100644 index 000000000000..52db5aef68ae --- /dev/null +++ b/bundler/lib/bundler/cli/fund.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Bundler + class CLI::Fund + attr_reader :options + + def initialize(options) + @options = options + end + + def run + Bundler.definition.validate_runtime! + + groups = Array(options[:group]).map(&:to_sym) + + deps = if groups.any? + Bundler.definition.dependencies_for(groups) + else + Bundler.definition.current_dependencies + end + + fund_info = deps.each_with_object([]) do |dep, arr| + spec = Bundler.definition.specs[dep.name].first + if spec.metadata.key?("funding_uri") + arr << "* #{spec.name} (#{spec.version})\n Funding: #{spec.metadata["funding_uri"]}" + end + end + + if fund_info.empty? + Bundler.ui.info "None of the installed gems you directly depend on are looking for funding." + else + Bundler.ui.info fund_info.join("\n") + end + end + end +end diff --git a/bundler/lib/bundler/cli/info.rb b/bundler/lib/bundler/cli/info.rb index 68b2d8035420..3111b64a33af 100644 --- a/bundler/lib/bundler/cli/info.rb +++ b/bundler/lib/bundler/cli/info.rb @@ -60,6 +60,7 @@ def print_gem_info(spec) gem_info << "\tHomepage: #{spec.homepage}\n" if spec.homepage gem_info << "\tDocumentation: #{metadata["documentation_uri"]}\n" if metadata.key?("documentation_uri") gem_info << "\tSource Code: #{metadata["source_code_uri"]}\n" if metadata.key?("source_code_uri") + gem_info << "\tFunding: #{metadata["funding_uri"]}\n" if metadata.key?("funding_uri") gem_info << "\tWiki: #{metadata["wiki_uri"]}\n" if metadata.key?("wiki_uri") gem_info << "\tChangelog: #{metadata["changelog_uri"]}\n" if metadata.key?("changelog_uri") gem_info << "\tBug Tracker: #{metadata["bug_tracker_uri"]}\n" if metadata.key?("bug_tracker_uri") diff --git a/bundler/lib/bundler/cli/install.rb b/bundler/lib/bundler/cli/install.rb index 95066f53fac0..edf86fe1ba77 100644 --- a/bundler/lib/bundler/cli/install.rb +++ b/bundler/lib/bundler/cli/install.rb @@ -82,6 +82,8 @@ def run require_relative "clean" Bundler::CLI::Clean.new(options).run end + + Bundler::CLI::Common.output_fund_metadata_summary rescue GemNotFound, VersionConflict => e if options[:local] && Bundler.app_cache.exist? Bundler.ui.warn "Some gems seem to be missing from your #{Bundler.settings.app_cache_path} directory." diff --git a/bundler/lib/bundler/cli/update.rb b/bundler/lib/bundler/cli/update.rb index 529dd9c94d1c..ae908be65e95 100644 --- a/bundler/lib/bundler/cli/update.rb +++ b/bundler/lib/bundler/cli/update.rb @@ -106,6 +106,8 @@ def run Bundler.ui.confirm "Bundle updated!" Bundler::CLI::Common.output_without_groups_message(:update) Bundler::CLI::Common.output_post_install_messages installer.post_install_messages + + Bundler::CLI::Common.output_fund_metadata_summary end end end diff --git a/bundler/lib/bundler/definition.rb b/bundler/lib/bundler/definition.rb index c464650592af..fc35f31be16c 100644 --- a/bundler/lib/bundler/definition.rb +++ b/bundler/lib/bundler/definition.rb @@ -233,6 +233,12 @@ def requested_specs end end + def requested_dependencies + groups = requested_groups + groups.map!(&:to_sym) + dependencies_for(groups) + end + def current_dependencies dependencies.select do |d| d.should_include? && !d.gem_platforms(@platforms).empty? @@ -244,6 +250,12 @@ def specs_for(groups) specs.for(expand_dependencies(deps)) end + def dependencies_for(groups) + current_dependencies.reject do |d| + (d.groups & groups).empty? + end + end + # Resolve all the dependencies specified in Gemfile. It ensures that # dependencies that have been already resolved via locked file and are fresh # are reused when resolving dependencies @@ -898,18 +910,6 @@ def expand_dependency_with_platforms(dep, platforms) end end - def dependencies_for(groups) - current_dependencies.reject do |d| - (d.groups & groups).empty? - end - end - - def requested_dependencies - groups = requested_groups - groups.map!(&:to_sym) - dependencies_for(groups) - end - def source_requirements # Load all specs from remote sources index diff --git a/bundler/spec/commands/fund_spec.rb b/bundler/spec/commands/fund_spec.rb new file mode 100644 index 000000000000..ee3aff3a29dc --- /dev/null +++ b/bundler/spec/commands/fund_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +RSpec.describe "bundle fund" do + it "prints fund information for all gems in the bundle" do + install_gemfile <<-G + source "#{file_uri_for(gem_repo1)}" + gem 'has_metadata' + gem 'has_funding' + gem 'rack-obama' + G + + bundle "fund" + + expect(out).to include("* has_metadata (1.0)\n Funding: https://example.com/has_metadata/funding") + expect(out).to include("* has_funding (1.2.3)\n Funding: https://example.com/has_funding/funding") + expect(out).to_not include("rack-obama") + end + + it "does not consider fund information for gem dependencies" do + install_gemfile <<-G + source "#{file_uri_for(gem_repo1)}" + gem 'gem_with_dependent_funding' + G + + bundle "fund" + + expect(out).to_not include("* has_funding (1.2.3)\n Funding: https://example.com/has_funding/funding") + expect(out).to_not include("gem_with_dependent_funding") + end + + it "prints message if none of the gems have fund information" do + install_gemfile <<-G + source "#{file_uri_for(gem_repo1)}" + gem 'rack-obama' + G + + bundle "fund" + + expect(out).to include("None of the installed gems you directly depend on are looking for funding.") + end + + describe "with --group option" do + it "prints fund message for only specified group gems" do + install_gemfile <<-G + source "#{file_uri_for(gem_repo1)}" + gem 'has_metadata', :group => :development + gem 'has_funding' + G + + bundle "fund --group development" + expect(out).to include("* has_metadata (1.0)\n Funding: https://example.com/has_metadata/funding") + expect(out).to_not include("* has_funding (1.2.3)\n Funding: https://example.com/has_funding/funding") + end + end +end diff --git a/bundler/spec/commands/info_spec.rb b/bundler/spec/commands/info_spec.rb index 9286e6824a2d..eec9c773bcf2 100644 --- a/bundler/spec/commands/info_spec.rb +++ b/bundler/spec/commands/info_spec.rb @@ -66,6 +66,7 @@ \tHomepage: http://example.com \tDocumentation: https://www.example.info/gems/bestgemever/0.0.1 \tSource Code: https://example.com/user/bestgemever +\tFunding: https://example.com/has_metadata/funding \tWiki: https://example.com/user/bestgemever/wiki \tChangelog: https://example.com/user/bestgemever/CHANGELOG.md \tBug Tracker: https://example.com/user/bestgemever/issues diff --git a/bundler/spec/install/gems/fund_spec.rb b/bundler/spec/install/gems/fund_spec.rb new file mode 100644 index 000000000000..57e7c3aed393 --- /dev/null +++ b/bundler/spec/install/gems/fund_spec.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +RSpec.describe "bundle install" do + context "with gem sources" do + context "when gems include a fund URI" do + it "displays the plural fund message after installing" do + install_gemfile <<-G + source "#{file_uri_for(gem_repo1)}" + gem 'has_metadata' + gem 'has_funding' + gem 'rack-obama' + G + + expect(out).to include("2 installed gems you directly depend on are looking for funding.") + end + + it "displays the singular fund message after installing" do + install_gemfile <<-G + source "#{file_uri_for(gem_repo1)}" + gem 'has_funding' + gem 'rack-obama' + G + + expect(out).to include("1 installed gem you directly depend on is looking for funding.") + end + end + + context "when gems do not include fund messages" do + it "does not display any fund messages" do + install_gemfile <<-G + source "#{file_uri_for(gem_repo1)}" + gem "activesupport" + G + + expect(out).not_to include("gem you depend on") + end + end + + context "when a dependency includes a fund message" do + it "does not display the fund message" do + install_gemfile <<-G + source "#{file_uri_for(gem_repo1)}" + gem 'gem_with_dependent_funding' + G + + expect(out).not_to include("gem you depend on") + end + end + end + + context "with git sources" do + context "when gems include fund URI" do + it "displays the fund message after installing" do + build_git "also_has_funding" do |s| + s.metadata = { + "funding_uri" => "https://example.com/also_has_funding/funding", + } + end + install_gemfile <<-G + source "#{file_uri_for(gem_repo1)}" + gem 'also_has_funding', :git => '#{lib_path("also_has_funding-1.0")}' + G + + expect(out).to include("1 installed gem you directly depend on is looking for funding.") + end + + it "displays the fund message if repo is updated" do + build_git "also_has_funding" do |s| + s.metadata = { + "funding_uri" => "https://example.com/also_has_funding/funding", + } + end + install_gemfile <<-G + source "#{file_uri_for(gem_repo1)}" + gem 'also_has_funding', :git => '#{lib_path("also_has_funding-1.0")}' + G + + build_git "also_has_funding", "1.1" do |s| + s.metadata = { + "funding_uri" => "https://example.com/also_has_funding/funding", + } + end + install_gemfile <<-G + source "#{file_uri_for(gem_repo1)}" + gem 'also_has_funding', :git => '#{lib_path("also_has_funding-1.1")}' + G + + expect(out).to include("1 installed gem you directly depend on is looking for funding.") + end + + it "displays the fund message if repo is not updated" do + build_git "also_has_funding" do |s| + s.metadata = { + "funding_uri" => "https://example.com/also_has_funding/funding", + } + end + gemfile <<-G + source "#{file_uri_for(gem_repo1)}" + gem 'also_has_funding', :git => '#{lib_path("also_has_funding-1.0")}' + G + + bundle :install + expect(out).to include("1 installed gem you directly depend on is looking for funding.") + + bundle :install + expect(out).to include("1 installed gem you directly depend on is looking for funding.") + end + end + end +end diff --git a/bundler/spec/support/builders.rb b/bundler/spec/support/builders.rb index e8481f98a377..a1770759a9df 100644 --- a/bundler/spec/support/builders.rb +++ b/bundler/spec/support/builders.rb @@ -322,10 +322,21 @@ def __pry__ "documentation_uri" => "https://www.example.info/gems/bestgemever/0.0.1", "homepage_uri" => "https://bestgemever.example.io", "mailing_list_uri" => "https://groups.example.com/bestgemever", + "funding_uri" => "https://example.com/has_metadata/funding", "source_code_uri" => "https://example.com/user/bestgemever", "wiki_uri" => "https://example.com/user/bestgemever/wiki", } end + + build_gem "has_funding", "1.2.3" do |s| + s.metadata = { + "funding_uri" => "https://example.com/has_funding/funding", + } + end + + build_gem "gem_with_dependent_funding", "1.0" do |s| + s.add_dependency "has_funding" + end end end diff --git a/bundler/spec/update/gems/fund_spec.rb b/bundler/spec/update/gems/fund_spec.rb new file mode 100644 index 000000000000..6d7075b42412 --- /dev/null +++ b/bundler/spec/update/gems/fund_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +RSpec.describe "bundle update" do + before do + gemfile <<-G + source "#{file_uri_for(gem_repo1)}" + gem 'has_metadata' + gem 'has_funding', '< 2.0' + G + + bundle :install + end + + context "when listed gems are updated" do + before do + gemfile <<-G + source "#{file_uri_for(gem_repo1)}" + gem 'has_metadata' + gem 'has_funding' + G + + bundle :update, :all => true + end + + it "displays fund message" do + expect(out).to include("2 installed gems you directly depend on are looking for funding.") + end + end +end