Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Manifest.txt
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,8 @@ lib/rubygems/compact_index_client/http_fetcher.rb
lib/rubygems/compact_index_client/parser.rb
lib/rubygems/compact_index_client/updater.rb
lib/rubygems/config_file.rb
lib/rubygems/cooldown.rb
lib/rubygems/cooldown_option.rb
lib/rubygems/core_ext/kernel_gem.rb
lib/rubygems/core_ext/kernel_require.rb
lib/rubygems/core_ext/kernel_warn.rb
Expand Down
7 changes: 7 additions & 0 deletions lib/rubygems/commands/install_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ def execute
end

@installed_specs = []
@cooldown_skipped = []

ENV.delete "GEM_PATH" if options[:install_dir].nil?

Expand All @@ -163,6 +164,8 @@ def execute

show_installed

Gem::Cooldown.output_skipped_summary @cooldown_skipped

say update_suggestion if eligible_for_update?

terminate_interaction exit_code
Expand All @@ -184,6 +187,8 @@ def install_from_gemdeps # :nodoc:

@installed_specs = specs

Gem::Cooldown.output_skipped_summary rs.resolver&.cooldown_skipped

terminate_interaction
end

Expand All @@ -207,6 +212,8 @@ def install_gem(name, version) # :nodoc:
@installed_specs.concat request_set.install options
end

(@cooldown_skipped ||= []).concat dinst.cooldown_skipped

show_install_errors dinst.errors
end

Expand Down
79 changes: 77 additions & 2 deletions lib/rubygems/commands/outdated_command.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
# frozen_string_literal: true

require_relative "../command"
require_relative "../cooldown"
require_relative "../cooldown_option"
require_relative "../local_remote_options"
require_relative "../spec_fetcher"
require_relative "../version_option"

class Gem::Commands::OutdatedCommand < Gem::Command
include Gem::LocalRemoteOptions
include Gem::VersionOption
include Gem::CooldownOption

def initialize
super "outdated", "Display all gems that need updates"

add_local_remote_options
add_platform_option
add_cooldown_option
end

def description # :nodoc:
Expand All @@ -26,8 +30,79 @@ def description # :nodoc:
end

def execute
Gem::Specification.outdated_and_latest_version.each do |spec, remote_version|
say "#{spec.name} (#{spec.version} < #{remote_version})"
@cooldown = Gem::Cooldown.from_options options

unless @cooldown.active?
Gem::Specification.outdated_and_latest_version.each do |spec, remote_version|
say "#{spec.name} (#{spec.version} < #{remote_version})"
end

return
end

execute_with_cooldown
end

private

##
# Like Gem::Specification.outdated_and_latest_version, but the newest
# version outside the cooldown period becomes the update candidate, and
# newer versions still within the period are annotated.

def execute_with_cooldown
fetcher = Gem::SpecFetcher.fetcher

Gem::Specification.latest_specs(true).each do |local_spec|
dependency = Gem::Dependency.new local_spec.name, ">= #{local_spec.version}"

# The :latest index carries only the newest version of each gem,
# which leaves nothing to fall back to when the cooldown excludes
# it, so search the full index instead.
remotes, = fetcher.search_for_dependency dependency,
type: dependency.prerelease? ? :complete : :released

selectable, embargoed = partition_by_cooldown remotes

candidate = selectable.max
candidate = nil unless candidate && local_spec.version < candidate

pending = embargoed.max
pending = nil unless pending && local_spec.version < pending &&
(candidate.nil? || candidate < pending)

next unless candidate || pending

pending = "#{pending} (cooldown #{@cooldown.days}d)" if pending
say "#{local_spec.name} (#{local_spec.version} < #{[candidate, pending].compact.join(", ")})"
end
end

##
# Splits [NameTuple, Gem::Source] pairs into versions outside and within
# the cooldown period. Tuples with an unknown publish time count as
# outside the period, so the cooldown fails open.

def partition_by_cooldown(spec_tuples)
selectable = []
embargoed = []

with_times = spec_tuples.map do |tup, source|
[tup, source, source.created_at(tup.name, tup.version, tup.platform)]
end

if !with_times.empty? && with_times.none? {|_, _, created_at| created_at }
Gem::Cooldown.warn_missing_created_at with_times.first[1]
end

with_times.each do |tup, _, created_at|
if @cooldown.skip?(created_at)
embargoed << tup.version
else
selectable << tup.version
end
end

[selectable, embargoed]
end
end
86 changes: 84 additions & 2 deletions lib/rubygems/commands/update_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require_relative "../command"
require_relative "../command_manager"
require_relative "../cooldown"
require_relative "../dependency_installer"
require_relative "../install_update_options"
require_relative "../local_remote_options"
Expand Down Expand Up @@ -95,8 +96,12 @@ def check_update_arguments # :nodoc:
end

def execute
@cooldown = Gem::Cooldown.from_options options
@cooldown_skipped = []

if options[:system]
update_rubygems
output_cooldown_skipped_summary
return
end

Expand Down Expand Up @@ -132,6 +137,8 @@ def execute
end
say "Gems already up-to-date: #{up_to_date_names.join(" ")}" unless up_to_date_names.empty?
say "Gems not currently installed: #{not_installed_names.join(" ")}" unless not_installed_names.empty?

output_cooldown_skipped_summary
end

def fetch_remote_gems(spec) # :nodoc:
Expand All @@ -140,7 +147,12 @@ def fetch_remote_gems(spec) # :nodoc:

fetcher = Gem::SpecFetcher.fetcher

spec_tuples, errors = fetcher.search_for_dependency dependency
# The default indexes carry only the newest version of each gem, which
# leaves nothing to fall back to when the cooldown excludes it, so
# search the full index instead.
type = dependency.prerelease? ? :complete : :released if @cooldown&.active?

