bundle_update_interactive 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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