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
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,22 @@ https://github.com/rails/rails/compare/5a8d894...77dfa65

This feature currently works for GitHub, GitLab, and Bitbucket repos.

### Limit updates by version type

In order to reduce the risks of an update, `bundle update-interactive` can be limited to either patch or minor level updates with the `--patch` and `--minor` options.

```sh
# Limit updates to patch version changes (exclude minor and major updates)
bundle update-interactive --patch

# Limit updates to patch and minor version changes (exclude major updates)
bundle update-interactive --minor
```

For example, consider a lock file that currently has rack 2.0.3. The latest version of rack is 3.1.7, but updating by a major version is risky. Instead, we'd like to update to the latest 2.0.x version first, which is 2.0.9. We can accomplish this with the `--patch` option.

Once this update is successful, we might wish to update rack from 2.0.9 to the last 2.x version, 2.2.9, before going to version 3. We can accomplish this with the `--minor` option.

### Limit impact by Gemfile groups

The effects of `bundle update-interactive` can be limited to one or more Gemfile groups using the `--exclusively` option:
Expand Down
12 changes: 9 additions & 3 deletions lib/bundle_update_interactive/bundler_commands.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,19 @@
module BundleUpdateInteractive
module BundlerCommands
class << self
def update_gems_conservatively(*gems)
system "#{bundle_bin.shellescape} update --conservative #{gems.flatten.map(&:shellescape).join(' ')}"
def update_gems_conservatively(*gems, level: nil)
command = ["#{bundle_bin.shellescape} update"]
command << "--minor" if level == :minor
command << "--patch" if level == :patch
command.push("--conservative #{gems.flatten.map(&:shellescape).join(' ')}")
system command.join(" ")
end

def read_updated_lockfile(*gems)
def read_updated_lockfile(*gems, level: nil)
command = ["#{bundle_bin.shellescape} lock --print"]
command << "--conservative" if gems.any?
command << "--minor" if level == :minor
command << "--patch" if level == :patch
command << "--update"
command.push(*gems.flatten.map(&:shellescape))

Expand Down
2 changes: 1 addition & 1 deletion lib/bundle_update_interactive/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def legend

def generate_report(options)
whisper "Resolving latest gem versions..."
report = Report.generate(groups: options.exclusively)
report = Report.generate(groups: options.exclusively, level: options.level)
updateable_gems = report.updateable_gems
return report if updateable_gems.empty?

Expand Down
14 changes: 12 additions & 2 deletions lib/bundle_update_interactive/cli/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def pastel
end

