summaryrefslogtreecommitdiff
path: root/lib/bundler/cli/outdated.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/bundler/cli/outdated.rb')
-rw-r--r--lib/bundler/cli/outdated.rb337
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