bundle_update_interactive 0.2.1 → 0.4.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: 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