bundle_update_interactive 0.2.1 → 0.4.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: 24de9c6dde2b3514ffad21a64256359200e2bb717b1a0385caa8d032776d5fe6
4
- data.tar.gz: 4a1a1c65bd89e4c7e17d563c2a854c80e07edeb7d04e6b4ffdf3cbef1d47ddee
3
+ metadata.gz: 5eb0324c92b1a60c3611ce47993022fef9c3d69aeba735a752252cc2cffe8158
4
+ data.tar.gz: f68c74d7fc286abf743796b3323bed579736bf548ccc2a405b8a2433bf3eddf1
5
5
  SHA512:
6
- metadata.gz: 493421b5621ff128b56ed96041fd4c1e776de8c0160f330d8458d40963248e2c2e779dc64c7f7afd390c2f98d62a6203b3495534e7b57051f79add6edc729715
7
- data.tar.gz: 638b235f21664177f8b9de29a0b69b9229ef1b57fc985c4ba6c73678ca2142a926caff18626a809b4ab4fdd2aeff4573e7a4137c471ca569d62b95e55322e0ec
6
+ metadata.gz: 874b63b22b4ff2ad9101f9a5a51b2947cf27009ff37096ae0ff3f00e3f5de5ba2ea32a4e2a2adae0d46bf4a523868deb5f8f7ec45599f988719a983e1bb9e16f
7
+ data.tar.gz: 7f134fec1b5304ed93921506a4e763a1f7ef7954f812b679cb63e271c99fc76bd291f68f1e9fd51a532a3d63a55e74ad01657767250924cd8a2a0d0d7baa69ce
data/README.md CHANGED
@@ -75,17 +75,47 @@ If you discover a gem that is missing a changelog in `bundle update-interactive`
75
75
 
76
76
  ### Git diffs
77
77
 
78
- If your `Gemfile` sources a gem from a GitHub repo like this:
78
+ If your `Gemfile` sources a gem from a Git repo like this:
79
79
 
80
80
  ```ruby
81
81
  gem "rails", github: "rails/rails", branch: "7-1-stable"
82
82
  ```
83
83
 
84
- Then `bundle update-interactive` will show a GitHub diff link instead of a changelog, so you can see exactly what changed when the gem is updated. For example:
84
+ Then `bundle update-interactive` will show a diff link instead of a changelog, so you can see exactly what changed when the gem is updated. For example:
85
85
 
86
86
  https://github.com/rails/rails/compare/5a8d894...77dfa65
87
87
 
88
- Currently only GitHub repos are supported, but I'm considering adding GitLab and BitBucket as well.
88
+ This feature currently works for GitHub, GitLab, and Bitbucket repos.
89
+
90
+ ### Limit impact by Gemfile groups
91
+
92
+ The effects of `bundle update-interactive` can be limited to one or more Gemfile groups using the `--exclusively` option:
93
+
94
+ ```sh
95
+ bundle update-interactive --exclusively=group1,group2
96
+ ```
97
+
98
+ This is especially useful when you want to safely update a subset of your lock file without introducing any risk to your application in production. The best way to do this is with `--exclusively=development,test`, which can be abbreviated to simply `-D`:
99
+
100
+ ```sh
101
+ # Update non-production dependencies.
102
+ # This is equivalent to `bundle update-interactive --exclusively=development,test`
103
+ bundle update-interactive -D
104
+ ```
105
+
106
+ The `--exclusively` and `-D` options will cause `update-interactive` to only consider gems that are used _exclusively_ by the specified Gemfile groups. Indirect dependencies that are shared with other Gemfile groups will not be updated.
107
+
108
+ For example, given this Gemfile:
109
+
110
+ ```ruby
111
+ gem "rails"
112
+
113
+ group :test do
114
+ gem "capybara"
115
+ end
116
+ ```
117
+
118
+ If `--exclusively=test` is used, `capybara` and its indirect dependency `xpath` are both exclusively used in test and can therefore be updated. However, capybara's `nokogiri` indirect dependency, which is also used in production via `rails` → `actionpack` → `nokogiri`, would not be allowed to update.
89
119
 
90
120
  ### Conservative updates
91
121
 