def build_parser(options) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
OptionParser.new do |parser|
OptionParser.new do |parser| # rubocop:disable Metrics/BlockLength
parser.summary_indent = " "
parser.summary_width = 24
parser.on(
Expand All @@ -70,6 +70,16 @@ def build_parser(options) # rubocop:disable Metrics/AbcSize, Metrics/MethodLengt
parser.on("-D", "Shorthand for --exclusively=development,test") do
options.exclusively = %i[development test]
end
parser.on("-m", "--minor", "Only update to the latest minor version") do
raise Error, "Please specify EITHER --patch or --minor option, not both" unless options.level.nil?

options.level = :minor
end
parser.on("-p", "--patch", "Only update to the latest patch version") do
raise Error, "Please specify EITHER --patch or --minor option, not both" unless options.level.nil?

options.level = :patch
end
parser.on("-v", "--version", "Display version") do
require "bundler"
puts "bundle_update_interactive/#{VERSION} bundler/#{Bundler::VERSION} #{RUBY_DESCRIPTION}"
Expand All @@ -83,7 +93,7 @@ def build_parser(options) # rubocop:disable Metrics/AbcSize, Metrics/MethodLengt
end
end

attr_accessor :exclusively
attr_accessor :exclusively, :level

def initialize
@exclusively = []
Expand Down
20 changes: 15 additions & 5 deletions lib/bundle_update_interactive/report.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,28 @@
module BundleUpdateInteractive
class Report
class << self
def generate(groups: [])
def generate(groups: [], level: nil)
gemfile = Gemfile.parse
current_lockfile = Lockfile.parse
gems = groups.any? ? current_lockfile.gems_exclusively_installed_by(gemfile: gemfile, groups: groups) : nil

updated_lockfile = gems&.none? ? nil : Lockfile.parse(BundlerCommands.read_updated_lockfile(*Array(gems)))
new(gemfile: gemfile, current_lockfile: current_lockfile, updated_lockfile: updated_lockfile)
updated_lockfile = if gems&.none?
nil
else
Lockfile.parse(
BundlerCommands.read_updated_lockfile(
*Array(gems),
level: level
)
)
end
new(gemfile: gemfile, current_lockfile: current_lockfile, updated_lockfile: updated_lockfile, level: level)
end
end

attr_reader :outdated_gems

def initialize(gemfile:, current_lockfile:, updated_lockfile:)
def initialize(gemfile:, current_lockfile:, updated_lockfile:, level:)
@current_lockfile = current_lockfile
@outdated_gems = current_lockfile.entries.each_with_object({}) do |current_lockfile_entry, hash|
name = current_lockfile_entry.name
Expand All @@ -29,6 +38,7 @@ def initialize(gemfile:, current_lockfile:, updated_lockfile:)

hash[name] = build_outdated_gem(current_lockfile_entry, updated_lockfile_entry, gemfile[name]&.groups)
end.freeze
@level = level
end

def [](gem_name)
Expand Down Expand Up @@ -61,7 +71,7 @@ def scan_for_vulnerabilities!

def bundle_update!(*gem_names)
expanded_names = expand_gems_with_exact_dependencies(*gem_names)
BundlerCommands.update_gems_conservatively(*expanded_names)
BundlerCommands.update_gems_conservatively(*expanded_names, level: @level)
end

private
Expand Down
14 changes: 14 additions & 0 deletions test/bundle_update_interactive/bundler_commands_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@ def test_read_updated_lockfile_raises_if_bundler_fails_to_run
assert_match(/bundle lock command failed/i, error.message)
end

def test_read_updated_lockfile_runs_bundle_lock_with_patch_option
expect_backticks("/exe/bundle lock --print --patch --update", captures: "bundler output")
result = BundlerCommands.read_updated_lockfile(level: :patch)

assert_equal "bundler output", result
end

def test_read_updated_lockfile_runs_bundle_lock_with_minor_option
expect_backticks("/exe/bundle lock --print --minor --update", captures: "bundler output")
result = BundlerCommands.read_updated_lockfile(level: :minor)

assert_equal "bundler output", result
end

private

def expect_backticks(command, captures: "", success: true)
Expand Down
18 changes: 18 additions & 0 deletions test/bundle_update_interactive/cli/options_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,24 @@ def test_prints_version_and_exits_when_given_dash_dash_version
assert_equal(0, status)
end

def test_allows_patch_option_to_be_specified
options = CLI::Options.parse(%w[--patch])
assert_equal :patch, options.level
end

def test_allows_minor_option_to_be_specified
options = CLI::Options.parse(%w[--patch])
assert_equal :patch, options.level
end

def test_raises_if_both_minor_and_patch_options_are_specified
error = assert_raises(BundleUpdateInteractive::Error) do
CLI::Options.parse(%w[--patch --minor])
end

assert_match(/specify EITHER --patch or --minor option/i, error.message)
end

def test_exclusively_is_empty_array_by_default
options = CLI::Options.parse([])
assert_empty options.exclusively
Expand Down
2 changes: 1 addition & 1 deletion test/bundle_update_interactive/cli_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def test_shows_interactive_list_of_gems_and_updates_the_selected_ones
VCR.use_cassette("changelog_requests") do
updated_lockfile = File.read("Gemfile.lock.updated")
BundlerCommands.expects(:read_updated_lockfile).returns(updated_lockfile)
BundlerCommands.expects(:update_gems_conservatively).with("addressable", "bigdecimal", "builder")
BundlerCommands.expects(:update_gems_conservatively).with("addressable", "bigdecimal", "builder", level: nil)
mock_vulnerable_gems([])

stdin_data = " j j \n" # SPACE,DOWN,SPACE,DOWN,SPACE,ENTER selects first three gems to update
Expand Down
7 changes: 4 additions & 3 deletions test/bundle_update_interactive/report_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def test_generate_creates_a_report_of_updatable_gems_that_can_be_rendered_as_a_t
VCR.use_cassette("changelog_requests") do
Dir.chdir(File.expand_path("../fixtures", __dir__)) do
updated_lockfile = File.read("Gemfile.lock.updated")
BundlerCommands.expects(:read_updated_lockfile).with.returns(updated_lockfile)
BundlerCommands.expects(:read_updated_lockfile).with(level: nil).returns(updated_lockfile)
mock_vulnerable_gems("actionpack", "rexml", "devise")

report = Report.generate
Expand All @@ -24,7 +24,7 @@ def test_generate_creates_a_report_of_updatable_gems_that_can_be_rendered_as_a_t
end

def test_generate_creates_a_report_of_updatable_gems_for_development_and_test_groups
VCR.use_cassette("changelog_requests") do
VCR.use_cassette("changelog_requests") do # rubocop:disable Metrics/BlockLength
Dir.chdir(File.expand_path("../fixtures", __dir__)) do
updated_lockfile = File.read("Gemfile.lock.development-test-updated")
BundlerCommands.expects(:read_updated_lockfile).with(
Expand All @@ -42,7 +42,8 @@ def test_generate_creates_a_report_of_updatable_gems_for_development_and_test_gr
web-console
websocket
xpath
]
],
level: nil
).returns(updated_lockfile)
mock_vulnerable_gems("actionpack", "rexml", "devise")

Expand Down