diff options
Diffstat (limited to 'lib/bundler/cli/outdated.rb')
| -rw-r--r-- | lib/bundler/cli/outdated.rb | 337 |
1 files changed, 207 insertions, 130 deletions
diff --git a/lib/bundler/cli/outdated.rb b/lib/bundler/cli/outdated.rb index 5125cc710b..465e56ada2 100644 --- a/lib/bundler/cli/outdated.rb +++ b/lib/bundler/cli/outdated.rb @@ -2,224 +2,255 @@ module Bundler class CLI::Outdated - attr_reader :options, :gems + attr_reader :options, :gems, :options_include_groups, :filter_options_patch, :sources, :strict + attr_accessor :outdated_gems def initialize(options, gems) @options = options @gems = gems - end + @sources = Array(options[:source]) - def run - check_for_deployment_mode + @filter_options_patch = options.keys & %w[filter-major filter-minor filter-patch] - sources = Array(options[:source]) + @outdated_gems = [] - gems.each do |gem_name| - Bundler::CLI::Common.select_spec(gem_name) + @options_include_groups = [:group, :groups].any? do |v| + options.keys.include?(v.to_s) end + # the patch level options imply strict is also true. It wouldn't make + # sense otherwise. + @strict = options["filter-strict"] || Bundler::CLI::Common.patch_level_options(options).any? + end + + def run + check_for_deployment_mode! + + Bundler::CLI::Common.validate_cooldown!(options[:cooldown]) + Bundler.settings.set_command_option_if_given :cooldown, options[:cooldown] + Bundler.definition.validate_runtime! current_specs = Bundler.ui.silence { Bundler.definition.resolve } - current_dependencies = {} - Bundler.ui.silence do - Bundler.load.dependencies.each do |dep| - current_dependencies[dep.name] = dep + + gems.each do |gem_name| + if current_specs[gem_name].empty? + raise GemNotFound, "Could not find gem '#{gem_name}'." end end + current_dependencies = Bundler.ui.silence do + Bundler.load.dependencies.map {|dep| [dep.name, dep] }.to_h + end + definition = if gems.empty? && sources.empty? # We're doing a full update Bundler.definition(true) else - Bundler.definition(:gems => gems, :sources => sources) + Bundler.definition(gems: gems, sources: sources) end Bundler::CLI::Common.configure_gem_version_promoter( Bundler.definition, - options + options.merge(strict: @strict) ) - # the patch level options imply strict is also true. It wouldn't make - # sense otherwise. - strict = options[:strict] || - Bundler::CLI::Common.patch_level_options(options).any? - - filter_options_patch = options.keys & - %w[filter-major filter-minor filter-patch] - definition_resolution = proc do options[:local] ? definition.resolve_with_cache! : definition.resolve_remotely! end if options[:parseable] - Bundler.ui.silence(&definition_resolution) + Bundler.ui.progress(&definition_resolution) else definition_resolution.call end Bundler.ui.info "" - outdated_gems_by_groups = {} - outdated_gems_list = [] # Loop through the current specs gemfile_specs, dependency_specs = current_specs.partition do |spec| current_dependencies.key? spec.name end - (gemfile_specs + dependency_specs).sort_by(&:name).each do |current_spec| - next if !gems.empty? && !gems.include?(current_spec.name) + specs = if options["only-explicit"] + gemfile_specs + else + gemfile_specs + dependency_specs + end + + specs.sort_by(&:name).uniq(&:name).each do |current_spec| + next unless gems.empty? || gems.include?(current_spec.name) - dependency = current_dependencies[current_spec.name] - active_spec = retrieve_active_spec(strict, definition, current_spec) + active_spec = retrieve_active_spec(definition, current_spec) + next unless active_spec - next if active_spec.nil? - if filter_options_patch.any? - update_present = update_present_via_semver_portions(current_spec, active_spec, options) - next unless update_present - end + next unless filter_options_patch.empty? || update_present_via_semver_portions(current_spec, active_spec, options) gem_outdated = Gem::Version.new(active_spec.version) > Gem::Version.new(current_spec.version) next unless gem_outdated || (current_spec.git_version != active_spec.git_version) - groups = nil + + dependency = current_dependencies[current_spec.name] + groups = "" if dependency && !options[:parseable] groups = dependency.groups.join(", ") end - outdated_gems_list << { :active_spec => active_spec, - :current_spec => current_spec, - :dependency => dependency, - :groups => groups } - - outdated_gems_by_groups[groups] ||= [] - outdated_gems_by_groups[groups] << { :active_spec => active_spec, - :current_spec => current_spec, - :dependency => dependency, - :groups => groups } + outdated_gems << { + active_spec: active_spec, + current_spec: current_spec, + dependency: dependency, + groups: groups, + } end - if outdated_gems_list.empty? - display_nothing_outdated_message(filter_options_patch) + relevant_outdated_gems = if options_include_groups + outdated_gems.group_by {|g| g[:groups] }.sort.flat_map do |groups, gems| + contains_group = groups.split(", ").include?(options[:group]) + next unless options[:groups] || contains_group + + gems + end.compact else - unless options[:parseable] - if options[:pre] - Bundler.ui.info "Outdated gems included in the bundle (including " \ - "pre-releases):" - else - Bundler.ui.info "Outdated gems included in the bundle:" - end - end + outdated_gems + end - options_include_groups = [:group, :groups].select do |v| - options.keys.include?(v.to_s) + if relevant_outdated_gems.empty? + unless options[:parseable] + Bundler.ui.info(nothing_outdated_message) end - - if options_include_groups.any? - ordered_groups = outdated_gems_by_groups.keys.compact.sort - [nil, ordered_groups].flatten.each do |groups| - gems = outdated_gems_by_groups[groups] - contains_group = if groups - groups.split(",").include?(options[:group]) - else - options[:group] == "group" - end - - next if (!options[:groups] && !contains_group) || gems.nil? - - unless options[:parseable] - if groups - Bundler.ui.info "===== Group #{groups} =====" - else - Bundler.ui.info "===== Without group =====" - end - end - - gems.each do |gem| - print_gem( - gem[:current_spec], - gem[:active_spec], - gem[:dependency], - groups, - options_include_groups.any? - ) - end - end + else + if options[:parseable] + print_gems(relevant_outdated_gems) else - outdated_gems_list.each do |gem| - print_gem( - gem[:current_spec], - gem[:active_spec], - gem[:dependency], - gem[:groups], - options_include_groups.any? - ) - end + print_gems_table(relevant_outdated_gems) end exit 1 end end - private + private + + def loaded_from_for(spec) + return unless spec.respond_to?(:loaded_from) + + spec.loaded_from + end + + def groups_text(group_text, groups) + "#{group_text}#{groups.split(",").size > 1 ? "s" : ""} \"#{groups}\"" + end - def retrieve_active_spec(strict, definition, current_spec) - if strict - active_spec = definition.find_resolved_spec(current_spec) + def nothing_outdated_message + if filter_options_patch.any? + display = filter_options_patch.map do |o| + o.sub("filter-", "") + end.join(" or ") + + "No #{display} updates to display.\n" else - active_specs = definition.find_indexed_specs(current_spec) - if !current_spec.version.prerelease? && !options[:pre] && active_specs.size > 1 - active_specs.delete_if {|b| b.respond_to?(:version) && b.version.prerelease? } - end - active_spec = active_specs.last + "Bundle up to date!\n" end + end + + def retrieve_active_spec(definition, current_spec) + active_spec = definition.resolve.find_by_name_and_platform(current_spec.name, current_spec.platform) + return unless active_spec + + return active_spec if strict - active_spec + active_specs = active_spec.source.specs.search(current_spec.name).select {|spec| spec.installable_on_platform?(current_spec.platform) }.sort_by(&:version) + if !current_spec.version.prerelease? && !options[:pre] && active_specs.size > 1 + active_specs.delete_if {|b| b.respond_to?(:version) && b.version.prerelease? } + end + active_specs.last end - def display_nothing_outdated_message(filter_options_patch) - unless options[:parseable] - if filter_options_patch.any? - display = filter_options_patch.map do |o| - o.sub("filter-", "") - end.join(" or ") + def print_gems(gems_list) + gems_list.each do |gem| + print_gem( + gem[:current_spec], + gem[:active_spec], + gem[:dependency], + gem[:groups], + ) + end + end - Bundler.ui.info "No #{display} updates to display.\n" - else - Bundler.ui.info "Bundle up to date!\n" - end + def print_gems_table(gems_list) + data = gems_list.map do |gem| + gem_column_for( + gem[:current_spec], + gem[:active_spec], + gem[:dependency], + gem[:groups], + ) end + + print_indented([table_header] + data) end - def print_gem(current_spec, active_spec, dependency, groups, options_include_groups) + def print_gem(current_spec, active_spec, dependency, groups) spec_version = "#{active_spec.version}#{active_spec.git_version}" - spec_version += " (from #{active_spec.loaded_from})" if Bundler.ui.debug? && active_spec.loaded_from + if Bundler.ui.debug? + loaded_from = loaded_from_for(active_spec) + spec_version += " (from #{loaded_from})" if loaded_from + end current_version = "#{current_spec.version}#{current_spec.git_version}" - if dependency && dependency.specific? + if dependency&.specific? dependency_version = %(, requested #{dependency.requirement}) end spec_outdated_info = "#{active_spec.name} (newest #{spec_version}, " \ - "installed #{current_version}#{dependency_version})" + "installed #{current_version}#{dependency_version}" + + release_date = release_date_for(active_spec) + spec_outdated_info += ", released #{release_date}" unless release_date.empty? + + remaining = cooldown_days_remaining(active_spec) + spec_outdated_info += ", in cooldown for #{remaining} more day#{"s" if remaining > 1}" if remaining + + spec_outdated_info += ")" output_message = if options[:parseable] spec_outdated_info.to_s - elsif options_include_groups || !groups + elsif options_include_groups || groups.empty? " * #{spec_outdated_info}" else - " * #{spec_outdated_info} in groups \"#{groups}\"" + " * #{spec_outdated_info} in #{groups_text("group", groups)}" end Bundler.ui.info output_message.rstrip end - def check_for_deployment_mode - return unless Bundler.frozen? - suggested_command = if Bundler.settings.locations("frozen")[:global] - "bundle config --delete frozen" + def gem_column_for(current_spec, active_spec, dependency, groups) + current_version = "#{current_spec.version}#{current_spec.git_version}" + spec_version = "#{active_spec.version}#{active_spec.git_version}" + remaining = cooldown_days_remaining(active_spec) + spec_version += " (cooldown #{remaining}d)" if remaining + dependency = dependency.requirement if dependency + + ret_val = [active_spec.name, current_version, spec_version, dependency.to_s, groups.to_s] + ret_val << release_date_for(active_spec) + ret_val << loaded_from_for(active_spec).to_s if Bundler.ui.debug? + ret_val + end + + def cooldown_days_remaining(spec, now = Time.now) + return nil unless spec.respond_to?(:created_at) && spec.created_at + return nil unless spec.respond_to?(:remote) && spec.remote + days = spec.remote.effective_cooldown + return nil if days.nil? || days <= 0 + remaining = days - ((now - spec.created_at) / 86_400.0) + remaining > 0 ? remaining.ceil : nil + end + + def check_for_deployment_mode! + return unless Bundler.frozen_bundle? + suggested_command = if Bundler.settings.locations("frozen").keys.&([:global, :local]).any? + "bundle config unset frozen" elsif Bundler.settings.locations("deployment").keys.&([:global, :local]).any? - "bundle config --delete deployment" - else - "bundle install --no-deployment" + "bundle config unset deployment" end raise ProductionError, "You are trying to check outdated gems in " \ "deployment mode. Run `bundle outdated` elsewhere.\n" \ @@ -254,7 +285,53 @@ module Bundler def get_version_semver_portion_value(spec, version_portion_index) version_section = spec.version.segments[version_portion_index, 1] - version_section.nil? ? 0 : (version_section.first || 0) + version_section.to_a[0].to_i + end + + def print_indented(matrix) + header = matrix[0] + data = matrix[1..-1] + + column_sizes = Array.new(header.size) do |index| + matrix.max_by {|row| row[index].length }[index].length + end + + Bundler.ui.info justify(header, column_sizes) + + data.sort_by! {|row| row[0] } + + data.each do |row| + Bundler.ui.info justify(row, column_sizes) + end + end + + def table_header + header = ["Gem", "Current", "Latest", "Requested", "Groups", "Release Date"] + header << "Path" if Bundler.ui.debug? + header + end + + def release_date_for(spec) + return "" unless spec.respond_to?(:date) + + date = spec.date + return "" unless date + + return "" unless Gem.const_defined?(:DEFAULT_SOURCE_DATE_EPOCH) + default_date = Time.at(Gem::DEFAULT_SOURCE_DATE_EPOCH).utc + default_date = Time.utc(default_date.year, default_date.month, default_date.day) + + date = date.utc if date.respond_to?(:utc) + + return "" if date == default_date + + date.strftime("%Y-%m-%d") + end + + def justify(row, sizes) + row.each_with_index.map do |element, index| + element.ljust(sizes[index]) + end.join(" ").strip + "\n" end end end |
