Skip to content

Commit 05811f8

Browse files
committed
Add 'call for update' to RubyGems install command.
1 parent fffe1f9 commit 05811f8

File tree

7 files changed

+245
-0
lines changed

7 files changed

+245
-0
lines changed

Manifest.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -528,6 +528,7 @@ lib/rubygems/tsort/LICENSE.txt
528528
lib/rubygems/tsort/lib/tsort.rb
529529
lib/rubygems/uninstaller.rb
530530
lib/rubygems/unknown_command_spell_checker.rb
531+
lib/rubygems/update_suggestion.rb
531532
lib/rubygems/uri.rb
532533
lib/rubygems/uri_formatter.rb
533534
lib/rubygems/user_interaction.rb
@@ -737,6 +738,7 @@ test/rubygems/test_gem_stub_specification.rb
737738
test/rubygems/test_gem_text.rb
738739
test/rubygems/test_gem_uninstaller.rb
739740
test/rubygems/test_gem_unsatisfiable_dependency_error.rb
741+
test/rubygems/test_gem_update_suggestion.rb
740742
test/rubygems/test_gem_uri.rb
741743
test/rubygems/test_gem_uri_formatter.rb
742744
test/rubygems/test_gem_util.rb

lib/rubygems/commands/install_command.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
require_relative "../local_remote_options"
66
require_relative "../validator"
77
require_relative "../version_option"
8+
require_relative "../update_suggestion"
89

910
##
1011
# Gem installer command line tool
@@ -17,6 +18,7 @@ class Gem::Commands::InstallCommand < Gem::Command
1718
include Gem::VersionOption
1819
include Gem::LocalRemoteOptions
1920
include Gem::InstallUpdateOptions
21+
include Gem::UpdateSuggestion
2022

