This commit is contained in:
@@ -0,0 +1,367 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Bundler
|
||||
class CLI::Outdated
|
||||
attr_reader :options, :gems, :options_include_groups, :filter_options_patch, :sources, :strict
|
||||
attr_accessor :outdated_gems
|
||||
|
||||
def initialize(options, gems)
|
||||
@options = options
|
||||
@gems = gems
|
||||
@sources = Array(options[:source])
|
||||
|
||||
@filter_options_patch = options.keys & %w[filter-major filter-minor filter-patch]
|
||||
|
||||
@outdated_gems = []
|
||||
|
||||
@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 }
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
Bundler::CLI::Common.configure_gem_version_promoter(
|
||||
Bundler.definition,
|
||||
options.merge(strict: @strict)
|
||||
)
|
||||
|
||||
definition_resolution = proc do
|
||||
options[:local] ? definition.resolve_with_cache! : definition.resolve_remotely!
|
||||
end
|
||||
|
||||
if options[:parseable]
|
||||
Bundler.ui.progress(&definition_resolution)
|
||||
else
|
||||
definition_resolution.call
|
||||
end
|
||||
|
||||
Bundler.ui.info ""
|
||||
|
||||
# Loop through the current specs
|
||||
gemfile_specs, dependency_specs = current_specs.partition do |spec|
|
||||
current_dependencies.key? spec.name
|
||||
end
|
||||
|
||||
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)
|
||||
|
||||
active_spec = retrieve_active_spec(definition, current_spec)
|
||||
next unless active_spec
|
||||
|
||||
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)
|
||||
|
||||
dependency = current_dependencies[current_spec.name]
|
||||
groups = ""
|
||||
if dependency && !options[:parseable]
|
||||
groups = dependency.groups.join(", ")
|
||||
end
|
||||
|
||||
outdated_gems << {
|
||||
active_spec: active_spec,
|
||||
current_spec: current_spec,
|
||||
dependency: dependency,
|
||||
groups: groups,
|
||||
}
|
||||
end
|
||||
|
||||
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
|
||||
outdated_gems
|
||||
end
|
||||
|
||||
if relevant_outdated_gems.empty?
|
||||
unless options[:parseable]
|
||||
Bundler.ui.info(nothing_outdated_message)
|
||||
end
|
||||
else
|
||||
if options[:parseable]
|
||||
print_gems(relevant_outdated_gems)
|
||||
else
|
||||
print_gems_table(relevant_outdated_gems)
|
||||
end
|
||||
|
||||
exit 1
|
||||
end
|
||||
end
|
||||
|
||||
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 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
|
||||
"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
|
||||
|
||||
matching_specs(active_spec, current_spec).last
|
||||
end
|
||||
|
||||
def matching_specs(active_spec, current_spec)
|
||||
@matching_specs ||= {}
|
||||
@matching_specs[[active_spec.source, current_spec.name, current_spec.platform]] ||= begin
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
# The newest version the cooldown setting would let bundler adopt right
|
||||
# now, when the newest overall version is still inside the window. Only a
|
||||
# version strictly between the installed one and the newest one is an
|
||||
# adoptable update worth showing.
|
||||
def newest_out_of_cooldown(active_spec, current_spec)
|
||||
newest = matching_specs(active_spec, current_spec).reverse_each.find {|spec| cooldown_days_remaining(spec).nil? }
|
||||
return unless newest
|
||||
return if newest.version >= active_spec.version || newest.version <= current_spec.version
|
||||
newest
|
||||
end
|
||||
|
||||
def print_gems(gems_list)
|
||||
gems_list.each do |gem|
|
||||
print_gem(
|
||||
gem[:current_spec],
|
||||
gem[:active_spec],
|
||||
gem[:dependency],
|
||||
gem[:groups],
|
||||
)
|
||||
end
|
||||
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)
|
||||
spec_version = "#{active_spec.version}#{active_spec.git_version}"
|
||||
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&.specific?
|
||||
dependency_version = %(, requested #{dependency.requirement})
|
||||
end
|
||||
|
||||
spec_outdated_info = "#{active_spec.name} (newest #{spec_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)
|
||||
if remaining
|
||||
spec_outdated_info += ", in cooldown for #{remaining} more day#{"s" if remaining > 1}"
|
||||
adoptable = newest_out_of_cooldown(active_spec, current_spec)
|
||||
spec_outdated_info += ", newest out of cooldown #{adoptable.version}" if adoptable
|
||||
end
|
||||
|
||||
spec_outdated_info += ")"
|
||||
|
||||
output_message = if options[:parseable]
|
||||
spec_outdated_info.to_s
|
||||
elsif options_include_groups || groups.empty?
|
||||
" * #{spec_outdated_info}"
|
||||
else
|
||||
" * #{spec_outdated_info} in #{groups_text("group", groups)}"
|
||||
end
|
||||
|
||||
Bundler.ui.info output_message.rstrip
|
||||
end
|
||||
|
||||
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)
|
||||
if remaining
|
||||
adoptable = newest_out_of_cooldown(active_spec, current_spec)
|
||||
adoptable_note = adoptable ? ", #{adoptable.version} out of cooldown" : ""
|
||||
spec_version += " (cooldown #{remaining}d#{adoptable_note})"
|
||||
end
|
||||
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 = cooldown_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 cooldown_now
|
||||
@cooldown_now ||= Time.now
|
||||
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 unset deployment"
|
||||
end
|
||||
raise ProductionError, "You are trying to check outdated gems in " \
|
||||
"deployment mode. Run `bundle outdated` elsewhere.\n" \
|
||||
"\nIf this is a development machine, remove the " \
|
||||
"#{Bundler.default_gemfile} freeze" \
|
||||
"\nby running `#{suggested_command}`."
|
||||
end
|
||||
|
||||
def update_present_via_semver_portions(current_spec, active_spec, options)
|
||||
current_major = current_spec.version.segments.first
|
||||
active_major = active_spec.version.segments.first
|
||||
|
||||
update_present = false
|
||||
update_present = active_major > current_major if options["filter-major"]
|
||||
|
||||
if !update_present && (options["filter-minor"] || options["filter-patch"]) && current_major == active_major
|
||||
current_minor = get_version_semver_portion_value(current_spec, 1)
|
||||
active_minor = get_version_semver_portion_value(active_spec, 1)
|
||||
|
||||
update_present = active_minor > current_minor if options["filter-minor"]
|
||||
|
||||
if !update_present && options["filter-patch"] && current_minor == active_minor
|
||||
current_patch = get_version_semver_portion_value(current_spec, 2)
|
||||
active_patch = get_version_semver_portion_value(active_spec, 2)
|
||||
|
||||
update_present = active_patch > current_patch
|
||||
end
|
||||
end
|
||||
|
||||
update_present
|
||||
end
|
||||
|
||||
def get_version_semver_portion_value(spec, version_portion_index)
|
||||
version_section = spec.version.segments[version_portion_index, 1]
|
||||
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
|
||||
Reference in New Issue
Block a user