spec_tuples, errors = fetcher.search_for_dependency dependency, type: type

error = errors.find {|e| e.respond_to? :exception }

Expand All @@ -167,12 +179,78 @@ def highest_installed_gems # :nodoc:
def highest_remote_name_tuple(spec) # :nodoc:
spec_tuples = fetch_remote_gems spec

highest_remote_gem = spec_tuples.max
highest_remote_gem = filter_cooldown_tuples(spec_tuples).max
return unless highest_remote_gem

highest_remote_gem.first
end

##
# Rejects [NameTuple, Gem::Source] pairs published within the cooldown
# period. Tuples with an unknown publish time are kept, so the cooldown
# fails open.

def filter_cooldown_tuples(spec_tuples) # :nodoc:
return spec_tuples unless @cooldown&.active?

with_times = spec_tuples.map do |tup, source|
[tup, source, source.created_at(tup.name, tup.version, tup.platform)]
end

if !with_times.empty? && with_times.none? {|_, _, created_at| created_at }
Gem::Cooldown.warn_missing_created_at with_times.first[1]
end

with_times.reject do |tup, _, created_at|
next false unless @cooldown.skip?(created_at)

(@cooldown_skipped_tuples ||= {})[[tup.name, tup.version]] ||= created_at
true
end.map {|tup, source, _| [tup, source] }
end

##
# Summary entries for tuples the cooldown kept out of the update, kept
# only when newer than the version the update actually settled on (the
# updated version, or the one already installed when nothing moved).

def cooldown_skipped_tuple_entries # :nodoc:
skipped = @cooldown_skipped_tuples
return [] unless skipped

resolved = {}
@updated.each do |spec|
version = resolved[spec.name]
resolved[spec.name] = spec.version if version.nil? || spec.version > version
end

skipped.filter_map do |(name, version), created_at|
resolved_version = resolved[name] || resolved_fallback_version(name)
next unless resolved_version && version > resolved_version

{
name: name,
version: version,
resolved: resolved_version,
available_in_days: @cooldown.remaining_days(created_at),
}
end
end

def resolved_fallback_version(name) # :nodoc:
if name == "rubygems-update"
Gem::Version.new Gem::VERSION
else
Gem::Specification.find_all_by_name(name).map(&:version).max
end
end

def output_cooldown_skipped_summary # :nodoc:
entries = (@cooldown_skipped || []) + cooldown_skipped_tuple_entries

Gem::Cooldown.output_skipped_summary entries
end

def install_rubygems(spec) # :nodoc:
args = update_rubygems_arguments
version = spec.version
Expand Down Expand Up @@ -256,6 +334,10 @@ def update_gem(name, version = Gem::Requirement.default)
@installer.installed_gems.each do |spec|
@updated << spec
end

(@cooldown_skipped ||= []).concat @installer.cooldown_skipped

@installer.installed_gems
end

def update_gems(gems_to_update)
Expand Down
13 changes: 12 additions & 1 deletion lib/rubygems/config_file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#
# +:backtrace+:: See #backtrace
# +:bulk_threshold+:: See #bulk_threshold
# +:cooldown+:: See #cooldown
# +:verbose+:: See #verbose
# +:update_sources+:: See #update_sources
# +:concurrent_downloads+:: See #concurrent_downloads
Expand Down Expand Up @@ -55,6 +56,7 @@ class Gem::ConfigFile

DEFAULT_BACKTRACE = true
DEFAULT_BULK_THRESHOLD = 1000
DEFAULT_COOLDOWN = 0
DEFAULT_VERBOSITY = true
DEFAULT_UPDATE_SOURCES = true
DEFAULT_CONCURRENT_DOWNLOADS = 8
Expand Down Expand Up @@ -116,6 +118,13 @@ class Gem::ConfigFile

attr_accessor :bulk_threshold

##
# Number of days a newly published gem version must wait before it is
# considered for installation or update (the cooldown period). 0
# disables the cooldown.

attr_accessor :cooldown

##
# Verbose level of output:
# * false -- No output
Expand Down Expand Up @@ -211,6 +220,7 @@ def initialize(args)

@backtrace = DEFAULT_BACKTRACE
@bulk_threshold = DEFAULT_BULK_THRESHOLD
@cooldown = DEFAULT_COOLDOWN
@verbose = DEFAULT_VERBOSITY
@update_sources = DEFAULT_UPDATE_SOURCES
@concurrent_downloads = DEFAULT_CONCURRENT_DOWNLOADS
Expand Down Expand Up @@ -239,7 +249,7 @@ def initialize(args)

@hash.transform_keys! do |k|
# gemhome and gempath are not working with symbol keys
if %w[backtrace bulk_threshold verbose update_sources cert_expiration_length_days
if %w[backtrace bulk_threshold cooldown verbose update_sources cert_expiration_length_days
concurrent_downloads install_extension_in_lib ipv4_fallback_enabled
global_gem_cache use_psych sources
disable_default_gem_server ssl_verify_mode ssl_ca_cert ssl_client_cert].include?(k)
Expand All @@ -252,6 +262,7 @@ def initialize(args)
# HACK: these override command-line args, which is bad
@backtrace = @hash[:backtrace] if @hash.key? :backtrace
@bulk_threshold = @hash[:bulk_threshold] if @hash.key? :bulk_threshold
@cooldown = @hash[:cooldown] if @hash.key? :cooldown
@verbose = @hash[:verbose] if @hash.key? :verbose
@update_sources = @hash[:update_sources] if @hash.key? :update_sources
@concurrent_downloads = @hash[:concurrent_downloads] if @hash.key? :concurrent_downloads
Expand Down
Loading