Skip to content

Commit

Permalink
WIP begin minor/patch option impl
Browse files Browse the repository at this point in the history
  • Loading branch information
dylnclrk committed Aug 4, 2024
1 parent 5c5ce37 commit e5aad27
Show file tree
Hide file tree
Showing 9 changed files with 90 additions and 15 deletions.
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

0 comments on commit e5aad27

Please sign in to comment.