data/exe/bundler-ui CHANGED
@@ -2,4 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "bundle_update_interactive"
5
- BundleUpdateInteractive::CLI.start(ARGV)
5
+
6
+ BundleUpdateInteractive::CLI.new.run
@@ -2,4 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require "bundle_update_interactive"
5
- BundleUpdateInteractive::CLI.start(ARGV)
5
+
6
+ BundleUpdateInteractive::CLI.new.run
@@ -9,10 +9,13 @@ module BundleUpdateInteractive
9
9
  system "bundle update --conservative #{gems.flatten.map(&:shellescape).join(' ')}"
10
10
  end
11
11
 
12
- def read_updated_lockfile
13
- `bundle lock --print --update`.tap do
14
- raise "bundle lock command failed" unless Process.last_status.success?
15
- end
12
+ def read_updated_lockfile(*gems)
13
+ command = ["bundle lock --print"]
14
+ command << "--conservative" if gems.any?
15
+ command << "--update"
16
+ command.push(*gems.flatten.map(&:shellescape))
17
+
18
+ `#{command.join(" ")}`.tap { raise "bundle lock command failed" unless Process.last_status.success? }
16
19
  end
17
20
  end
18
21
  end
@@ -3,55 +3,79 @@
3
3
  require "faraday"
4
4
  require "json"
5
5
 
