Skip to content
Draft
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
6 changes: 6 additions & 0 deletions bundler/lib/bundler/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,12 @@ def help(cli = nil)
end

def self.handle_no_command_error(command, has_namespace = $thor_runner)
if command == "upgrade"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put this check above the check for plugins below, so that would prevent having a plugin named "upgrade". That seems desirable to me, but what do you think?

Copy link
Collaborator

@Edouard-chin Edouard-chin Mar 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd have to move this down below (L199), otherwise this is going to break plugins. Bundler has a plugin system where the CLI can be extended to offer more commands. I don't know if there is already a plugin in the wild that provides bundle upgrade when installed, but if it does, this is going to break it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I called this out in a comment: #9407 (comment)

You mention this in your review's main message:

What Bundler does differently from pretty much any package managers though is the command to update itself with gem update --system or bundle lock --bundler whereas every other ones uses <tool> update/upgrade (depending on what verb they chose to bump dependencies).

It would maybe be best to reserve the word upgrade for this use case in the future.

If we do want to reserve upgrade for future use (I agree we shuold), then this is the correct place to have this check, so plugins can't take over bundle upgrade.

This is also why I didn't use an alias. Since this PR implements this as an error message, it would not be a breaking change to swap out the error message and put in real behaviour.

By comparison if upgrade was an alias or allowed to be a plugin name, switching it be a real bundler feature would be a breaking change.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I called this out in a comment: #9407 (comment)

Oops sorry I missed that.

Since this PR implements this as an error message, it would not be a breaking change to swap out the error message and put in real behaviour.

I agree, but it is a breaking change now. There is probably a plugin in the wild that offer a upgrade command and we should warn that this will be reserved in the future, but continue to execute the plugin command for now.

command = Bundler.ui.add_color("bundle update <gem_name>", :bold, :cyan)
Bundler.ui.info("Please use #{command} to update gems in your bundle.")
exit(1)
end

if Bundler.settings[:plugins] && Bundler::Plugin.command?(command)
return Bundler::Plugin.exec_command(command, ARGV[1..-1])
end
Expand Down
6 changes: 6 additions & 0 deletions bundler/spec/other/cli_dispatch_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,10 @@
bundle "in", raise_on_error: false
expect(err).to eq("Ambiguous command in matches [info, init, install]")
end

it "prints a helpful message for 'upgrade'" do
bundle "upgrade", raise_on_error: false
expect(out).to eq("Please use bundle update <gem_name> to update gems in your bundle.")
expect(exitstatus).to eq(1)
end
end
38 changes: 38 additions & 0 deletions lib/rubygems/exceptions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,44 @@ def self.attach_correctable
DidYouMean.correct_error(Gem::UnknownCommandError, Gem::UnknownCommandSpellChecker)
end
end

def detailed_message(highlight: true, **)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried implementing this by modifying the Gem::UnknownCommandSpellChecker, but that didn't work as well.

  1. I can't just modify the dictionary passed to DidYouMean::SpellChecker.new. "Upgrade" is too different from "update, so it doesn't trigger DYM's correction.
  2. I could patch the corrections method there, but then the message isn't as nice. If I'm going to add a special check for "upgrade", I'd rather get a nicer message out of it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I could do it there, but improve the formatting of all such messages with a DidYouMean::Formatter?

msg = super(highlight: highlight)

case unknown_command
when "upgrade"
command = "bundle update <gem_name>"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
command = "bundle update <gem_name>"
command = "gem update <gem_name>"


bold = "\e[1m"
cyan = "\e[36m"
reset = "\e[0m"

command = if highlight && can_display_colors?
"#{bold}#{cyan}#{command}#{reset}"
else
"`#{command}`"
end

msg + "\nPlease use #{command} to update gems in your bundle."
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Please use #{command} to update gems is a bit more clear as there is no concept of bundle when using the gem command


else
msg
end
end

private

def can_display_colors?
are_colors_supported? && !are_colors_disabled?
end

def are_colors_supported?
stdout.tty? && ENV["TERM"] != "dumb"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
stdout.tty? && ENV["TERM"] != "dumb"
$stdout.tty? && ENV["TERM"] != "dumb"

end

def are_colors_disabled?
!ENV["NO_COLOR"].nil? && !ENV["NO_COLOR"].empty?
end
end

class Gem::DependencyError < Gem::Exception; end
Expand Down
18 changes: 18 additions & 0 deletions test/rubygems/test_gem_command_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,24 @@
assert_equal message, actual_message
end

def test_upgrade_points_users_to_update
e = assert_raise Gem::UnknownCommandError do
@command_manager.find_command "upgrade"
end

assert_equal <<~MSG.chomp, e.detailed_message(highlight: false)
Unknown command upgrade (Gem::UnknownCommandError)
Please use `bundle update <gem_name>` to update gems in your bundle.
MSG

def e.can_display_colors? = true
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we don't need this in test ? I find difficult to read the assertion otherwise.


assert_equal <<~MSG.chomp, e.detailed_message(highlight: true)

Check failure on line 106 in test/rubygems/test_gem_command_manager.rb

View workflow job for this annotation

GitHub Actions / Rubygems on Ubuntu (jruby)

Failure

<"\e[1mUnknown command upgrade (\e[1;4mGem::UnknownCommandError\e[m\e[1m)\e[m\n" + "Please use \e[1m\e[36mbundle update <gem_name>\e[0m to update gems in your bundle.">("UTF-8") expected but was <"\e[1mUnknown command upgrade\n" + "Please use \e[1m\e[36mbundle update <gem_name>\e[0m to update gems in your bundle.">("US-ASCII"). diff: - Unknown command upgrade (Gem::UnknownCommandError�[m)�[m + Unknown command upgrade Please use bundle update <gem_name> to update gems in your bundle.
\e[1mUnknown command upgrade (\e[1;4mGem::UnknownCommandError\e[m\e[1m)\e[m
Please use \e[1m\e[36mbundle update <gem_name>\e[0m to update gems in your bundle.
MSG
end

def test_run_interrupt
old_load_path = $:.dup
$: << File.expand_path("test/rubygems", PROJECT_DIR)
Expand Down
Loading