2123
def initialize
2224
defaults = Gem::DependencyInstaller::DEFAULT_OPTIONS.merge({
@@ -168,6 +170,8 @@ def execute
168170

169171
show_installed
170172

173+
say update_suggestion if eglible_for_update?
174+
171175
terminate_interaction exit_code
172176
end
173177

lib/rubygems/config_file.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,18 @@ def backtrace
371371
@backtrace || $DEBUG
372372
end
373373

374+
# Check config file is writable. Creates empty file if not present to ensure we can write to it.
375+
def config_file_writable?
376+
if File.exist?(config_file_name)
377+
File.writable?(config_file_name)
378+
else
379+
require "fileutils"
380+
FileUtils.mkdir_p File.dirname(config_file_name)
381+
File.open(config_file_name, "w") {}
382+
true
383+
end
384+
end
385+
374386
# The name of the configuration file.
375387
def config_file_name
376388
@config_file_name || Gem.config_file

lib/rubygems/update_suggestion.rb

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# frozen_string_literal: true
2+
3+
##
4+
# Mixin methods for Gem::Command to promote available RubyGems update
5+
6+
module Gem::UpdateSuggestion
7+
# list taken from https://github.com/watson/ci-info/blob/7a3c30d/index.js#L56-L66
8+
CI_ENV_VARS = [
9+
"CI", # Travis CI, CircleCI, Cirrus CI, Gitlab CI, Appveyor, CodeShip, dsari
10+
"CONTINUOUS_INTEGRATION", # Travis CI, Cirrus CI
11+
"BUILD_NUMBER", # Jenkins, TeamCity
12+
"CI_APP_ID", "CI_BUILD_ID", "CI_BUILD_NUMBER", # Applfow
13+
"RUN_ID" # TaskCluster, dsari
14+
].freeze
15+
16+
ONE_WEEK = 7 * 24 * 60 * 60
17+
18+
##
19+
# Message to promote available RubyGems update with related gem update command.
20+
21+
def update_suggestion
22+
<<-MESSAGE
23+
24+
A new release of RubyGems is available: #{Gem.rubygems_version}#{Gem.latest_rubygems_version}!
25+
Run `gem update --system #{Gem.latest_rubygems_version}` to update your installation.
26+
27+
MESSAGE
28+
end
29+
30+
##
31+
# Determines if current environment is eglible for update suggestion.
32+
33+
def eglible_for_update?
34+
# explicit opt-out
35+
return false if Gem.configuration[:prevent_update_suggestion]
36+
return false if ENV["RUBYGEMS_PREVENT_UPDATE_SUGGESTION"]
37+
38+
# focus only on human usage of final RubyGems releases
39+
return false unless Gem.ui.tty?
40+
return false if Gem.rubygems_version.prerelease?
41+
return false if Gem.disable_system_update_message
42+
return false if ci?
43+
44+
# check makes sense only when we can store of last try
45+
# otherwise we will not be able to prevent annoying update message
46+
# on each command call
47+
return unless Gem.configuration.config_file_writable?
48+
49+
# load time of last check, ensure the difference is enough to repeat the suggestion
50+
check_time = Time.now.to_i
51+
last_update_check = Gem.configuration[:last_update_check] || 0
52+
return false if (check_time - last_update_check) < ONE_WEEK
53+
54+
# compare current and latest version, this is the part where
55+
# latest rubygems spec is fetched from remote
56+
(Gem.rubygems_version < Gem.latest_rubygems_version).tap do |eglible|
57+
if eglible
58+
# store the time of last successful check into config file
59+
Gem.configuration[:last_update_check] = check_time
60+
Gem.configuration.write
61+
end
62+
end
63+
rescue # don't block install command on any problem
64+
false
65+
end
66+
67+
def ci?
68+
CI_ENV_VARS.any? {|var| ENV.include?(var) }
69+
end
70+
end

test/rubygems/helper.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ def setup
309309
ENV["XDG_DATA_HOME"] = nil
310310
ENV["SOURCE_DATE_EPOCH"] = nil
311311
ENV["BUNDLER_VERSION"] = nil
312+
ENV["RUBYGEMS_PREVENT_UPDATE_SUGGESTION"] = "true"
312313

313314
@current_dir = Dir.pwd
314315
@fetcher = nil

test/rubygems/test_gem_commands_install_command.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# frozen_string_literal: true
22
require_relative "helper"
3+
require_relative "test_gem_update_suggestion"
34
require "rubygems/commands/install_command"
45
require "rubygems/request_set"
56
require "rubygems/rdoc"
@@ -1550,4 +1551,22 @@ def test_explain_platform_ruby_ignore_dependencies
15501551
assert_equal " a-3", out.shift
15511552
assert_empty out
15521553
end
1554+
1555+
def test_suggest_update_if_enabled
1556+
TestUpdateSuggestion.with_eglible_environment(cmd: @cmd) do
1557+
spec_fetcher do |fetcher|
1558+
fetcher.gem "a", 2
1559+
end
1560+
1561+
@cmd.options[:args] = %w[a]
1562+
1563+
use_ui @ui do
1564+
assert_raise Gem::MockGemUi::SystemExitException, @ui.error do
1565+
@cmd.execute
1566+
end
1567+
end
1568+
1569+
assert_includes @ui.output, "A new release of RubyGems is available: 1.2.3 → 2.0.0!"
1570+
end
1571+
end
15531572
end
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# frozen_string_literal: true
2+
require_relative "helper"
3+
require "rubygems/command"
4+
require "rubygems/update_suggestion"
5+
6+
class TestUpdateSuggestion < Gem::TestCase
7+
def setup
8+
super
9+
10+
@cmd = Gem::Command.new "dummy", "dummy"
11+
@cmd.extend Gem::UpdateSuggestion
12+
end
13+
14+
def with_eglible_environment(**params)
15+
self.class.with_eglible_environment(**params) do
16+
yield
17+
end
18+
end
19+
20+
def self.with_eglible_environment(
21+
tty: true,
22+
rubygems_version: Gem::Version.new("1.2.3"),
23+
latest_rubygems_version: Gem::Version.new("2.0.0"),
24+
ci: false,
25+
cmd:
26+
)
27+
original_config, Gem.configuration[:prevent_update_suggestion] = Gem.configuration[:prevent_update_suggestion], nil
28+
original_env, ENV["RUBYGEMS_PREVENT_UPDATE_SUGGESTION"] = ENV["RUBYGEMS_PREVENT_UPDATE_SUGGESTION"], nil
29+
original_disable, Gem.disable_system_update_message = Gem.disable_system_update_message, nil
30+
Gem.configuration[:last_update_check] = nil
31+
32+
Gem.ui.stub :tty?, tty do
33+
Gem.stub :rubygems_version, rubygems_version do
34+
Gem.stub :latest_rubygems_version, latest_rubygems_version do
35+
cmd.stub :ci?, ci do
36+
yield
37+
end
38+
end
39+
end
40+
end
41+
ensure
42+
Gem.configuration[:prevent_update_suggestion] = original_config
43+
ENV["RUBYGEMS_PREVENT_UPDATE_SUGGESTION"] = original_env
44+
Gem.disable_system_update_message = original_disable
45+
end
46+
47+
def test_update_suggestion
48+
Gem.stub :rubygems_version, Gem::Version.new("1.2.3") do
49+
Gem.stub :latest_rubygems_version, Gem::Version.new("2.0.0") do
50+
assert_equal @cmd.update_suggestion, <<~SUGGESTION
51+
52+
A new release of RubyGems is available: 1.2.3 → 2.0.0!
53+
Run `gem update --system 2.0.0` to update your installation.
54+
55+
SUGGESTION
56+
end
57+
end
58+
end
59+
60+
def test_eglible_for_update
61+
with_eglible_environment(cmd: @cmd) do
62+
Time.stub :now, 123456789 do
63+
assert @cmd.eglible_for_update?
64+
assert_equal Gem.configuration[:last_update_check], 123456789
65+
66+
# test last check is written to config file
67+
assert File.read(Gem.configuration.config_file_name).match("last_update_check: 123456789")
68+
end
69+
end
70+
end
71+
72+
def test_eglible_for_update_prevent_config
73+
with_eglible_environment(cmd: @cmd) do
74+
begin
75+
original_config, Gem.configuration[:prevent_update_suggestion] = Gem.configuration[:prevent_update_suggestion], true
76+
refute @cmd.eglible_for_update?
77+
ensure
78+
Gem.configuration[:prevent_update_suggestion] = original_config
79+
end
80+
end
81+
end
82+
83+
def test_eglible_for_update_prevent_env
84+
with_eglible_environment(cmd: @cmd) do
85+
begin
86+
original_env, ENV["RUBYGEMS_PREVENT_UPDATE_SUGGESTION"] = ENV["RUBYGEMS_PREVENT_UPDATE_SUGGESTION"], "yes"
87+
refute @cmd.eglible_for_update?
88+
ensure
89+
ENV["RUBYGEMS_PREVENT_UPDATE_SUGGESTION"] = original_env
90+
end
91+
end
92+
end
93+
94+
def test_eglible_for_update_non_tty
95+
with_eglible_environment(tty: false, cmd: @cmd) do
96+
refute @cmd.eglible_for_update?
97+
end
98+
end
99+
100+
def test_eglible_for_update_for_prerelease
101+
with_eglible_environment(rubygems_version: Gem::Version.new("1.0.0-rc1"), cmd: @cmd) do
102+
refute @cmd.eglible_for_update?
103+
end
104+
end
105+
106+
def test_eglible_for_update_disabled_update
107+
with_eglible_environment(cmd: @cmd) do
108+
begin
109+
original_disable, Gem.disable_system_update_message = Gem.disable_system_update_message, "disabled"
110+
refute @cmd.eglible_for_update?
111+
ensure
112+
Gem.disable_system_update_message = original_disable
113+
end
114+
end
115+
end
116+
117+
def test_eglible_for_update_on_ci
118+
with_eglible_environment(ci: true, cmd: @cmd) do
119+
refute @cmd.eglible_for_update?
120+
end
121+
end
122+
123+
def test_eglible_for_update_unwrittable_config
124+
with_eglible_environment(ci: true, cmd: @cmd) do
125+
Gem.configuration.stub :config_file_writable?, false do
126+
refute @cmd.eglible_for_update?
127+
end
128+
end
129+
end
130+
131+
def test_eglible_for_update_notification_delay
132+
with_eglible_environment(cmd: @cmd) do
133+
Gem.configuration[:last_update_check] = Time.now.to_i
134+
refute @cmd.eglible_for_update?
135+
end
136+
end
137+
end

0 commit comments

Comments
 (0)