6
- GITHUB_PATTERN = %r{^(?:https?://)?github\.com/([^/]+/[^/]+)(?:\.git)?/?}.freeze
7
- URI_KEYS = %w[source_code_uri homepage_uri bug_tracker_uri wiki_uri].freeze
8
- FILE_PATTERN = /changelog|changes|history|news|release/i.freeze
9
- EXT_PATTERN = /md|txt|rdoc/i.freeze
10
-
11
6
  module BundleUpdateInteractive
12
7
  class ChangelogLocator
13
- # TODO: refactor
14
- def find_changelog_uri(name:, version: nil) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
15
- if version
16
- response = Faraday.get("https://rubygems.org/api/v2/rubygems/#{name}/versions/#{version}.json")
17
- version = nil unless response.success?
8
+ GITHUB_PATTERN = %r{^(?:https?://)?github\.com/([^/]+/[^/]+)(?:\.git)?/?}.freeze
9
+ URI_KEYS = %w[source_code_uri homepage_uri bug_tracker_uri wiki_uri].freeze
10
+ FILE_PATTERN = /changelog|changes|history|news|release/i.freeze
11
+ EXT_PATTERN = /md|txt|rdoc/i.freeze
12
+
13
+ class GitHubRepo
14
+ def self.from_uris(*uris)
15
+ uris.flatten.each do |uri|
16
+ return new(Regexp.last_match(1)) if uri&.match(GITHUB_PATTERN)
17
+ end
18
+ nil
18
19
  end
19
20
 
20
- response = Faraday.get("https://rubygems.org/api/v1/gems/#{name}.json") if version.nil?
21
+ attr_reader :path
21
22
 
22
- return nil unless response.success?
23
+ def initialize(path)
24
+ @path = path
25
+ end
23
26
 
24
- data = JSON.parse(response.body)
27
+ def discover_changelog_uri(version)
28
+ repo_html = fetch_repo_html(follow_redirect: true)
29
+ return if repo_html.nil?
25
30
 
26
- version ||= data["version"]
27
- changelog_uri = data["changelog_uri"]
28
- github_repo = guess_github_repo(data)
31
+ changelog_path = repo_html[%r{/(#{path}/blob/[^/]+/#{FILE_PATTERN}(?:\.#{EXT_PATTERN})?)"}i, 1]
32
+ return "https://github.com/#{changelog_path}" if changelog_path
29
33
 
30
- if changelog_uri.nil? && github_repo
31
- file_list = Faraday.get("https://github.com/#{github_repo}")
32
- if file_list.status == 301
33
- github_repo = file_list.headers["Location"][GITHUB_PATTERN, 1]
34
- file_list = Faraday.get(file_list.headers["Location"])
35
- end
36
- match = file_list.body.match(%r{/(#{github_repo}/blob/[^/]+/#{FILE_PATTERN}(?:\.#{EXT_PATTERN})?)"}i)
37
- changelog_uri = "https://github.com/#{match[1]}" if match
34
+ releases_url = "https://github.com/#{path}/releases"
35
+ releases_url if Faraday.head("#{releases_url}/tag/v#{version}").success?
38
36
  end
39
37
 
40
- if changelog_uri.nil? && github_repo
41
- releases_uri = "https://github.com/#{github_repo}/releases"
42
- changelog_uri = releases_uri if Faraday.head("#{releases_uri}/tag/v#{version}").success?
38
+ private
39
+
40
+ def fetch_repo_html(follow_redirect:)
41
+ response = Faraday.get("https://github.com/#{path}")
42
+
43
+ if response.status == 301 && follow_redirect
44
+ @path = response.headers["Location"][GITHUB_PATTERN, 1]
45
+ return fetch_repo_html(follow_redirect: false)
46
+ end
47
+
48
+ response.success? ? response.body : nil
43
49
  end
50
+ end
44
51
 
45
- changelog_uri
52
+ def find_changelog_uri(name:, version: nil)
53
+ data = fetch_rubygems_data(name, version)
54
+ return if data.nil?
55
+
56
+ if (rubygems_changelog_uri = data["changelog_uri"])
57
+ rubygems_changelog_uri
58
+ elsif (github_repo = GitHubRepo.from_uris(data.values_at(*URI_KEYS)))
59
+ github_repo.discover_changelog_uri(data["version"])
60
+ end
46
61
  end
47
62
 
48
63
  private
49
64
 
50
- def guess_github_repo(data)
51
- data.values_at(*URI_KEYS).each do |uri|
52
- return Regexp.last_match(1) if uri&.match(GITHUB_PATTERN)
53
- end
54
- nil
65
+ def fetch_rubygems_data(name, version)
66
+ api_url = if version.nil?
67
+ "https://rubygems.org/api/v1/gems/#{name}.json"
68
+ else
69
+ "https://rubygems.org/api/v2/rubygems/#{name}/versions/#{version}.json"
70
+ end
71
+
72
+ response = Faraday.get(api_url)
73
+
74
+ # Try again without the version in case the version does not exist at rubygems for some reason.
75
+ # This can happen when using a pre-release Ruby that has a bundled gem newer than the published version.
76
+ return fetch_rubygems_data(name, nil) if !response.success? && !version.nil?
77
+
78
+ response.success? ? JSON.parse(response.body) : nil
55
79
  end
56
80
  end
57
81
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+
5
+ module BundleUpdateInteractive
6
+ class CLI::Options
7
+ class << self
8
+ def parse(argv=ARGV)
9
+ options = new
10
+ remaining = build_parser(options).parse!(argv.dup)
11
+ raise Error, "update-interactive does not accept arguments. See --help for available options." if remaining.any?
12
+
13
+ options.freeze
14
+ end
15
+
16
+ private
17
+
18
+ def build_parser(options) # rubocop:disable Metrics/MethodLength
19
+ OptionParser.new do |parser|
20
+ parser.banner = "Usage: bundle update-interactive"
21
+ parser.on(
22
+ "--exclusively=GROUP",
23
+ "Update gems that exclusively belong to the specified Gemfile GROUP(s) (comma-separated)"
24
+ ) do |value|
25
+ options.exclusively = value.split(",").map(&:strip).reject(&:empty?).map(&:to_sym)
26
+ end
27
+ parser.on(
28
+ "-D",
29
+ "Update development and test gems only; short for --exclusively=development,test"
30
+ ) do
31
+ options.exclusively = %i[development test]
32
+ end
33
+ parser.on("-v", "--version", "Display bundle_update_interactive version") do
34
+ require "bundler"
35
+ puts "bundle_update_interactive/#{VERSION} bundler/#{Bundler::VERSION} #{RUBY_DESCRIPTION}"
36
+ exit
37
+ end
38
+ parser.on("-h", "--help", "Show this help") do
39
+ puts parser
40
+ exit
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ attr_accessor :exclusively
47
+
48
+ def initialize
49
+ @exclusively = []
50
+ end
51
+ end
52
+ end
@@ -1,40 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "thor"
3
+ require "bundler"
4
4
 
5
5
  module BundleUpdateInteractive
6
- class CLI < Thor
6
+ class CLI
7
7
  autoload :MultiSelect, "bundle_update_interactive/cli/multi_select"
8
+ autoload :Options, "bundle_update_interactive/cli/options"
8
9
  autoload :Row, "bundle_update_interactive/cli/row"
9
10
  autoload :Table, "bundle_update_interactive/cli/table"
10
- autoload :ThorExt, "bundle_update_interactive/cli/thor_ext"
11
11
 
12
- extend ThorExt::Start
12
+ def run(argv: ARGV) # rubocop:disable Metrics/AbcSize
13
+ options = Options.parse(argv)
13
14
 
14
- default_command :ui
15
- map %w[-v --version] => "version"
15
+ report = generate_report(options)
16
+ puts("No gems to update.").then { return } if report.updateable_gems.empty?
16
17
 
17
- desc "version", "Display bundle_update_interactive version", hide: true
18
- def version
19
- say "bundle_update_interactive/#{VERSION} #{RUBY_DESCRIPTION}"
20
- end
21
-
22
- desc "ui", "Update Gemfile.lock interactively", hide: true
23
- def ui # rubocop:disable Metrics/AbcSize
24
- report = generate_report
25
- say("No gems to update.") && return if report.updateable_gems.empty?
26
-
27
- say
28
- say legend
29
- say
18
+ puts
19
+ puts legend
20
+ puts
30
21
  selected_gems = MultiSelect.prompt_for_gems_to_update(report.updateable_gems)
31
- say("No gems to update.") && return if selected_gems.empty?
22
+ puts("No gems to update.").then { return } if selected_gems.empty?
32
23
 
33
- say "\nUpdating the following gems."
34
- say
35
- say Table.new(selected_gems).render
36
- say
24
+ puts "\nUpdating the following gems."
25
+ puts
26
+ puts Table.new(selected_gems).render
27
+ puts
37
28
  report.bundle_update!(*selected_gems.keys)
29
+ rescue Exception => e # rubocop:disable Lint/RescueException
30
+ handle_exception(e)
38
31
  end
39
32
 
40
33
  private
@@ -51,9 +44,9 @@ module BundleUpdateInteractive
51
44
  LEGEND
52
45
  end
53
46
 
54
- def generate_report
47
+ def generate_report(options)
55
48
  whisper "Resolving latest gem versions..."
56
- report = Report.generate
49
+ report = Report.generate(groups: options.exclusively)
57
50
  updateable_gems = report.updateable_gems
58
51
  return report if updateable_gems.empty?
59
52
 
@@ -65,7 +58,7 @@ module BundleUpdateInteractive
65
58
  end
66
59
 
67
60
  def whisper(message)
68
- $stderr.puts(message) # rubocop:disable Style/StderrPuts
61
+ $stderr.puts(message)
69
62
  end
70
63
 
71
64
  def progress(message, items, &block)
@@ -76,5 +69,17 @@ module BundleUpdateInteractive
76
69
  end
77
70
  $stderr.print("\n")
78
71
  end
72
+
73
+ def handle_exception(error)
74
+ case error
75
+ when Errno::EPIPE
76
+ # Ignore
77
+ when BundleUpdateInteractive::Error, OptionParser::ParseError, Interrupt, Bundler::Dsl::DSLError
78
+ $stderr.puts BundleUpdateInteractive.pastel.red(error.message)
79
+ exit false
80
+ else
81
+ raise
82
+ end
83
+ end
79
84
  end
80
85
  end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BundleUpdateInteractive
4
+ class Error < StandardError
5
+ end
6
+ end
@@ -18,5 +18,9 @@ module BundleUpdateInteractive
18
18
  def [](name)
19
19
  @dependencies[name]
20
20
  end
21
+
22
+ def dependencies
23
+ @dependencies.values
24
+ end
21
25
  end
22
26
  end
@@ -1,54 +1,70 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler"
4
+ require "set"
4
5
 
5
6
  module BundleUpdateInteractive
6
7
  class Lockfile
7
- # TODO: refactor
8
- def self.parse(lockfile_contents=File.read("Gemfile.lock")) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
8
+ def self.parse(lockfile_contents=File.read("Gemfile.lock"))
9
9
  parser = Bundler::LockfileParser.new(lockfile_contents)
10
- specs_by_name = {}
11
- exact = Set.new
12
- exact_children = {}
10
+ new(parser.specs)
11
+ end
13
12
 
14
- parser.specs.each do |spec|
15
- specs_by_name[spec.name] = spec
13
+ def initialize(specs)
14
+ @specs_by_name = {}
15
+ required_exactly = Set.new
16
16
 
17
- spec.dependencies.each do |dep|
18
- next unless dep.requirement.exact?
17
+ specs.each do |spec|
18
+ specs_by_name[spec.name] = spec
19
+ spec.dependencies.each { |dep| required_exactly << dep.name if dep.requirement.exact? }
20
+ end
19
21
 
20
- exact << dep.name
21
- (exact_children[spec.name] ||= []) << dep.name
22
- end
22
+ @entries_by_name = specs_by_name.transform_values do |spec|
23
+ build_entry(spec, required_exactly.include?(spec.name))
23
24
  end
25
+ end
24
26
 
25
- entries = specs_by_name.transform_values do |spec|
26
- exact_dependencies = Set.new
27
- traversal = exact_children[spec.name]&.dup || []
28
- until traversal.empty?
29
- name = traversal.pop
30
- next if exact_dependencies.include?(name)
27
+ def entries
28
+ entries_by_name.values
29
+ end
31
30
 
32
- exact_dependencies << name
33
- traversal.push(*exact_children.fetch(name, []))
34
- end
31
+ def [](gem_name)
32
+ entries_by_name[gem_name]
33
+ end
35
34
 
36
- LockfileEntry.new(spec, exact_dependencies, exact.include?(spec.name))
37
- end
35
+ def gems_exclusively_installed_by(gemfile:, groups:)
36
+ return [] if groups.empty?
38
37
 
39
- new(entries)
40
- end
38
+ other_group_gems = gemfile.dependencies.filter_map { |gem| gem.name unless (gem.groups & groups).any? }
39
+ other_group_gems &= entries_by_name.keys
40
+ gems_installed_by_other_groups = other_group_gems + traverse_transient_dependencies(*other_group_gems)
41
41
 
42
- def initialize(entries)
43
- @entries = entries.freeze
42
+ entries_by_name.keys - gems_installed_by_other_groups
44
43
  end
45
44
 
46
- def entries
47
- @entries.values
45
+ private
46
+
47
+ attr_reader :entries_by_name, :specs_by_name
48
+
49
+ def build_entry(spec, exact)
50
+ exact_dependencies = traverse_transient_dependencies(spec.name) { |dep| dep.requirement.exact? }
51
+ LockfileEntry.new(spec, exact_dependencies, exact)
48
52
  end
49
53
 
50
- def [](gem_name)
51
- @entries[gem_name]
54
+ def traverse_transient_dependencies(*gem_names) # rubocop:disable Metrics/AbcSize
55
+ traversal = Set.new
56
+ stack = gem_names.flatten
57
+ until stack.empty?
58
+ specs_by_name[stack.pop].dependencies.each do |dep|
59
+ next if traversal.include?(dep.name)
60
+ next unless specs_by_name.key?(dep.name)
61
+ next if block_given? && !yield(dep)
62
+
63
+ traversal << dep.name
64
+ stack << dep.name
65
+ end
66
+ end
67
+ traversal.to_a
52
68
  end
53
69
  end
54
70
  end
@@ -4,10 +4,10 @@ module BundleUpdateInteractive
4
4
  class LockfileEntry
5
5
  attr_reader :spec, :exact_dependencies
6
6
 
7
- def initialize(spec, exact_dependencies, exact_dependency)
7
+ def initialize(spec, exact_dependencies, exact_requirement)
8
8
  @spec = spec
9
9
  @exact_dependencies = exact_dependencies
10
- @exact_dependency = exact_dependency
10
+ @exact_requirement = exact_requirement
11
11
  end
12
12
 
13
13
  def name
@@ -28,8 +28,8 @@ module BundleUpdateInteractive
28
28
  end
29
29
  end
30
30
 
31
- def exact_dependency?
32
- @exact_dependency
31
+ def exact_requirement?
32
+ @exact_requirement
33
33
  end
34
34
 
35
35
  def git_version
@@ -35,8 +35,8 @@ module BundleUpdateInteractive
35
35
  return @changelog_uri if defined?(@changelog_uri)
36
36
 
37
37
  @changelog_uri =
38
- if git_version_changed? && github_repo
39
- "https://github.com/#{github_repo}/compare/#{current_git_version}...#{updated_git_version}"
38
+ if (diff_url = build_git_diff_url)
39
+ diff_url
40
40
  elsif rubygems_source?
41
41
  changelog_locator.find_changelog_uri(name: name, version: updated_version.to_s)
42
42
  else
@@ -56,10 +56,34 @@ module BundleUpdateInteractive
56
56
 
57
57
  attr_reader :changelog_locator
58
58
 
59
+ def build_git_diff_url
60
+ return nil unless git_version_changed?
61
+
62
+ if github_repo
63
+ "https://github.com/#{github_repo}/compare/#{current_git_version}...#{updated_git_version}"
64
+ elsif gitlab_repo
65
+ "https://gitlab.com/os85/httpx/-/compare/#{current_git_version}...#{updated_git_version}"
66
+ elsif bitbucket_cloud_repo
67
+ "https://bitbucket.org/#{bitbucket_cloud_repo}/branches/compare/#{updated_git_version}..#{current_git_version}"
68
+ end
69
+ end
70
+
59
71
  def github_repo
60
72
  return nil unless updated_git_version
61
73
 
62
74
  git_source_uri.to_s[%r{^(?:git@github.com:|https://github.com/)([^/]+/[^/]+?)(:?\.git)?(?:$|/)}i, 1]
63
75
  end
76
+
77
+ def gitlab_repo
78
+ return nil unless updated_git_version
79
+
80
+ git_source_uri.to_s[%r{^(?:git@gitlab.com:|https://gitlab.com/)([^/]+/[^/]+?)(:?\.git)?(?:$|/)}i, 1]
81
+ end
82
+
83
+ def bitbucket_cloud_repo
84
+ return nil unless updated_git_version
85
+
86
+ git_source_uri.to_s[%r{(?:@|://)bitbucket.org[:/]([^/]+/[^/]+?)(:?\.git)?(?:$|/)}i, 1]
87
+ end
64
88
  end
65
89
  end
@@ -8,11 +8,12 @@ require "set"
8
8
  module BundleUpdateInteractive
9
9
  class Report
10
10
  class << self
11
- def generate
11
+ def generate(groups: [])
12
12
  gemfile = Gemfile.parse
13
13
  current_lockfile = Lockfile.parse
14
- updated_lockfile = Lockfile.parse(BundlerCommands.read_updated_lockfile)
14
+ gems = groups.any? ? current_lockfile.gems_exclusively_installed_by(gemfile: gemfile, groups: groups) : nil
15
15
 
16
+ updated_lockfile = gems&.none? ? nil : Lockfile.parse(BundlerCommands.read_updated_lockfile(*Array(gems)))
16
17
  new(gemfile: gemfile, current_lockfile: current_lockfile, updated_lockfile: updated_lockfile)
17
18
  end
18
19
  end
@@ -21,9 +22,9 @@ module BundleUpdateInteractive
21
22
 
22
23
  def initialize(gemfile:, current_lockfile:, updated_lockfile:)
23
24
  @current_lockfile = current_lockfile
24
- @outdated_gems ||= current_lockfile.entries.each_with_object({}) do |current_lockfile_entry, hash|
25
+ @outdated_gems = current_lockfile.entries.each_with_object({}) do |current_lockfile_entry, hash|
25
26
  name = current_lockfile_entry.name
26
- updated_lockfile_entry = updated_lockfile[name]
27
+ updated_lockfile_entry = updated_lockfile && updated_lockfile[name]
27
28
  next unless current_lockfile_entry.older_than?(updated_lockfile_entry)
28
29
 
29
30
  hash[name] = build_outdated_gem(current_lockfile_entry, updated_lockfile_entry, gemfile[name]&.groups)
@@ -36,7 +37,7 @@ module BundleUpdateInteractive
36
37
 
37
38
  def updateable_gems
38
39
  @updateable_gems ||= outdated_gems.reject do |name, _|
39
- current_lockfile[name].exact_dependency?
40
+ current_lockfile[name].exact_requirement?
40
41
  end.freeze
41
42
  end
42
43
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BundleUpdateInteractive
4
- VERSION = "0.2.1"
4
+ VERSION = "0.4.0"
5
5
  end
@@ -6,6 +6,7 @@ module BundleUpdateInteractive
6
6
  autoload :BundlerCommands, "bundle_update_interactive/bundler_commands"
7
7
  autoload :ChangelogLocator, "bundle_update_interactive/changelog_locator"
8
8
  autoload :CLI, "bundle_update_interactive/cli"
9
+ autoload :Error, "bundle_update_interactive/error"
9
10
  autoload :Gemfile, "bundle_update_interactive/gemfile"
10
11
  autoload :Lockfile, "bundle_update_interactive/lockfile"
11
12
  autoload :LockfileEntry, "bundle_update_interactive/lockfile_entry"
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.2.1
4
+ version: 0.4.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-07-17 00:00:00.000000000 Z
11
+ date: 2024-07-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -66,20 +66,6 @@ dependencies:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: 0.8.0
69
- - !ruby/object:Gem::Dependency
70
- name: thor
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - "~>"
74
- - !ruby/object:Gem::Version
75
- version: '1.2'
76
- type: :runtime
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - "~>"
81
- - !ruby/object:Gem::Version
82
- version: '1.2'
83
69
  - !ruby/object:Gem::Dependency
84
70
  name: tty-prompt
85
71
  requirement: !ruby/object:Gem::Requirement
@@ -126,9 +112,10 @@ files:
126
112
  - lib/bundle_update_interactive/changelog_locator.rb
127
113
  - lib/bundle_update_interactive/cli.rb
128
114
  - lib/bundle_update_interactive/cli/multi_select.rb
115
+ - lib/bundle_update_interactive/cli/options.rb
129
116
  - lib/bundle_update_interactive/cli/row.rb
130
117
  - lib/bundle_update_interactive/cli/table.rb
131
- - lib/bundle_update_interactive/cli/thor_ext.rb
118
+ - lib/bundle_update_interactive/error.rb
132
119
  - lib/bundle_update_interactive/gemfile.rb
133
120
  - lib/bundle_update_interactive/lockfile.rb
134
121
  - lib/bundle_update_interactive/lockfile_entry.rb
@@ -160,7 +147,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
160
147
  - !ruby/object:Gem::Version
161
148
  version: '0'
162
149
  requirements: []
163
- rubygems_version: 3.5.11
150
+ rubygems_version: 3.5.16
164
151
  signing_key:
165
152
  specification_version: 4
166
153
  summary: Adds an update-interactive command to Bundler
@@ -1,76 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "bundler"
4
-
5
- class BundleUpdateInteractive::CLI
6
- module ThorExt
7
- # Configures Thor to behave more like a typical CLI, with better help and error handling.
8
- #
9
- # - Passing -h or --help to a command will show help for that command.
10
- # - Unrecognized options will be treated as errors (instead of being silently ignored).
11
- # - Error messages will be printed in red to stderr, without stack trace.
12
- # - Full stack traces can be enabled by setting the VERBOSE environment variable.
13
- # - Errors will cause Thor to exit with a non-zero status.
14
- #
15
- # To take advantage of this behavior, your CLI should subclass Thor and extend this module.
16
- #
17
- # class CLI < Thor
18
- # extend ThorExt::Start
19
- # end
20
- #
21
- # Start your CLI with:
22
- #
23
- # CLI.start
24
- #
25
- # In tests, prevent Kernel.exit from being called when an error occurs, like this:
26
- #
27
- # CLI.start(args, exit_on_failure: false)
28
- #
29
- module Start
30
- def self.extended(base)
31
- super
32
- base.check_unknown_options!
33
- end
34
-
35
- def start(given_args=ARGV, config={})
36
- config[:shell] ||= Thor::Base.shell.new
37
- handle_help_switches(given_args) do |args|
38
- dispatch(nil, args, nil, config)
39
- end
40
- rescue Exception => e # rubocop:disable Lint/RescueException
41
- handle_exception_on_start(e, config)
42
- end
43
-
44
- private
45
-
46
- def handle_help_switches(given_args)
47
- yield(given_args.dup)
48
- rescue Thor::UnknownArgumentError => e
49
- retry_with_args = []
50
-
51
- if given_args.first == "help"
52
- retry_with_args = ["help"] if given_args.length > 1
53
- elsif (e.unknown & %w[-h --help]).any?
54
- retry_with_args = ["help", (given_args - e.unknown).first]
55
- end
56
- raise unless retry_with_args.any?
57
-
58
- yield(retry_with_args)
59
- end
60
-
61
- def handle_exception_on_start(error, config)
62
- case error
63
- when Errno::EPIPE
64
- # Ignore
65
- when Thor::Error, Interrupt, Bundler::Dsl::DSLError
66
- raise unless config.fetch(:exit_on_failure, true)
67
-
68
- config[:shell]&.say_error(error.message, :red)
69
- exit(false)
70
- else
71
- raise
72
- end
73
- end
74
- end
75
- end
76
- end