bundle_update_interactive 0.6.0 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +36 -0
- data/lib/bundle_update_interactive/bundler_commands.rb +15 -0
- data/lib/bundle_update_interactive/cli/multi_select.rb +4 -2
- data/lib/bundle_update_interactive/cli/options.rb +12 -0
- data/lib/bundle_update_interactive/cli/row.rb +4 -11
- data/lib/bundle_update_interactive/cli/table.rb +36 -4
- data/lib/bundle_update_interactive/cli.rb +32 -22
- data/lib/bundle_update_interactive/latest/gem_requirement.rb +62 -0
- data/lib/bundle_update_interactive/latest/gemfile_editor.rb +68 -0
- data/lib/bundle_update_interactive/latest/updater.rb +41 -0
- data/lib/bundle_update_interactive/outdated_gem.rb +1 -0
- data/lib/bundle_update_interactive/report.rb +12 -52
- data/lib/bundle_update_interactive/string_helper.rb +12 -0
- data/lib/bundle_update_interactive/updater.rb +81 -0
- data/lib/bundle_update_interactive/version.rb +1 -1
- data/lib/bundle_update_interactive.rb +6 -13
- metadata +22 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0cbe5e5a4fef836edfd7c1bc87939066c2f7460f4f1b883510ea065840747a54
|
4
|
+
data.tar.gz: 26bc0b61f7d3d2830c3fd687bd8e723486446cdbe6acf91455e6d02dfbb93df6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d351f14505560566df5b09fd27ca1a537bca621208bb1859bb6ba1a430333a5889a2d8f1dfd6ce5b72ede3cecb3429c148a1e48a3a795c3290effe0e37c207d0
|
7
|
+
data.tar.gz: c649e223608a24c65715f7c430abc64841b41a8d765612a57c64cc5c7c56f4cc1327389916521c5f19af0742e177b4f3e5d31267aacbda3048efa5b9b9b0cc78
|
data/README.md
CHANGED
@@ -12,6 +12,7 @@
|
|
12
12
|
---
|
13
13
|
|
14
14
|
- [Quick start](#quick-start)
|
15
|
+
- [Options](#options)
|
15
16
|
- [Features](#features)
|
16
17
|
- [Prior art](#prior-art)
|
17
18
|
- [Support](#support)
|
@@ -39,6 +40,11 @@ Or the shorthand:
|
|
39
40
|
bundle ui
|
40
41
|
```
|
41
42
|
|
43
|
+
## Options
|
44
|
+
|
45
|
+
- `--latest` [modifies the Gemfile if necessary to allow the latest gem versions](#allow-latest-versions)
|
46
|
+
- `-D` / `--exclusively=GROUP` [limits updatable gems by Gemfile groups](#limit-impact-by-gemfile-groups)
|
47
|
+
|
42
48
|
## Features
|
43
49
|
|
44
50
|
### Semver highlighting
|
@@ -63,6 +69,36 @@ Some gems, notably `rails`, are composed of smaller gems like `actionpack`, `act
|
|
63
69
|
|
64
70
|
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
71
|
|
72
|
+
### Held back gems
|
73
|
+
|
74
|
+
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.
|
75
|
+
|
76
|
+
<img src="images/held-back.png" alt="Screenshot of rails and selenium-webdriver gems held back due to Gemfile requirements" width="717" />
|
77
|
+
|
78
|
+
To allow updates for gems that would normally be held back, use the `--latest` option (explained in the next section).
|
79
|
+
|
80
|
+
### Allow latest versions
|
81
|
+
|
82
|
+
Normally `update-interactive` only makes changes to your Gemfile.lock. It honors the version restrictions ("pins") in your Gemfile and will not update your Gemfile.lock to have versions that are not allowed. However with the `--latest` flag, update-interactive can update the version pins in your Gemfile as well. Consider the following Gemfile:
|
83
|
+
|
84
|
+
```ruby
|
85
|
+
gem "rails", "~> 7.1.0"
|
86
|
+
```
|
87
|
+
|
88
|
+
Normally running `bundle update-interactive` will report that Rails is held back and therefore cannot be updated to the latest version. However, if you pass the `--latest` option like this:
|
89
|
+
|
90
|
+
```
|
91
|
+
bundle update-interactive --latest
|
92
|
+
```
|
93
|
+
|
94
|
+
Now Rails will be allowed to update. If you select to update Rails to the latest version (e.g. 7.2.0), `update-interactive` will modify the version requirement in your Gemfile to look like this:
|
95
|
+
|
96
|
+
```ruby
|
97
|
+
gem "rails", "~> 7.2.0"
|
98
|
+
```
|
99
|
+
|
100
|
+
In other words, it works similarly to `yarn upgrade-interactive --latest`.
|
101
|
+
|
66
102
|
### Changelogs
|
67
103
|
|
68
104
|
`bundle update-interactive` will do its best to find an appropriate changelog for each gem.
|
@@ -10,6 +10,13 @@ module BundleUpdateInteractive
|
|
10
10
|
system "#{bundle_bin.shellescape} update --conservative #{gems.flatten.map(&:shellescape).join(' ')}"
|
11
11
|
end
|
12
12
|
|
13
|
+
def lock
|
14
|
+
success = system "#{bundle_bin.shellescape} lock"
|
15
|
+
raise "bundle lock command failed" unless success
|
16
|
+
|
17
|
+
true
|
18
|
+
end
|
19
|
+
|
13
20
|
def read_updated_lockfile(*gems)
|
14
21
|
command = ["#{bundle_bin.shellescape} lock --print"]
|
15
22
|
command << "--conservative" if gems.any?
|
@@ -19,6 +26,14 @@ module BundleUpdateInteractive
|
|
19
26
|
`#{command.join(" ")}`.tap { raise "bundle lock command failed" unless Process.last_status.success? }
|
20
27
|
end
|
21
28
|
|
29
|
+
def parse_outdated(*gems)
|
30
|
+
command = ["#{bundle_bin.shellescape} outdated --parseable", *gems.flatten.map(&:shellescape)]
|
31
|
+
output = `#{command.join(" ")}`
|
32
|
+
raise "bundle outdated command failed" if output.empty? && !Process.last_status.success?
|
33
|
+
|
34
|
+
output.scan(/^(\S+) \(newest (\S+),/).to_h
|
35
|
+
end
|
36
|
+
|
22
37
|
private
|
23
38
|
|
24
39
|
def bundle_bin
|
@@ -7,6 +7,8 @@ require "tty/screen"
|
|
7
7
|
|
8
8
|
class BundleUpdateInteractive::CLI
|
9
9
|
class MultiSelect
|
10
|
+
extend BundleUpdateInteractive::StringHelper
|
11
|
+
|
10
12
|
class List < TTY::Prompt::MultiList
|
11
13
|
def initialize(prompt, **options)
|
12
14
|
@opener = options.delete(:opener)
|
@@ -46,8 +48,8 @@ class BundleUpdateInteractive::CLI
|
|
46
48
|
end
|
47
49
|
|
48
50
|
def self.prompt_for_gems_to_update(outdated_gems, prompt: nil)
|
49
|
-
table = Table.
|
50
|
-
title = "#{outdated_gems.length}
|
51
|
+
table = Table.updatable(outdated_gems)
|
52
|
+
title = "#{pluralize(outdated_gems.length, 'gem')} can be updated."
|
51
53
|
opener = lambda do |gem|
|
52
54
|
url = outdated_gems[gem].changelog_uri
|
53
55
|
Launchy.open(url) unless url.nil?
|
@@ -48,6 +48,9 @@ module BundleUpdateInteractive
|
|
48
48
|
Show updates for development and test gems only, leaving production gems untouched.
|
49
49
|
#{pastel.green('bundle update-interactive')} #{pastel.yellow('-D')}
|
50
50
|
|
51
|
+
Allow the latest gem versions, ignoring Gemfile pins. May modify the Gemfile.
|
52
|
+
#{pastel.green('bundle update-interactive')} #{pastel.yellow('--latest')}
|
53
|
+
|
51
54
|
HELP
|
52
55
|
end
|
53
56
|
|
@@ -61,6 +64,9 @@ module BundleUpdateInteractive
|
|
61
64
|
OptionParser.new do |parser|
|
62
65
|
parser.summary_indent = " "
|
63
66
|
parser.summary_width = 24
|
67
|
+
parser.on("--latest", "Modify the Gemfile to allow the latest gem versions") do
|
68
|
+
options.latest = true
|
69
|
+
end
|
64
70
|
parser.on(
|
65
71
|
"--exclusively=GROUP",
|
66
72
|
"Update gems exclusively belonging to the specified Gemfile GROUP(s)"
|
@@ -84,9 +90,15 @@ module BundleUpdateInteractive
|
|
84
90
|
end
|
85
91
|
|
86
92
|
attr_accessor :exclusively
|
93
|
+
attr_writer :latest
|
87
94
|
|
88
95
|
def initialize
|
89
96
|
@exclusively = []
|
97
|
+
@latest = false
|
98
|
+
end
|
99
|
+
|
100
|
+
def latest?
|
101
|
+
@latest
|
90
102
|
end
|
91
103
|
end
|
92
104
|
end
|
@@ -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
|
-
|
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
|
-
|
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 =
|
12
|
-
@rows =
|
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
|
|
@@ -4,34 +4,42 @@ require "bundler"
|
|
4
4
|
|
5
5
|
module BundleUpdateInteractive
|
6
6
|
class CLI
|
7
|
-
autoload :MultiSelect, "bundle_update_interactive/cli/multi_select"
|
8
|
-
autoload :Options, "bundle_update_interactive/cli/options"
|
9
|
-
autoload :Row, "bundle_update_interactive/cli/row"
|
10
|
-
autoload :Table, "bundle_update_interactive/cli/table"
|
11
|
-
|
12
7
|
def run(argv: ARGV) # rubocop:disable Metrics/AbcSize
|
13
8
|
options = Options.parse(argv)
|
9
|
+
report, updater = generate_report(options)
|
14
10
|
|
15
|
-
report
|
16
|
-
puts("No gems to update.").then { return } if report.
|
11
|
+
puts_legend_and_withheld_gems(report) unless report.empty?
|
12
|
+
puts("No gems to update.").then { return } if report.updatable_gems.empty?
|
17
13
|
|
18
|
-
|
19
|
-
puts legend
|
20
|
-
puts
|
21
|
-
selected_gems = MultiSelect.prompt_for_gems_to_update(report.updateable_gems)
|
14
|
+
selected_gems = MultiSelect.prompt_for_gems_to_update(report.updatable_gems)
|
22
15
|
puts("No gems to update.").then { return } if selected_gems.empty?
|
23
16
|
|
24
|
-
puts "
|
17
|
+
puts "Updating the following gems."
|
18
|
+
puts Table.updatable(selected_gems).render
|
25
19
|
puts
|
26
|
-
|
27
|
-
|
28
|
-
report.bundle_update!(*selected_gems.keys)
|
20
|
+
updater.apply_updates(*selected_gems.keys)
|
21
|
+
puts_gemfile_modified_notice if updater.modified_gemfile?
|
29
22
|
rescue Exception => e # rubocop:disable Lint/RescueException
|
30
23
|
handle_exception(e)
|
31
24
|
end
|
32
25
|
|
33
26
|
private
|
34
27
|
|
28
|
+
def puts_gemfile_modified_notice
|
29
|
+
puts BundleUpdateInteractive.pastel.yellow("Your Gemfile was changed to accommodate the latest gem versions.")
|
30
|
+
end
|
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,15 +54,17 @@ module BundleUpdateInteractive
|
|
46
54
|
|
47
55
|
def generate_report(options)
|
48
56
|
whisper "Resolving latest gem versions..."
|
49
|
-
|
50
|
-
|
51
|
-
return report if updateable_gems.empty?
|
57
|
+
updater_class = options.latest? ? Latest::Updater : Updater
|
58
|
+
updater = updater_class.new(groups: options.exclusively)
|
52
59
|
|
53
|
-
|
54
|
-
report.
|
60
|
+
report = updater.generate_report
|
61
|
+
unless report.empty?
|
62
|
+
whisper "Checking for security vulnerabilities..."
|
63
|
+
report.scan_for_vulnerabilities!
|
64
|
+
progress "Finding changelogs", report.all_gems.values, &:changelog_uri
|
65
|
+
end
|
55
66
|
|
56
|
-
|
57
|
-
report
|
67
|
+
[report, updater]
|
58
68
|
end
|
59
69
|
|
60
70
|
def whisper(message)
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BundleUpdateInteractive
|
4
|
+
module Latest
|
5
|
+
class GemRequirement
|
6
|
+
def self.parse(version)
|
7
|
+
return version if version.is_a?(GemRequirement)
|
8
|
+
|
9
|
+
_, operator, number = version.strip.match(/^([^\d\s]*)\s*(.+)/).to_a
|
10
|
+
operator = nil if operator.empty?
|
11
|
+
|
12
|
+
new(parts: number.split("."), operator: operator, parsed_from: version)
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :parts, :operator
|
16
|
+
|
17
|
+
def initialize(parts:, operator: nil, parsed_from: nil)
|
18
|
+
@parts = parts
|
19
|
+
@operator = operator
|
20
|
+
@parsed_from = parsed_from
|
21
|
+
end
|
22
|
+
|
23
|
+
def exact?
|
24
|
+
operator.nil?
|
25
|
+
end
|
26
|
+
|
27
|
+
def relax
|
28
|
+
return self if %w[!= > >=].include?(operator)
|
29
|
+
return self.class.parse(">= 0") if %w[< <=].include?(operator)
|
30
|
+
|
31
|
+
self.class.new(parts: parts, operator: ">=")
|
32
|
+
end
|
33
|
+
|
34
|
+
def shift(new_version) # rubocop:disable Metrics/AbcSize
|
35
|
+
return self.class.parse(new_version) if exact?
|
36
|
+
return self if Gem::Requirement.new(to_s).satisfied_by?(Gem::Version.new(new_version))
|
37
|
+
return self.class.new(parts: self.class.parse(new_version).parts, operator: "<=") if %w[< <=].include?(operator)
|
38
|
+
|
39
|
+
new_slice = self.class.parse(new_version).slice(parts.length)
|
40
|
+
self.class.new(parts: new_slice.parts, operator: "~>")
|
41
|
+
end
|
42
|
+
|
43
|
+
def slice(amount)
|
44
|
+
self.class.new(parts: parts[0, amount], operator: operator)
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_s
|
48
|
+
parsed_from || [operator, parts.join(".")].compact.join(" ")
|
49
|
+
end
|
50
|
+
|
51
|
+
def ==(other)
|
52
|
+
return false unless other.is_a?(GemRequirement)
|
53
|
+
|
54
|
+
to_s == other.to_s
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
attr_reader :parsed_from
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BundleUpdateInteractive
|
4
|
+
module Latest
|
5
|
+
class GemfileEditor
|
6
|
+
def initialize(gemfile_path: "Gemfile", lockfile_path: "Gemfile.lock")
|
7
|
+
@gemfile_path = gemfile_path
|
8
|
+
@lockfile_path = lockfile_path
|
9
|
+
end
|
10
|
+
|
11
|
+
def with_relaxed_gemfile
|
12
|
+
original, modified = modify_gemfile { |_, requirement| requirement.relax }
|
13
|
+
yield
|
14
|
+
ensure
|
15
|
+
File.write(gemfile_path, original) if original && original != modified
|
16
|
+
end
|
17
|
+
|
18
|
+
def shift_gemfile
|
19
|
+
lockfile = Lockfile.parse(File.read(lockfile_path))
|
20
|
+
original, modified = modify_gemfile do |name, requirement|
|
21
|
+
lockfile_entry = lockfile[name]
|
22
|
+
requirement.shift(lockfile_entry.version.to_s) if lockfile_entry
|
23
|
+
end
|
24
|
+
original != modified
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :gemfile_path, :lockfile_path
|
30
|
+
|
31
|
+
def modify_gemfile(&block)
|
32
|
+
original_contents = File.read(gemfile_path)
|
33
|
+
new_contents = original_contents.dup
|
34
|
+
|
35
|
+
find_rewritable_gem_names(original_contents).each do |name|
|
36
|
+
rewrite_contents(name, new_contents, &block)
|
37
|
+
end
|
38
|
+
|
39
|
+
File.write(gemfile_path, new_contents) unless new_contents == original_contents
|
40
|
+
[original_contents, new_contents]
|
41
|
+
end
|
42
|
+
|
43
|
+
def find_rewritable_gem_names(contents)
|
44
|
+
Gemfile.parse(gemfile_path).dependencies.filter_map do |dep|
|
45
|
+
gem_name = dep.name
|
46
|
+
gem_name if gem_declaration_with_requirement_re(gem_name).match?(contents)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def rewrite_contents(gem_name, contents)
|
51
|
+
found = contents.sub!(gem_declaration_with_requirement_re(gem_name)) do |match|
|
52
|
+
version = Regexp.last_match[1]
|
53
|
+
match[Regexp.last_match.regexp, 1] = yield(gem_name, GemRequirement.parse(version)).to_s
|
54
|
+
match
|
55
|
+
end
|
56
|
+
raise "Can't rewrite version for #{gem_name}" unless found
|
57
|
+
end
|
58
|
+
|
59
|
+
def gem_declaration_re(gem_name)
|
60
|
+
/^\s*gem\s+["']#{Regexp.escape(gem_name)}["']/
|
61
|
+
end
|
62
|
+
|
63
|
+
def gem_declaration_with_requirement_re(gem_name)
|
64
|
+
/#{gem_declaration_re(gem_name)},\s*["']([^'"]+)["']/
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Extends the default Updater class to allow updating to the latest gem versions.
|
4
|
+
# Does this by using GemfileEditor to relax the Gemfile requirements before
|
5
|
+
# `find_updatable_gems` and `apply_updates` are called.
|
6
|
+
module BundleUpdateInteractive
|
7
|
+
module Latest
|
8
|
+
class Updater < BundleUpdateInteractive::Updater
|
9
|
+
def initialize(editor: GemfileEditor.new, **kwargs)
|
10
|
+
super(**kwargs)
|
11
|
+
@modified_gemfile = false
|
12
|
+
@editor = editor
|
13
|
+
end
|
14
|
+
|
15
|
+
def apply_updates(*, **)
|
16
|
+
result = editor.with_relaxed_gemfile { super }
|
17
|
+
@modified_gemfile = editor.shift_gemfile
|
18
|
+
BundlerCommands.lock
|
19
|
+
result
|
20
|
+
end
|
21
|
+
|
22
|
+
def modified_gemfile?
|
23
|
+
@modified_gemfile
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
attr_reader :editor
|
29
|
+
|
30
|
+
def find_updatable_gems
|
31
|
+
editor.with_relaxed_gemfile { super }
|
32
|
+
end
|
33
|
+
|
34
|
+
# Overrides the default Updater implementation.
|
35
|
+
# When updating the latest gems, by definition nothing is withheld, so we can skip this.
|
36
|
+
def find_withheld_gems(**)
|
37
|
+
{}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -7,78 +7,38 @@ require "set"
|
|
7
7
|
|
8
8
|
module BundleUpdateInteractive
|
9
9
|
class Report
|
10
|
-
|
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
|
-
|
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
|
-
@
|
26
|
-
|
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
|
35
|
-
|
18
|
+
def empty?
|
19
|
+
withheld_gems.empty? && updatable_gems.empty?
|
36
20
|
end
|
37
21
|
|
38
|
-
def
|
39
|
-
@
|
40
|
-
current_lockfile[name].exact_requirement?
|
41
|
-
end.freeze
|
42
|
-
end
|
43
|
-
|
44
|
-
def expand_gems_with_exact_dependencies(*gem_names)
|
45
|
-
gem_names.flatten!
|
46
|
-
gem_names.flat_map { |name| [name, *current_lockfile[name].exact_dependencies] }.uniq
|
22
|
+
def all_gems
|
23
|
+
@all_gems ||= withheld_gems.merge(updatable_gems)
|
47
24
|
end
|
48
25
|
|
49
26
|
def scan_for_vulnerabilities!
|
50
|
-
return false if
|
27
|
+
return false if all_gems.empty?
|
51
28
|
|
52
29
|
Bundler::Audit::Database.update!(quiet: true)
|
53
30
|
audit_report = Bundler::Audit::Scanner.new.report
|
54
31
|
vulnerable_gem_names = Set.new(audit_report.vulnerable_gems.map(&:name))
|
55
32
|
|
56
|
-
|
57
|
-
|
33
|
+
all_gems.each do |name, gem|
|
34
|
+
exact_deps = current_lockfile && current_lockfile[name].exact_dependencies
|
35
|
+
gem.vulnerable = (vulnerable_gem_names & [name, *Array(exact_deps)]).any?
|
58
36
|
end
|
59
37
|
true
|
60
38
|
end
|
61
39
|
|
62
|
-
def bundle_update!(*gem_names)
|
63
|
-
expanded_names = expand_gems_with_exact_dependencies(*gem_names)
|
64
|
-
BundlerCommands.update_gems_conservatively(*expanded_names)
|
65
|
-
end
|
66
|
-
|
67
40
|
private
|
68
41
|
|
69
42
|
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
43
|
end
|
84
44
|
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# lib/bundle_update_interactive/string_helper.rb
|
4
|
+
module BundleUpdateInteractive
|
5
|
+
module StringHelper
|
6
|
+
def pluralize(count, singular, plural=nil)
|
7
|
+
plural ||= "#{singular}s"
|
8
|
+
"#{count} #{count == 1 ? singular : plural}"
|
9
|
+
end
|
10
|
+
module_function :pluralize
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module BundleUpdateInteractive
|
4
|
+
class Updater
|
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
|
+
def apply_updates(*gem_names)
|
19
|
+
expanded_names = expand_gems_with_exact_dependencies(*gem_names)
|
20
|
+
BundlerCommands.update_gems_conservatively(*expanded_names)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Overridden by Latest::Updater subclass
|
24
|
+
def modified_gemfile?
|
25
|
+
false
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
attr_reader :gemfile, :current_lockfile, :candidate_gems
|
31
|
+
|
32
|
+
def find_updatable_gems
|
33
|
+
return {} if candidate_gems && candidate_gems.empty?
|
34
|
+
|
35
|
+
updated_lockfile = Lockfile.parse(BundlerCommands.read_updated_lockfile(*Array(candidate_gems)))
|
36
|
+
current_lockfile.entries.each_with_object({}) do |current_lockfile_entry, hash|
|
37
|
+
name = current_lockfile_entry.name
|
38
|
+
updated_lockfile_entry = updated_lockfile && updated_lockfile[name]
|
39
|
+
next unless current_lockfile_entry.older_than?(updated_lockfile_entry)
|
40
|
+
next if current_lockfile_entry.exact_requirement?
|
41
|
+
|
42
|
+
hash[name] = build_outdated_gem(name, updated_lockfile_entry.version, updated_lockfile_entry.git_version)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def find_withheld_gems(exclude: [])
|
47
|
+
possibly_withheld = gemfile.dependencies.filter_map do |dep|
|
48
|
+
dep.name if dep.should_include? && !dep.requirement.none? # rubocop:disable Style/InverseMethods
|
49
|
+
end
|
50
|
+
possibly_withheld -= exclude
|
51
|
+
possibly_withheld &= candidate_gems unless candidate_gems.nil?
|
52
|
+
|
53
|
+
return {} if possibly_withheld.empty?
|
54
|
+
|
55
|
+
BundlerCommands.parse_outdated(*possibly_withheld).to_h do |name, newest|
|
56
|
+
[name, build_outdated_gem(name, newest, nil)]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def build_outdated_gem(name, updated_version, updated_git_version)
|
61
|
+
current_lockfile_entry = current_lockfile[name]
|
62
|
+
|
63
|
+
OutdatedGem.new(
|
64
|
+
name: name,
|
65
|
+
gemfile_groups: gemfile[name]&.groups,
|
66
|
+
gemfile_requirement: gemfile[name]&.requirement&.to_s,
|
67
|
+
rubygems_source: current_lockfile_entry.rubygems_source?,
|
68
|
+
git_source_uri: current_lockfile_entry.git_source_uri&.to_s,
|
69
|
+
current_version: current_lockfile_entry.version.to_s,
|
70
|
+
current_git_version: current_lockfile_entry.git_version&.strip,
|
71
|
+
updated_version: updated_version.to_s,
|
72
|
+
updated_git_version: updated_git_version&.strip
|
73
|
+
)
|
74
|
+
end
|
75
|
+
|
76
|
+
def expand_gems_with_exact_dependencies(*gem_names)
|
77
|
+
gem_names.flatten!
|
78
|
+
gem_names.flat_map { |name| [name, *current_lockfile[name].exact_dependencies] }.uniq
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -2,20 +2,13 @@
|
|
2
2
|
|
3
3
|
require "pastel"
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
autoload :Gemfile, "bundle_update_interactive/gemfile"
|
11
|
-
autoload :HTTP, "bundle_update_interactive/http"
|
12
|
-
autoload :Lockfile, "bundle_update_interactive/lockfile"
|
13
|
-
autoload :LockfileEntry, "bundle_update_interactive/lockfile_entry"
|
14
|
-
autoload :OutdatedGem, "bundle_update_interactive/outdated_gem"
|
15
|
-
autoload :Report, "bundle_update_interactive/report"
|
16
|
-
autoload :SemverChange, "bundle_update_interactive/semver_change"
|
17
|
-
autoload :VERSION, "bundle_update_interactive/version"
|
5
|
+
require "zeitwerk"
|
6
|
+
loader = Zeitwerk::Loader.for_gem
|
7
|
+
loader.inflector.inflect "cli" => "CLI"
|
8
|
+
loader.inflector.inflect "http" => "HTTP"
|
9
|
+
loader.setup
|
18
10
|
|
11
|
+
module BundleUpdateInteractive
|
19
12
|
class << self
|
20
13
|
attr_accessor :pastel
|
21
14
|
end
|
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.
|
4
|
+
version: 0.8.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-
|
11
|
+
date: 2024-08-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -94,6 +94,20 @@ dependencies:
|
|
94
94
|
- - ">="
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: 0.8.2
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: zeitwerk
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '2.6'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '2.6'
|
97
111
|
description:
|
98
112
|
email:
|
99
113
|
- opensource@mattbrictson.com
|
@@ -118,11 +132,16 @@ files:
|
|
118
132
|
- lib/bundle_update_interactive/error.rb
|
119
133
|
- lib/bundle_update_interactive/gemfile.rb
|
120
134
|
- lib/bundle_update_interactive/http.rb
|
135
|
+
- lib/bundle_update_interactive/latest/gem_requirement.rb
|
136
|
+
- lib/bundle_update_interactive/latest/gemfile_editor.rb
|
137
|
+
- lib/bundle_update_interactive/latest/updater.rb
|
121
138
|
- lib/bundle_update_interactive/lockfile.rb
|
122
139
|
- lib/bundle_update_interactive/lockfile_entry.rb
|
123
140
|
- lib/bundle_update_interactive/outdated_gem.rb
|
124
141
|
- lib/bundle_update_interactive/report.rb
|
125
142
|
- lib/bundle_update_interactive/semver_change.rb
|
143
|
+
- lib/bundle_update_interactive/string_helper.rb
|
144
|
+
- lib/bundle_update_interactive/updater.rb
|
126
145
|
- lib/bundle_update_interactive/version.rb
|
127
146
|
homepage: https://github.com/mattbrictson/bundle_update_interactive
|
128
147
|
licenses:
|
@@ -148,7 +167,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
148
167
|
- !ruby/object:Gem::Version
|
149
168
|
version: '0'
|
150
169
|
requirements: []
|
151
|
-
rubygems_version: 3.5.
|
170
|
+
rubygems_version: 3.5.11
|
152
171
|
signing_key:
|
153
172
|
specification_version: 4
|
154
173
|
summary: Adds an update-interactive command to Bundler
|