bundle_update_interactive 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e71278ee6c0ffbfb39537712a8d97e956c57062dad9723d358d97474cf63b69c
4
- data.tar.gz: 13491cad6f990d97fb3a2c918913fb4bc5eb5048829a18dd8c5400f6e5f9fadf
3
+ metadata.gz: 58fa0e12f3322d018b1fcaa788dd3b8a2c3354042fe1e6ab21313d0bc0d7518b
4
+ data.tar.gz: a43866c8bad9bc0256b369cbf7e29db56c2b06293f2ae3a4a3e560ff75506e16
5
5
  SHA512:
6
- metadata.gz: 9bf5c295484ae3aa19d12d5891afd7883873204d0a9314babcf111b832cab60b89be13afcb6f33e8a8d2cbc7b702df9b0c800ca0639a2c8148e56adb6964cde4
7
- data.tar.gz: 0caebaf4d1bbc2a2d3cd6a5820ed031491a41a43286d7ba6f3e7bf419c28c5627ac58b8bed0407658d46efdc01d105f75857c66784b61b98eb581c00fe0ec575
6
+ metadata.gz: 5325810c277c4a3f58d3308b9899c9097a2c723996e79030ccda96f79408c83734b2bcf9ae5920a1ee74f32837ebae91f4e946aa9ff08225db73bf6d06d69e91
7
+ data.tar.gz: d241d58cb408a0b6294a8b56e010b8ffa841bdbb5ccf28c794fb2db4ebcc562f7ec486742ec36720ed9522c9cf2ce1d27d8a68619fefbee59d28810b3044e7fb
data/README.md CHANGED
@@ -63,6 +63,12 @@ Some gems, notably `rails`, are composed of smaller gems like `actionpack`, `act
63
63
 
64
64
  Therefore, if any Rails component has a security vulnerability, `bundle update-interactive` will automatically roll up that information into a single `rails` line item, so you can select it and upgrade all of its components in one shot.
65
65
 
66
+ ### Held back gems
67
+
68
+ When a newer version of a gem is available, but updating is not allowed due to a Gemfile requirement, `update-interactive` will report that the gem has been held back.
69
+
70
+ <img src="images/held-back.png" alt="Screenshot of rails and selenium-webdriver gems held back due to Gemfile requirements" width="717" />
71
+
66
72
  ### Changelogs
67
73
 
68
74
  `bundle update-interactive` will do its best to find an appropriate changelog for each gem.
@@ -19,6 +19,14 @@ module BundleUpdateInteractive
19
19
  `#{command.join(" ")}`.tap { raise "bundle lock command failed" unless Process.last_status.success? }
20
20
  end
21
21
 
