bundleup 1.3.0 → 2.1.3

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: db5d58970165c2381d963205eae11b610d57f35e9654bee2edff54f2d1ff861f
4
- data.tar.gz: 508d1cba2d3ab7c2167b60ec38fd1bf49e0252f614e07fe7cce3ed36de3dc32e
3
+ metadata.gz: f8a962e269c6e12d142a5e7e464044acdf1ffd80e4a75939fc6f5be7a2a19ad1
4
+ data.tar.gz: 86692adf1c57156cf441e2dc7b8aba2fe9f424fe1e93c3abb075e1db7b38a22e
5
5
  SHA512:
6
- metadata.gz: c8133402453232bfd33b17a9c3c5f903874f92f190063bc84e7f29723514ab5333d791d0ac2e6eb4c8deb75a5d5f734a20cb47d63101a79ff879b8e566c63952
7
- data.tar.gz: c508a0e3360b51d537c948e1cd95790fd69ad01d43d86a97976729fea4737a84aa25fedd69513f379c26ba82c0abcea3f71bc3f25ad1e802845b24c81f3deb9e
6
+ metadata.gz: c288f7fdcc1b441a1bd027303ef0270870d6a36be2272d0c4e4984f04612a1d69a847ae73e47db9dba4fd6eba326c5d44cb7b256a860221cb975dc04a7ae283b
7
+ data.tar.gz: 7891c266cc969b16a66e1ab5ef245fbf6119218972e4ed94317bc2488e914863176e49eba0ce04aea634df1be51c827f4d9357ddad9be1fb5a6f63ae2b09e09f
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2020 Matt Brictson
3
+ Copyright (c) 2021 Matt Brictson
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # bundleup
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/bundleup.svg)](http://badge.fury.io/rb/bundleup)
4
- [![Build Status](https://travis-ci.org/mattbrictson/bundleup.svg?branch=main)](https://travis-ci.org/mattbrictson/bundleup)
4
+ [![Build Status](https://circleci.com/gh/mattbrictson/bundleup/tree/main.svg?style=shield)](https://app.circleci.com/pipelines/github/mattbrictson/bundleup?branch=main)
5
5
 
6
6
  **Run `bundleup` on a Ruby project containing a Gemfile to see what gem dependencies need updating.** It is a friendlier command-line interface to [Bundler’s][bundler] `bundle update` and `bundle outdated`.
7
7
 
@@ -14,7 +14,7 @@ You might like bundleup because it:
14
14
 
15
15
  Here it is in action:
16
16
 
17
- <img src="https://raw.github.com/mattbrictson/bundleup/main/sample.png" width="599" height="553" alt="Sample output">
17
+ <img src="./demo.gif" width="682" height="351" alt="Sample output">
18
18
 
19
19
  ## Requirements
20
20
 
@@ -44,7 +44,37 @@ Protip: Any extra command-line arguments will be passed along to `bundle update`
44
44
  bundleup --group=development
45
45
  ```
46
46
 
47
- ## How it works
47
+ ### Experimental: `--update-gemfile`
48
+
49
+ > 💡 This is an experimental feature that may be removed or changed in future versions.
50
+
51
+ Normally bundleup 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 `--update-gemfile` flag, bundleup can update the version pins in your Gemfile as well. Consider the following Gemfile:
52
+
53
+ ```ruby
54
+ gem 'sidekiq', '~> 5.2'
55
+ gem 'rubocop', '0.89.0'
56
+ ```
57
+
58
+ Normally running `bundleup` will report that these gems are pinned and therefore cannot be updated to the latest versions. However, if you pass the `--update-gemfile` option like this:
59
+
60
+ ```
61
+ $ bundleup --update-gemfile
62
+ ```
63
+
64
+ Now bundleup will automatically edit your Gemfile pins as needed to bring those gems up to date. For example, bundleup would change the Gemfile to look like this:
65
+
66
+ ```ruby
67
+ gem 'sidekiq', '~> 6.1'
68
+ gem 'rubocop', '0.90.0'
69
+ ```
70
+
71
+ Note that `--update-gemfile` will _not_ modify Gemfile entries that contain a comment, like this:
72
+
73
+ ```ruby
74
+ gem 'sidekiq', '~> 5.2' # our monkey patch doesn't work on 6.0+
75
+ ```
76
+
77
+ ## How bundleup works
48
78
 
49
79
  bundleup starts by making a backup copy of your Gemfile.lock. Next it runs `bundle check` (and `bundle install` if any gems are missing in your local environment), `bundle list`, then `bundle update` and `bundle list` again to find what gems versions are being used before and after Bundler does its updating magic. (Since gems are actually being installed into your Ruby environment during these steps, the process may take a few moments to complete, especially if gems with native extensions need to be compiled.)
50
80
 
@@ -54,7 +84,7 @@ After displaying its findings, bundleup gives you the option of keeping the chan
54
84
 
55
85
  ## Roadmap
56
86
 
57
- bundleup is a very simple script at this point, but it could be more. Some possibilities:
87
+ bundleup is very simple at this point, but it could be more. Some possibilities:
58
88
 
59
89
  - Automatically commit the Gemfile.lock changes with a nice commit message
60
90
  - Integrate with bundler-audit to mark upgrades that have important security fixes
data/exe/bundleup CHANGED
@@ -1,4 +1,10 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require "bundleup"
4
- Bundleup::CLI.new.run
4
+
5
+ begin
6
+ Bundleup::CLI.new(ARGV).run
7
+ rescue Bundleup::CLI::Error => e
8
+ Bundleup.logger.error(e.message)
9
+ exit(false)
10
+ end
data/lib/bundleup.rb CHANGED
@@ -1,8 +1,22 @@
1
1
  require "bundleup/version"
2
- require "bundleup/console"
3
- require "bundleup/bundle_commands"
4
- require "bundleup/gem_status"
5
- require "bundleup/gemfile"
6
- require "bundleup/outdated_parser"
7
- require "bundleup/upgrade"
2
+ require "bundleup/backup"
3
+ require "bundleup/colors"
8
4
  require "bundleup/cli"
5
+ require "bundleup/commands"
6
+ require "bundleup/gemfile"
7
+ require "bundleup/logger"
8
+ require "bundleup/report"
9
+ require "bundleup/shell"
10
+ require "bundleup/pin_report"
11
+ require "bundleup/update_report"
12
+ require "bundleup/version_spec"
13
+
14
+ module Bundleup
15
+ class << self
16
+ attr_accessor :commands, :logger, :shell
17
+ end
18
+ end
19
+
20
+ Bundleup.commands = Bundleup::Commands.new
21
+ Bundleup.logger = Bundleup::Logger.new
22
+ Bundleup.shell = Bundleup::Shell.new
@@ -0,0 +1,29 @@
1
+ module Bundleup
2
+ class Backup
3
+ def self.restore_on_error(*paths)
4
+ backup = new(*paths)
5
+ begin
6
+ yield(backup)
7
+ rescue StandardError, Interrupt
8
+ backup.restore
9
+ raise
10
+ end
11
+ end
12
+
13
+ def initialize(*paths)
14
+ @original_contents = paths.each_with_object({}) do |path, hash|
15
+ hash[path] = IO.read(path)
16
+ end
17
+ end
18
+
19
+ def restore
20
+ original_contents.each do |path, contents|
21
+ IO.write(path, contents)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :original_contents
28
+ end
29
+ end
data/lib/bundleup/cli.rb CHANGED
@@ -1,86 +1,148 @@
1
+ require "forwardable"
2
+
1
3
  module Bundleup
2
4
  class CLI
3
- include Console
4
-
5
- def run
6
- puts \
7
- "Please wait a moment while I upgrade your Gemfile.lock..."
8
-
9
- committed = false
10
- review_upgrades
11
- review_pins
12
- committed = upgrades.any? && confirm_commit
13
- puts "Done!" if committed
14
- ensure
15
- restore_lockfile unless committed
16
- end
5
+ Error = Class.new(StandardError)
17
6
 
18
- private
7
+ include Colors
8
+ extend Forwardable
9
+ def_delegators :Bundleup, :commands, :logger
19
10
 
20
- def review_upgrades
21
- if upgrades.any?
22
- puts "\nThe following gem(s) will be updated:\n\n"
23
- print_upgrades_table
24
- else
25
- ok("Nothing to update.")
11
+ def initialize(args)
12
+ @args = args.dup
13
+ @update_gemfile = @args.delete("--update-gemfile")
14
+ end
15
+
16
+ def run # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
17
+ print_usage && return if (args & %w[-h --help]).any?
18
+
19
+ assert_gemfile_and_lock_exist!
20
+
21
+ logger.puts "Please wait a moment while I upgrade your Gemfile.lock..."
22
+ Backup.restore_on_error("Gemfile", "Gemfile.lock") do |backup|
23
+ perform_analysis_and_optionally_bump_gemfile_versions do |update_report, pin_report|
24
+ if update_report.empty?
25
+ logger.ok "Nothing to update."
26
+ logger.puts "\n#{pin_report}" unless pin_report.empty?
27
+ break
28
+ end
29
+
30
+ logger.puts
31
+ logger.puts update_report
32
+ logger.puts pin_report unless pin_report.empty?
33
+
34
+ if logger.confirm?("Do you want to apply these changes?")
35
+ logger.ok "Done!"
36
+ else
37
+ backup.restore
38
+ logger.puts "Your original Gemfile.lock has been restored."
39
+ end
40
+ end
26
41
  end
27
42
  end
28
43
 
29
- def review_pins
30
- return if pins.empty?
44
+ private
31
45
 
32
- puts "\nNote that the following gem(s) are being held back:\n\n"
33
- print_pins_table
34
- end
46
+ attr_reader :args
35
47
 
36
- def confirm_commit
37
- confirm("\nDo you want to apply these changes?")
38
- end
48
+ def print_usage # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
49
+ logger.puts(<<~USAGE.gsub(/^/, " "))
39
50
 
40
- def restore_lockfile
41
- return unless defined?(@upgrade)
42
- return unless upgrade.lockfile_changed?
51
+ Usage: #{green('bundleup')} #{yellow('[GEMS...] [OPTIONS]')}
43
52
 
44
- upgrade.undo
45
- puts "Your original Gemfile.lock has been restored."
46
- end
53
+ Use #{blue('bundleup')} in place of #{blue('bundle update')} to interactively update your project
54
+ Gemfile.lock to the latest gem versions. Bundleup will show what gems will
55
+ be updated, color-code them based on semver, and ask you to confirm the
56
+ updates before finalizing them. For example:
47
57
 
48
- def upgrade
49
- @upgrade ||= Upgrade.new(ARGV)
50
- end
58
+ The following gems will be updated:
51
59
 
52
- def upgrades
53
- upgrade.upgrades
54
- end
60
+ #{yellow('bundler-audit 0.6.1 → 0.7.0.1')}
61
+ i18n 1.8.2 → 1.8.5
62
+ #{red('json 2.2.0 → (removed)')}
63
+ parser 2.7.1.1 → 2.7.1.4
64
+ #{red('rails e063bef → 57a4ead')}
65
+ #{blue('rubocop-ast (new) → 0.3.0')}
66
+ #{red('thor 0.20.3 → 1.0.1')}
67
+ #{yellow('zeitwerk 2.3.0 → 2.4.0')}
68
+
69
+ #{yellow('Do you want to apply these changes [Yn]?')}
70
+
71
+ Bundleup will also let you know if there are gems that can't be updated
72
+ because they are pinned in the Gemfile. Any relevant comments from the
73
+ Gemfile will also be included, explaining the pins:
74
+
75
+ Note that the following gems are being held back:
55
76
 
56
- def pins
57
- upgrade.pins
77
+ rake 12.3.3 → 13.0.1 : pinned at ~> 12.0 #{gray('# Not ready for 13 yet')}
78
+ rubocop 0.89.0 → 0.89.1 : pinned at = 0.89.0
79
+
80
+ You may optionally specify one or more #{yellow('GEMS')} or pass #{yellow('OPTIONS')} to bundleup;
81
+ these will be passed through to bundler. See #{blue('bundle update --help')} for the
82
+ full list of the options that bundler supports.
83
+
84
+ Finally, bundleup also supports an experimental #{yellow('--update-gemfile')} option.
85
+ If specified, bundleup with modify the version restrictions specified in
86
+ your Gemfile so that it can install the latest version of each gem. For
87
+ instance, if your Gemfile specifies #{yellow('gem "sidekiq", "~> 5.2"')} but an update
88
+ to version 6.1.2 is available, bundleup will modify the Gemfile entry to
89
+ be #{yellow('gem "sidekiq", "~> 6.1"')} in order to permit the update.
90
+
91
+ Examples:
92
+
93
+ #{gray('# Update all gems')}
94
+ #{blue('$ bundleup')}
95
+
96
+ #{gray('# Only update gems in the development group')}
97
+ #{blue('$ bundleup --group=development')}
98
+
99
+ #{gray('# Only update the rake gem')}
100
+ #{blue('$ bundleup rake')}
101
+
102
+ #{gray('# Experimental: modify Gemfile to allow the latest gem versions')}
103
+ #{blue('$ bundleup --update-gemfile')}
104
+
105
+ USAGE
106
+ true
58
107
  end
59
108
 
60
- def print_upgrades_table
61
- rows = tableize(upgrades) do |g|
62
- [g.name, g.old_version || "(new)", "→", g.new_version || "(removed)"]
63
- end
64
- upgrades.zip(rows).each do |g, row|
65
- puts color(g.color, row)
66
- end
109
+ def assert_gemfile_and_lock_exist!
110
+ return if File.exist?("Gemfile") && File.exist?("Gemfile.lock")
111
+
112
+ raise Error, "Gemfile and Gemfile.lock must both be present."
67
113
  end
68
114
 
69
- def print_pins_table
70
- rows = tableize(pins) do |g|
71
- [g.name, g.new_version, "", g.newest_version, *pin_reason(g)]
115
+ def perform_analysis_and_optionally_bump_gemfile_versions # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
116
+ gemfile = Gemfile.new
117
+ lockfile_backup = Backup.new("Gemfile.lock")
118
+ update_report, pin_report, _, outdated_gems = perform_analysis
119
+ updatable_gems = gemfile.gem_pins_without_comments.slice(*outdated_gems.keys)
120
+
121
+ if updatable_gems.any? && @update_gemfile
122
+ lockfile_backup.restore
123
+ orig_gemfile = Gemfile.new
124
+ gemfile.relax_gem_pins!(updatable_gems.keys)
125
+ update_report, pin_report, new_versions, = perform_analysis
126
+ orig_gemfile.shift_gem_pins!(new_versions.slice(*updatable_gems.keys))
127
+ commands.install
72
128
  end
73
- puts rows.join("\n")
74
- end
75
129
 
76
- def pin_reason(gem)
77
- notes = color(:gray, gemfile.gem_comment(gem.name))
78
- pin_operator, pin_version = gem.pin.split(" ", 2)
79
- [":", "pinned at", pin_operator.rjust(2), pin_version, notes]
130
+ logger.clear_line
131
+ yield(update_report, pin_report)
80
132
  end
81
133
 
82
- def gemfile
83
- @gemfile ||= Gemfile.new
134
+ def perform_analysis # rubocop:disable Metrics/AbcSize
135
+ gem_comments = Gemfile.new.gem_comments
136
+ commands.check? || commands.install
137
+ old_versions = commands.list
138
+ commands.update(args)
139
+ new_versions = commands.list
140
+ outdated_gems = commands.outdated
141
+
142
+ update_report = UpdateReport.new(old_versions: old_versions, new_versions: new_versions)
143
+ pin_report = PinReport.new(gem_versions: new_versions, outdated_gems: outdated_gems, gem_comments: gem_comments)
144
+
145
+ [update_report, pin_report, new_versions, outdated_gems]
84
146
  end
85
147
  end
86
148
  end
@@ -0,0 +1,56 @@
1
+ module Bundleup
2
+ module Colors
3
+ ANSI_CODES = {
4
+ red: 31,
5
+ green: 32,
6
+ yellow: 33,
7
+ blue: 34,
8
+ gray: 90
9
+ }.freeze
10
+ private_constant :ANSI_CODES
11
+
12
+ class << self
13
+ attr_writer :enabled
14
+
15
+ def enabled?
16
+ return @enabled if defined?(@enabled)
17
+
18
+ @enabled = determine_color_support
19
+ end
20
+
21
+ private
22
+
23
+ def determine_color_support
24
+ if ENV["CLICOLOR_FORCE"] == "1"
25
+ true
26
+ elsif ENV["TERM"] == "dumb"
27
+ false
28
+ else
29
+ tty?($stdout) && tty?($stderr)
30
+ end
31
+ end
32
+
33
+ def tty?(io)
34
+ io.respond_to?(:tty?) && io.tty?
35
+ end
36
+ end
37
+
38
+ module_function
39
+
40
+ def plain(str)
41
+ str
42
+ end
43
+
44
+ def strip(str)
45
+ str.gsub(/\033\[[0-9;]*m/, "")
46
+ end
47
+
48
+ ANSI_CODES.each do |name, code|
49
+ define_method(name) do |str|
50
+ return str if str.to_s.empty?
51
+
52
+ Colors.enabled? ? "\e[0;#{code};49m#{str}\e[0m" : str
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,40 @@
1
+ require "forwardable"
2
+
3
+ module Bundleup
4
+ class Commands
5
+ GEMFILE_ENTRY_REGEXP = /\* (\S+) \((\S+)(?: (\S+))?\)/.freeze
6
+ OUTDATED_2_1_REGEXP = /\* (\S+) \(newest (\S+),.* requested (.*)\)/.freeze
7
+ OUTDATED_2_2_REGEXP = /^(\S+)\s\s+\S+\s\s+(\d\S+)\s\s+(\S.*?)(?:$|\s\s)/.freeze
8
+
9
+ extend Forwardable
10
+ def_delegators :Bundleup, :shell
11
+
12
+ def check?
13
+ shell.run?(%w[bundle check])
14
+ end
15
+
16
+ def install
17
+ shell.run(%w[bundle install])
18
+ end
19
+
20
+ def list
21
+ output = shell.capture(%w[bundle list])
22
+ output.scan(GEMFILE_ENTRY_REGEXP).each_with_object({}) do |(name, ver, sha), gems|
23
+ gems[name] = sha || ver
24
+ end
25
+ end
26
+
27
+ def outdated
28
+ output = shell.capture(%w[bundle outdated], raise_on_error: false)
29
+ expr = output.match?(/^Gem\s+Current\s+Latest/) ? OUTDATED_2_2_REGEXP : OUTDATED_2_1_REGEXP
30
+
31
+ output.scan(expr).each_with_object({}) do |(name, newest, pin), gems|
32
+ gems[name] = { newest: newest, pin: pin }
33
+ end
34
+ end
35
+
36
+ def update(args=[])
37
+ shell.run(%w[bundle update] + args)
38
+ end
39
+ end
40
+ end
@@ -1,17 +1,59 @@
1
1
  module Bundleup
2
2
  class Gemfile
3
+ attr_reader :path
4
+
3
5
  def initialize(path="Gemfile")
6
+ @path = path
4
7
  @contents = IO.read(path)
5
8
  end
6
9
 
7
- def gem_comment(gem_name)
8
- inline_comment(gem_name) || prefix_comment(gem_name)
10
+ def gem_comments
11
+ gem_names.each_with_object({}) do |gem_name, hash|
12
+ comment = inline_comment(gem_name) || prefix_comment(gem_name)
13
+ hash[gem_name] = comment unless comment.nil?
14
+ end
15
+ end
16
+
17
+ def gem_pins_without_comments
18
+ (gem_names - gem_comments.keys).each_with_object({}) do |gem_name, hash|
19
+ next unless (match = gem_declaration_with_pinned_version_re(gem_name).match(contents))
20
+
21
+ version = match[1]
22
+ hash[gem_name] = VersionSpec.parse(version)
23
+ end
24
+ end
25
+
26
+ def relax_gem_pins!(gem_names)
27
+ gem_names.each do |gem_name|
28
+ rewrite_gem_version!(gem_name, &:relax)
29
+ end
30
+ end
31
+
32
+ def shift_gem_pins!(new_gem_versions)
33
+ new_gem_versions.each do |gem_name, new_version|
34
+ rewrite_gem_version!(gem_name) { |version_spec| version_spec.shift(new_version) }
35
+ end
9
36
  end
10
37
 
11
38
  private
12
39
 
40
+ def rewrite_gem_version!(gem_name)
41
+ found = contents.sub!(gem_declaration_with_pinned_version_re(gem_name)) do |match|
42
+ version = Regexp.last_match[1]
43
+ match[Regexp.last_match.regexp, 1] = yield(VersionSpec.parse(version)).to_s
44
+ match
45
+ end
46
+ raise "Can't rewrite version for #{gem_name}; it does not have a pin" unless found
47
+
48
+ IO.write(path, contents)
49
+ end
50
+
13
51
  attr_reader :contents
14
52
 
53
+ def gem_names
54
+ contents.scan(/^\s*gem\s+["'](.+?)["']/).flatten.uniq
55
+ end
56
+
15
57
  def inline_comment(gem_name)
16
58
  contents[/#{gem_declaration_re(gem_name)}.*(#\s*\S+.*)/, 1]
17
59
  end
@@ -23,5 +65,9 @@ module Bundleup
23
65
  def gem_declaration_re(gem_name)
24
66
  /^\s*gem\s+["']#{Regexp.escape(gem_name)}["']/
25
67
  end
68
+
69
+ def gem_declaration_with_pinned_version_re(gem_name)
70
+ /#{gem_declaration_re(gem_name)},\s*["']([^'"]+)["']\s*$/
71
+ end
26
72
  end
27
73
  end
@@ -0,0 +1,64 @@
1
+ require "io/console"
2
+
3
+ module Bundleup
4
+ class Logger
5
+ extend Forwardable
6
+ def_delegators :@stdout, :print, :puts, :tty?
7
+ def_delegators :@stdin, :gets
8
+
9
+ def initialize(stdin: $stdin, stdout: $stdout, stderr: $stderr)
10
+ @stdin = stdin
11
+ @stdout = stdout
12
+ @stderr = stderr
13
+ @spinner = %w[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏].cycle
14
+ end
15
+
16
+ def ok(message)
17
+ puts Colors.green("✔ #{message}")
18
+ end
19
+
20
+ def error(message)
21
+ stderr.puts Colors.red("ERROR: #{message}")
22
+ end
23
+
24
+ def attention(message)
25
+ puts Colors.yellow(message)
26
+ end
27
+
28
+ def confirm?(question)
29
+ print Colors.yellow(question.sub(/\??\z/, " [Yn]? "))
30
+ gets =~ /^($|y)/i
31
+ end
32
+
33
+ def clear_line
34
+ print "\r".ljust(console_width - 1)
35
+ print "\r"
36
+ end
37
+
38
+ def while_spinning(message, &block)
39
+ thread = Thread.new(&block)
40
+ thread.report_on_exception = false
41
+ message = message.ljust(console_width - 2)
42
+ print "\r#{Colors.blue([spinner.next, message].join(' '))}" until wait_for_exit(thread, 0.1)
43
+ thread.value
44
+ end
45
+
46
+ private
47
+
48
+ attr_reader :spinner, :stderr
49
+
50
+ def console_width
51
+ width = IO.console.winsize.last if tty?
52
+ width.to_i.positive? ? width : 80
53
+ end
54
+
55
+ def wait_for_exit(thread, seconds)
56
+ thread.join(seconds)
57
+ rescue StandardError
58
+ # Sanity check. If we get an exception, the thread should be dead.
59
+ raise if thread.alive?
60
+
61
+ thread
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,37 @@
1
+ module Bundleup
2
+ class PinReport < Report
3
+ def initialize(gem_versions:, outdated_gems:, gem_comments:)
4
+ super()
5
+ @gem_versions = gem_versions
6
+ @outdated_gems = outdated_gems
7
+ @gem_comments = gem_comments
8
+ end
9
+
10
+ def title
11
+ return "Note that this gem is being held back:" if rows.count == 1
12
+
13
+ "Note that the following gems are being held back:"
14
+ end
15
+
16
+ def rows
17
+ outdated_gems.keys.sort.map do |gem|
18
+ meta = outdated_gems[gem]
19
+ current_version = gem_versions[gem]
20
+ newest_version = meta[:newest]
21
+ pin = meta[:pin]
22
+
23
+ [gem, current_version, "→", newest_version, *pin_reason(gem, pin)]
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :gem_versions, :outdated_gems, :gem_comments
30
+
31
+ def pin_reason(gem, pin)
32
+ notes = Colors.gray(gem_comments[gem].to_s)
33
+ pin_operator, pin_version = pin.split(" ", 2)
34
+ [":", "pinned at", pin_operator.rjust(2), pin_version, notes]
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,38 @@
1
+ require "forwardable"
2
+
3
+ module Bundleup
4
+ class Report
5
+ extend Forwardable
6
+ def_delegators :rows, :empty?, :one?
7
+
8
+ def many?
9
+ rows.length > 1
10
+ end
11
+
12
+ def to_s
13
+ [
14
+ title,
15
+ tableize(rows).map { |row| row.join(" ").rstrip }.join("\n"),
16
+ ""
17
+ ].join("\n\n")
18
+ end
19
+
20
+ private
21
+
22
+ def tableize(rows)
23
+ widths = max_length_of_each_column(rows)
24
+ rows.map do |row|
25
+ row.zip(widths).map do |value, width|
26
+ padding = " " * (width - Colors.strip(value).length)
27
+ "#{value}#{padding}"
28
+ end
29
+ end
30
+ end
31
+
32
+ def max_length_of_each_column(rows)
33
+ Array.new(rows.first.count) do |i|
34
+ rows.map { |values| Colors.strip(values[i]).length }.max
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,35 @@
1
+ require "forwardable"
2
+ require "open3"
3
+
4
+ module Bundleup
5
+ class Shell
6
+ extend Forwardable
7
+ def_delegators :Bundleup, :logger
8
+
9
+ def capture(command, raise_on_error: true)
10
+ stdout, stderr, status = capture3(command)
11
+ raise ["Failed to execute: #{command}", stdout, stderr].compact.join("\n") if raise_on_error && !status.success?
12
+
13
+ stdout
14
+ end
15
+
16
+ def run(command)
17
+ capture(command)
18
+ true
19
+ end
20
+
21
+ def run?(command)
22
+ _, _, status = capture3(command)
23
+ status.success?
24
+ end
25
+
26
+ private
27
+
28
+ def capture3(command)
29
+ command = Array(command)
30
+ logger.while_spinning("running: #{command.join(' ')}") do
31
+ Open3.capture3(*command)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,63 @@
1
+ module Bundleup
2
+ class UpdateReport < Report
3
+ def initialize(old_versions:, new_versions:)
4
+ super()
5
+ @old_versions = old_versions
6
+ @new_versions = new_versions
7
+ end
8
+
9
+ def title
10
+ return "This gem will be updated:" if rows.count == 1
11
+
12
+ "The following gems will be updated:"
13
+ end
14
+
15
+ def rows
16
+ gem_names.each_with_object([]) do |gem, rows|
17
+ old = old_versions[gem]
18
+ new = new_versions[gem]
19
+ next if old == new
20
+
21
+ row = [gem, old || "(new)", "→", new || "(removed)"]
22
+
23
+ color = color_for_gem(gem)
24
+ rows << row.map { |col| Colors.public_send(color, col) }
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :old_versions, :new_versions
31
+
32
+ def gem_names
33
+ (old_versions.keys | new_versions.keys).sort
34
+ end
35
+
36
+ def color_for_gem(gem)
37
+ old_version = old_versions[gem]
38
+ new_version = new_versions[gem]
39
+
40
+ return :blue if old_version.nil?
41
+ return :red if new_version.nil? || major_upgrade?(old_version, new_version)
42
+ return :yellow if minor_upgrade?(old_version, new_version)
43
+
44
+ :plain
45
+ end
46
+
47
+ def major_upgrade?(old_version, new_version)
48
+ major(new_version) != major(old_version)
49
+ end
50
+
51
+ def minor_upgrade?(old_version, new_version)
52
+ minor(new_version) != minor(old_version)
53
+ end
54
+
55
+ def major(version)
56
+ version.split(".", 2)[0]
57
+ end
58
+
59
+ def minor(version)
60
+ version.split(".", 3)[1]
61
+ end
62
+ end
63
+ end
@@ -1,3 +1,3 @@
1
1
  module Bundleup
2
- VERSION = "1.3.0".freeze
2
+ VERSION = "2.1.3".freeze
3
3
  end
@@ -0,0 +1,54 @@
1
+ module Bundleup
2
+ class VersionSpec
3
+ def self.parse(version)
4
+ return version if version.is_a?(VersionSpec)
5
+
6
+ version = version.strip
7
+ _, operator, number = version.match(/^([^\d\s]*)\s*(.+)/).to_a
8
+ operator = nil if operator.empty?
9
+
10
+ new(parts: number.split("."), operator: operator)
11
+ end
12
+
13
+ attr_reader :parts, :operator
14
+
15
+ def initialize(parts:, operator: nil)
16
+ @parts = parts
17
+ @operator = operator
18
+ end
19
+
20
+ def exact?
21
+ operator.nil?
22
+ end
23
+
24
+ def relax
25
+ return self if %w[!= > >=].include?(operator)
26
+ return self.class.parse(">= 0") if %w[< <=].include?(operator)
27
+
28
+ self.class.new(parts: parts, operator: ">=")
29
+ end
30
+
31
+ def shift(new_version) # rubocop:disable Metrics/AbcSize
32
+ return self.class.parse(new_version) if exact?
33
+ return self if Gem::Requirement.new(to_s).satisfied_by?(Gem::Version.new(new_version))
34
+ return self.class.new(parts: self.class.parse(new_version).parts, operator: "<=") if %w[< <=].include?(operator)
35
+
36
+ new_slice = self.class.parse(new_version).slice(parts.length)
37
+ self.class.new(parts: new_slice.parts, operator: "~>")
38
+ end
39
+
40
+ def slice(amount)
41
+ self.class.new(parts: parts[0, amount], operator: operator)
42
+ end
43
+
44
+ def to_s
45
+ [operator, parts.join(".")].compact.join(" ")
46
+ end
47
+
48
+ def ==(other)
49
+ return false unless other.is_a?(VersionSpec)
50
+
51
+ to_s == other.to_s
52
+ end
53
+ end
54
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bundleup
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 2.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Brictson
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-08-08 00:00:00.000000000 Z
11
+ date: 2021-04-04 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Use `bundleup` whenever you want to update the locked Gemfile dependencies
14
14
  of a Ruby project. It shows exactly what gems will be updated with color output
@@ -27,14 +27,18 @@ files:
27
27
  - README.md
28
28
  - exe/bundleup
29
29
  - lib/bundleup.rb
30
- - lib/bundleup/bundle_commands.rb
30
+ - lib/bundleup/backup.rb
31
31
  - lib/bundleup/cli.rb
32
- - lib/bundleup/console.rb
33
- - lib/bundleup/gem_status.rb
32
+ - lib/bundleup/colors.rb
33
+ - lib/bundleup/commands.rb
34
34
  - lib/bundleup/gemfile.rb
35
- - lib/bundleup/outdated_parser.rb
36
- - lib/bundleup/upgrade.rb
35
+ - lib/bundleup/logger.rb
36
+ - lib/bundleup/pin_report.rb
37
+ - lib/bundleup/report.rb
38
+ - lib/bundleup/shell.rb
39
+ - lib/bundleup/update_report.rb
37
40
  - lib/bundleup/version.rb
41
+ - lib/bundleup/version_spec.rb
38
42
  homepage: https://github.com/mattbrictson/bundleup
39
43
  licenses:
40
44
  - MIT
@@ -58,7 +62,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
58
62
  - !ruby/object:Gem::Version
59
63
  version: '0'
60
64
  requirements: []
61
- rubygems_version: 3.1.4
65
+ rubygems_version: 3.2.15
62
66
  signing_key:
63
67
  specification_version: 4
64
68
  summary: A friendlier command-line interface for Bundler’s `update` and `outdated`
@@ -1,52 +0,0 @@
1
- require "open3"
2
-
3
- module Bundleup
4
- class BundleCommands
5
- class Result
6
- attr_reader :output
7
-
8
- def initialize(output, success)
9
- @output = output
10
- @success = success
11
- end
12
-
13
- def success?
14
- @success
15
- end
16
- end
17
-
18
- include Console
19
-
20
- def check
21
- run(%w[bundle check], fail_silently: true).success?
22
- end
23
-
24
- def install
25
- run(%w[bundle install]).output
26
- end
27
-
28
- def outdated
29
- run(%w[bundle outdated], fail_silently: true).output
30
- end
31
-
32
- def list
33
- run(%w[bundle list]).output
34
- end
35
-
36
- def update(args=[])
37
- run(%w[bundle update] + args).output
38
- end
39
-
40
- private
41
-
42
- def run(cmd, fail_silently: false)
43
- cmd_line = cmd.join(" ")
44
- progress("Running `#{cmd_line}`") do
45
- out, err, status = Open3.capture3(*cmd)
46
- next(Result.new(out, status.success?)) if status.success? || fail_silently
47
-
48
- raise ["Failed to execute: #{cmd_line}", out, err].compact.join("\n")
49
- end
50
- end
51
- end
52
- end
@@ -1,100 +0,0 @@
1
- module Bundleup
2
- module Console
3
- ANSI_CODES = {
4
- red: 31,
5
- green: 32,
6
- yellow: 33,
7
- blue: 34,
8
- gray: 90
9
- }.freeze
10
-
11
- def ok(message)
12
- puts color(:green, "✔ #{message}")
13
- end
14
-
15
- def attention(message)
16
- puts color(:yellow, message)
17
- end
18
-
19
- def color(color_name, message)
20
- code = ANSI_CODES[color_name]
21
- return message if code.nil?
22
-
23
- "\e[0;#{code};49m#{message}\e[0m"
24
- end
25
-
26
- def confirm(question)
27
- print question.sub(/\??\z/, " [Yn]? ")
28
- $stdin.gets =~ /^($|y)/i
29
- end
30
-
31
- # Runs a block in the background and displays a spinner until it completes.
32
- def progress(message, &block)
33
- spinner = %w[/ - \\ |].cycle
34
- print "\e[90m#{message}... \e[0m"
35
- result = observing_thread(block, 0.5, 0.1) do
36
- print "\r\e[90m#{message}... #{spinner.next} \e[0m"
37
- end
38
- puts "\r\e[90m#{message}... OK\e[0m"
39
- result
40
- rescue StandardError
41
- puts "\r\e[90m#{message}...\e[0m \e[31mFAILED\e[0m"
42
- raise
43
- end
44
-
45
- # Given a two-dimensional Array of strings representing a table of data,
46
- # translate each row into a single string by joining the values with
47
- # whitespace such that all the columns are nicely aligned.
48
- #
49
- # If a block is given, map the rows through the block first. These two
50
- # usages are equivalent:
51
- #
52
- # tableize(rows.map(&something))
53
- # tableize(rows, &something)
54
- #
55
- # Returns a one-dimensional Array of strings, each representing a formatted
56
- # row of the resulting table.
57
- #
58
- def tableize(rows, &block)
59
- rows = rows.map(&block) if block
60
- widths = max_length_of_each_column(rows)
61
- rows.map do |row|
62
- row.zip(widths).map { |value, width| value.ljust(width) }.join(" ")
63
- end
64
- end
65
-
66
- private
67
-
68
- def max_length_of_each_column(rows)
69
- Array.new(rows.first.count) do |i|
70
- rows.map { |values| values[i].to_s.length }.max
71
- end
72
- end
73
-
74
- # Starts the `callable` in a background thread and waits for it to complete.
75
- # If the callable fails with an exception, it will be raised here. Otherwise
76
- # the main thread is paused for an `initial_wait` time in seconds, and
77
- # subsequently for `periodic_wait` repeatedly until the thread completes.
78
- # After each wait, `yield` is called to allow a block to execute.
79
- def observing_thread(callable, initial_wait, periodic_wait)
80
- thread = Thread.new(&callable)
81
- thread.report_on_exception = false
82
- wait_for_exit(thread, initial_wait)
83
- loop do
84
- break if wait_for_exit(thread, periodic_wait)
85
-
86
- yield
87
- end
88
- thread.value
89
- end
90
-
91
- def wait_for_exit(thread, seconds)
92
- thread.join(seconds)
93
- rescue StandardError
94
- # Sanity check. If we get an exception, the thread should be dead.
95
- raise if thread.alive?
96
-
97
- thread
98
- end
99
- end
100
- end
@@ -1,59 +0,0 @@
1
- # rubocop:disable Metrics/BlockLength
2
- module Bundleup
3
- GemStatus = Struct.new(:name,
4
- :old_version,
5
- :new_version,
6
- :newest_version,
7
- :pin) do
8
- def pinned?
9
- !pin.nil?
10
- end
11
-
12
- def upgraded?
13
- new_version != old_version
14
- end
15
-
16
- def added?
17
- old_version.nil?
18
- end
19
-
20
- def removed?
21
- new_version.nil?
22
- end
23
-
24
- def color
25
- if major_upgrade? || removed?
26
- :red
27
- elsif minor_upgrade?
28
- :yellow
29
- elsif added?
30
- :blue
31
- else
32
- :plain
33
- end
34
- end
35
-
36
- def major_upgrade?
37
- return false if new_version.nil? || old_version.nil?
38
-
39
- major(new_version) != major(old_version)
40
- end
41
-
42
- def minor_upgrade?
43
- return false if new_version.nil? || old_version.nil?
44
-
45
- !major_upgrade? && minor(new_version) != minor(old_version)
46
- end
47
-
48
- private
49
-
50
- def major(version)
51
- version.split(".", 2)[0]
52
- end
53
-
54
- def minor(version)
55
- version.split(".", 3)[1]
56
- end
57
- end
58
- end
59
- # rubocop:enable Metrics/BlockLength
@@ -1,17 +0,0 @@
1
- module Bundleup
2
- module OutdatedParser
3
- def self.parse(output)
4
- expr = if output.match?(/^Gem\s+Current\s+Latest/)
5
- # Bundler >= 2.2 format
6
- /^(\S+)\s\s+\S+\s\s+(\d\S+)\s\s+(\S.*?)(?:$|\s\s)/
7
- else
8
- # Bundler < 2.2
9
- /\* (\S+) \(newest (\S+),.* requested (.*)\)/
10
- end
11
-
12
- output.scan(expr).map do |name, newest, pin|
13
- { name: name, newest: newest, pin: pin }
14
- end
15
- end
16
- end
17
- end
@@ -1,60 +0,0 @@
1
- module Bundleup
2
- class Upgrade
3
- def initialize(update_args=[], commands=BundleCommands.new)
4
- @update_args = update_args
5
- @commands = commands
6
- @gem_statuses = {}
7
- @original_lockfile_contents = IO.read(lockfile)
8
- run
9
- end
10
-
11
- def upgrades
12
- @gem_statuses.values.select(&:upgraded?).sort_by(&:name)
13
- end
14
-
15
- def pins
16
- @gem_statuses.values.select(&:pinned?).sort_by(&:name)
17
- end
18
-
19
- def lockfile_changed?
20
- IO.read(lockfile) != original_lockfile_contents
21
- end
22
-
23
- def undo
24
- IO.write(lockfile, original_lockfile_contents)
25
- end
26
-
27
- private
28
-
29
- attr_reader :update_args, :commands, :original_lockfile_contents
30
-
31
- def run
32
- commands.check || commands.install
33
- find_versions(:old)
34
- commands.update(update_args)
35
- find_versions(:new)
36
- find_pinned_versions
37
- end
38
-
39
- def lockfile
40
- "Gemfile.lock"
41
- end
42
-
43
- def find_pinned_versions
44
- OutdatedParser.parse(commands.outdated).each do |gem|
45
- gem_status(gem[:name]).newest_version = gem[:newest]
46
- gem_status(gem[:name]).pin = gem[:pin]
47
- end
48
- end
49
-
50
- def find_versions(type)
51
- commands.list.scan(/\* (\S+) \((\S+)(?: (\S+))?\)/) do |name, ver, sha|
52
- gem_status(name).public_send("#{type}_version=", sha || ver)
53
- end
54
- end
55
-
56
- def gem_status(name)
57
- @gem_statuses[name] ||= GemStatus.new(name)
58
- end
59
- end
60
- end