22
+ def parse_outdated(*gems)
23
+ command = ["#{bundle_bin.shellescape} outdated --parseable", *gems.flatten.map(&:shellescape)]
24
+ output = `#{command.join(" ")}`
25
+ raise "bundle outdated command failed" if output.empty? && !Process.last_status.success?
26
+
27
+ output.scan(/^(\S+) \(newest (\S+),/).to_h
28
+ end
29
+
22
30
  private
23
31
 
24
32
  def bundle_bin
@@ -46,7 +46,7 @@ class BundleUpdateInteractive::CLI
46
46
  end
47
47
 
48
48
  def self.prompt_for_gems_to_update(outdated_gems, prompt: nil)
49
- table = Table.new(outdated_gems)
49
+ table = Table.updatable(outdated_gems)
50
50
  title = "#{outdated_gems.length} gems can be updated."
51
51
  opener = lambda do |gem|
52
52
  url = outdated_gems[gem].changelog_uri
@@ -16,17 +16,6 @@ class BundleUpdateInteractive::CLI
16
16
  @pastel = BundleUpdateInteractive.pastel
17
17
  end
18
18
 
19
- def to_a
20
- [
21
- formatted_gem_name,
22
- formatted_current_version,
23
- "→",
24
- formatted_updated_version,
25
- formatted_gemfile_groups,
26
- formatted_changelog_uri
27
- ]
28
- end
29
-
30
19
  def formatted_gem_name
31
20
  vulnerable? ? pastel.white.on_red(name) : apply_semver_highlight(name)
32
21
  end
@@ -46,6 +35,10 @@ class BundleUpdateInteractive::CLI
46
35
  gemfile_groups&.map(&:inspect)&.join(", ")
47
36
  end
48
37
 
38
+ def formatted_gemfile_requirement
39
+ gemfile_requirement.to_s == ">= 0" ? "" : gemfile_requirement.to_s
40
+ end
41
+
49
42
  def formatted_changelog_uri
50
43
  pastel.blue(changelog_uri)
51
44
  end
@@ -4,12 +4,44 @@ require "pastel"
4
4
 
5
5
  class BundleUpdateInteractive::CLI
6
6
  class Table
7
- HEADERS = ["name", "from", nil, "to", "group", "url"].freeze
7
+ class << self
8
+ def withheld(gems)
9
+ columns = [
10
+ ["name", :formatted_gem_name],
11
+ ["requirement", :formatted_gemfile_requirement],
12
+ ["current", :formatted_current_version],
13
+ ["latest", :formatted_updated_version],
14
+ ["group", :formatted_gemfile_groups],
15
+ ["url", :formatted_changelog_uri]
16
+ ]
17
+ new(gems, columns)
18
+ end
8
19
 
9
- def initialize(outdated_gems)
20
+ def updatable(gems)
21
+ columns = [
22
+ ["name", :formatted_gem_name],
23
+ ["from", :formatted_current_version],
24
+ [nil, "→"],
25
+ ["to", :formatted_updated_version],
26
+ ["group", :formatted_gemfile_groups],
27
+ ["url", :formatted_changelog_uri]
28
+ ]
29
+ new(gems, columns)
30
+ end
31
+ end
32
+
33
+ def initialize(gems, columns)
10
34
  @pastel = BundleUpdateInteractive.pastel
11
- @headers = HEADERS.map { |h| pastel.dim.underline(h) }
12
- @rows = outdated_gems.transform_values { |gem| Row.new(gem).to_a.map(&:to_s) }
35
+ @headers = columns.map { |header, _| pastel.dim.underline(header) }
36
+ @rows = gems.transform_values do |gem|
37
+ row = Row.new(gem)
38
+ columns.map do |_, col|
39
+ case col
40
+ when Symbol then row.public_send(col).to_s
41
+ when String then col
42
+ end
43
+ end
44
+ end
13
45
  @column_widths = calculate_column_widths
14
46
  end
15
47
 
@@ -11,19 +11,16 @@ module BundleUpdateInteractive
11
11
 
12
12
  def run(argv: ARGV) # rubocop:disable Metrics/AbcSize
13
13
  options = Options.parse(argv)
14
-
15
14
  report = generate_report(options)
16
- puts("No gems to update.").then { return } if report.updateable_gems.empty?
17
15
 
18
- puts
19
- puts legend
20
- puts
21
- selected_gems = MultiSelect.prompt_for_gems_to_update(report.updateable_gems)
16
+ puts_legend_and_withheld_gems(report) unless report.empty?
17
+ puts("No gems to update.").then { return } if report.updatable_gems.empty?
18
+
19
+ selected_gems = MultiSelect.prompt_for_gems_to_update(report.updatable_gems)
22
20
  puts("No gems to update.").then { return } if selected_gems.empty?
23
21
 
24
- puts "\nUpdating the following gems."
25
- puts
26
- puts Table.new(selected_gems).render
22
+ puts "Updating the following gems."
23
+ puts Table.updatable(selected_gems).render
27
24
  puts
28
25
  report.bundle_update!(*selected_gems.keys)
29
26
  rescue Exception => e # rubocop:disable Lint/RescueException
@@ -32,6 +29,17 @@ module BundleUpdateInteractive
32
29
 
33
30
  private
34
31
 
32
+ def puts_legend_and_withheld_gems(report)
33
+ puts
34
+ puts legend
35
+ puts
36
+ return if report.withheld_gems.empty?
37
+
38
+ puts "The following gems are being held back and cannot be updated."
39
+ puts Table.withheld(report.withheld_gems).render
40
+ puts
41
+ end
42
+
35
43
  def legend
36
44
  pastel = BundleUpdateInteractive.pastel
37
45
  <<~LEGEND
@@ -46,14 +54,13 @@ module BundleUpdateInteractive
46
54
 
47
55
  def generate_report(options)
48
56
  whisper "Resolving latest gem versions..."
49
- report = Report.generate(groups: options.exclusively)
50
- updateable_gems = report.updateable_gems
51
- return report if updateable_gems.empty?
57
+ report = Reporter.new(groups: options.exclusively).generate_report
58
+ return report if report.empty?
52
59
 
53
60
  whisper "Checking for security vulnerabilities..."
54
61
  report.scan_for_vulnerabilities!
55
62
 
56
- progress "Finding changelogs", updateable_gems.values, &:changelog_uri
63
+ progress "Finding changelogs", report.all_gems.values, &:changelog_uri
57
64
  report
58
65
  end
59
66
 
@@ -4,6 +4,7 @@ module BundleUpdateInteractive
4
4
  class OutdatedGem
5
5
  attr_accessor :name,
6
6
  :gemfile_groups,
7
+ :gemfile_requirement,
7
8
  :git_source_uri,
8
9
  :current_version,
9
10
  :current_git_version,
@@ -7,38 +7,20 @@ require "set"
7
7
 
8
8
  module BundleUpdateInteractive
9
9
  class Report
10
- class << self
11
- def generate(groups: [])
12
- gemfile = Gemfile.parse
13
- current_lockfile = Lockfile.parse
14
- gems = groups.any? ? current_lockfile.gems_exclusively_installed_by(gemfile: gemfile, groups: groups) : nil
10
+ attr_reader :withheld_gems, :updatable_gems
15
11
 
16
- updated_lockfile = gems&.none? ? nil : Lockfile.parse(BundlerCommands.read_updated_lockfile(*Array(gems)))
17
- new(gemfile: gemfile, current_lockfile: current_lockfile, updated_lockfile: updated_lockfile)
18
- end
19
- end
20
-
21
- attr_reader :outdated_gems
22
-
23
- def initialize(gemfile:, current_lockfile:, updated_lockfile:)
12
+ def initialize(current_lockfile:, withheld_gems:, updatable_gems:)
24
13
  @current_lockfile = current_lockfile
25
- @outdated_gems = current_lockfile.entries.each_with_object({}) do |current_lockfile_entry, hash|
26
- name = current_lockfile_entry.name
27
- updated_lockfile_entry = updated_lockfile && updated_lockfile[name]
28
- next unless current_lockfile_entry.older_than?(updated_lockfile_entry)
29
-
30
- hash[name] = build_outdated_gem(current_lockfile_entry, updated_lockfile_entry, gemfile[name]&.groups)
31
- end.freeze
14
+ @withheld_gems = withheld_gems.freeze
15
+ @updatable_gems = updatable_gems.freeze
32
16
  end
33
17
 
34
- def [](gem_name)
35
- outdated_gems[gem_name]
18
+ def empty?
19
+ withheld_gems.empty? && updatable_gems.empty?
36
20
  end
37
21
 
38
- def updateable_gems
39
- @updateable_gems ||= outdated_gems.reject do |name, _|
40
- current_lockfile[name].exact_requirement?
41
- end.freeze
22
+ def all_gems
23
+ @all_gems ||= withheld_gems.merge(updatable_gems)
42
24
  end
43
25
 
44
26
  def expand_gems_with_exact_dependencies(*gem_names)
@@ -47,13 +29,13 @@ module BundleUpdateInteractive
47
29
  end
48
30
 
49
31
  def scan_for_vulnerabilities!
50
- return false if outdated_gems.empty?
32
+ return false if all_gems.empty?
51
33
 
52
34
  Bundler::Audit::Database.update!(quiet: true)
53
35
  audit_report = Bundler::Audit::Scanner.new.report
54
36
  vulnerable_gem_names = Set.new(audit_report.vulnerable_gems.map(&:name))
55
37
 
56
- outdated_gems.each do |name, gem|
38
+ all_gems.each do |name, gem|
57
39
  gem.vulnerable = (vulnerable_gem_names & [name, *current_lockfile[name].exact_dependencies]).any?
58
40
  end
59
41
  true
@@ -67,18 +49,5 @@ module BundleUpdateInteractive
67
49
  private
68
50
 
69
51
  attr_reader :current_lockfile
70
-
71
- def build_outdated_gem(current_lockfile_entry, updated_lockfile_entry, gemfile_groups)
72
- OutdatedGem.new(
73
- name: current_lockfile_entry.name,
74
- gemfile_groups: gemfile_groups,
75
- rubygems_source: updated_lockfile_entry.rubygems_source?,
76
- git_source_uri: updated_lockfile_entry.git_source_uri&.to_s,
77
- current_version: current_lockfile_entry.version.to_s,
78
- current_git_version: current_lockfile_entry.git_version&.strip,
79
- updated_version: updated_lockfile_entry.version.to_s,
80
- updated_git_version: updated_lockfile_entry.git_version&.strip
81
- )
82
- end
83
52
  end
84
53
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BundleUpdateInteractive
4
+ class Reporter
5
+ def initialize(groups: [])
6
+ @gemfile = Gemfile.parse
7
+ @current_lockfile = Lockfile.parse
8
+ @candidate_gems = current_lockfile.gems_exclusively_installed_by(gemfile: gemfile, groups: groups) if groups.any?
9
+ end
10
+
11
+ def generate_report
12
+ updatable_gems = find_updatable_gems
13
+ withheld_gems = find_withheld_gems(exclude: updatable_gems.keys)
14
+
15
+ Report.new(current_lockfile: current_lockfile, updatable_gems: updatable_gems, withheld_gems: withheld_gems)
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :gemfile, :current_lockfile, :candidate_gems
21
+
22
+ def find_updatable_gems
23
+ return {} if candidate_gems && candidate_gems.empty?
24
+
25
+ updated_lockfile = Lockfile.parse(BundlerCommands.read_updated_lockfile(*Array(candidate_gems)))
26
+ current_lockfile.entries.each_with_object({}) do |current_lockfile_entry, hash|
27
+ name = current_lockfile_entry.name
28
+ updated_lockfile_entry = updated_lockfile && updated_lockfile[name]
29
+ next unless current_lockfile_entry.older_than?(updated_lockfile_entry)
30
+ next if current_lockfile_entry.exact_requirement?
31
+
32
+ hash[name] = build_outdated_gem(name, updated_lockfile_entry.version, updated_lockfile_entry.git_version)
33
+ end
34
+ end
35
+
36
+ def build_outdated_gem(name, updated_version, updated_git_version)
37
+ current_lockfile_entry = current_lockfile[name]
38
+
39
+ OutdatedGem.new(
40
+ name: name,
41
+ gemfile_groups: gemfile[name]&.groups,
42
+ gemfile_requirement: gemfile[name]&.requirement&.to_s,
43
+ rubygems_source: current_lockfile_entry.rubygems_source?,
44
+ git_source_uri: current_lockfile_entry.git_source_uri&.to_s,
45
+ current_version: current_lockfile_entry.version.to_s,
46
+ current_git_version: current_lockfile_entry.git_version&.strip,
47
+ updated_version: updated_version.to_s,
48
+ updated_git_version: updated_git_version&.strip
49
+ )
50
+ end
51
+
52
+ def find_withheld_gems(exclude: [])
53
+ possibly_withheld = gemfile.dependencies.filter_map do |dep|
54
+ dep.name if dep.should_include? && !dep.requirement.none? # rubocop:disable Style/InverseMethods
55
+ end
56
+ possibly_withheld -= exclude
57
+ possibly_withheld &= candidate_gems unless candidate_gems.nil?
58
+
59
+ return {} if possibly_withheld.empty?
60
+
61
+ BundlerCommands.parse_outdated(*possibly_withheld).to_h do |name, newest|
62
+ [name, build_outdated_gem(name, newest, nil)]
63
+ end
64
+ end
65
+ end
66
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BundleUpdateInteractive
4
- VERSION = "0.6.0"
4
+ VERSION = "0.7.0"
5
5
  end
@@ -13,6 +13,7 @@ module BundleUpdateInteractive
13
13
  autoload :LockfileEntry, "bundle_update_interactive/lockfile_entry"
14
14
  autoload :OutdatedGem, "bundle_update_interactive/outdated_gem"
15
15
  autoload :Report, "bundle_update_interactive/report"
16
+ autoload :Reporter, "bundle_update_interactive/reporter"
16
17
  autoload :SemverChange, "bundle_update_interactive/semver_change"
17
18
  autoload :VERSION, "bundle_update_interactive/version"
18
19
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bundle_update_interactive
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Brictson
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-08-10 00:00:00.000000000 Z
11
+ date: 2024-08-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -122,6 +122,7 @@ files:
122
122
  - lib/bundle_update_interactive/lockfile_entry.rb
123
123
  - lib/bundle_update_interactive/outdated_gem.rb
124
124
  - lib/bundle_update_interactive/report.rb
125
+ - lib/bundle_update_interactive/reporter.rb
125
126
  - lib/bundle_update_interactive/semver_change.rb
126
127
  - lib/bundle_update_interactive/version.rb
127
128
  homepage: https://github.com/mattbrictson/bundle_update_interactive