bundleup 0.9.0 → 2.0.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: 9a12520bbc6803f69e10659bf902b5ea02b1902e1c3115ab77ca13f9721e3190
4
- data.tar.gz: 57e69c40eaa3639ebb05dbdacc28b8c38d189138b320146044a9ff145dc9bfa5
3
+ metadata.gz: 82ad26069759aeaa4516bb5305da2d0d75a38e1c30d8efd275b3fd09df6f2697
4
+ data.tar.gz: 1cec30ec08cf62b3332c6b8b6539c4231fbf6b1bc29f2c8a4069b5afff2d0b83
5
5
  SHA512:
6
- metadata.gz: aeed97c4f56cab2a1bd993e5cdea4dd1f38381ad5604948ca38e98632572b9dbd1c162e86ee71a5bd015ea1647a57cafaf86e9d6a44db5553c260195e47df457
7
- data.tar.gz: 59ba702fe9f6078299c5a439b43b1ec1ef134e7d54d66156bbee909a73d5a2f2d77eb2df1e6df0c081baa9fc30893fc70c5f8de67fe0d058ce93f9a002fa82d1
6
+ metadata.gz: 388e5e6c1ca95b073319c7cd5954348190c2ff1a2e63836659a966eebc7c4da8583474434c33a979d0f70bc69e02686d8a0c4829fcba4123e25b675665af2ee4
7
+ data.tar.gz: 651c38cc47925f3bafbbc30b50d777c58dfc2918ca3a1329c182ae38735d6e33fb287cb6ba9096b878c4c137746cdc6baa18726362a02eb036ab7a63db9e77a7
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=master)](https://travis-ci.org/mattbrictson/bundleup)
4
+ [![Build Status](https://travis-ci.org/mattbrictson/bundleup.svg?branch=main)](https://travis-ci.org/mattbrictson/bundleup)
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,12 +14,12 @@ 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/master/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
 
21
21
  - Bundler 1.16 or later
22
- - Ruby 2.4 or later
22
+ - Ruby 2.5 or later
23
23
 
24
24
  ## Usage
25
25
 
@@ -46,7 +46,7 @@ bundleup --group=development
46
46
 
47
47
  ## How it works
48
48
 
49
- bundleup starts by making a backup copy of your Gemfile.lock. Next it runs `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.)
49
+ 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
50
 
51
51
  Finally, bundleup runs `bundle outdated` to see the gems that were _not_ updated due to Gemfile restrictions.
52
52
 
@@ -54,7 +54,7 @@ After displaying its findings, bundleup gives you the option of keeping the chan
54
54
 
55
55
  ## Roadmap
56
56
 
57
- bundleup is a very simple script at this point, but it could be more. Some possibilities:
57
+ bundleup is very simple at this point, but it could be more. Some possibilities:
58
58
 
59
59
  - Automatically commit the Gemfile.lock changes with a nice commit message
60
60
  - Integrate with bundler-audit to mark upgrades that have important security fixes
@@ -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
@@ -1,7 +1,21 @@
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/upgrade"
2
+ require "bundleup/backup"
3
+ require "bundleup/colors"
7
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
+
13
+ module Bundleup
14
+ class << self
15
+ attr_accessor :commands, :logger, :shell
16
+ end
17
+ end
18
+
19
+ Bundleup.commands = Bundleup::Commands.new
20
+ Bundleup.logger = Bundleup::Logger.new
21
+ Bundleup.shell = Bundleup::Shell.new
@@ -0,0 +1,26 @@
1
+ module Bundleup
2
+ class Backup
3
+ def self.restore_on_error(path)
4
+ backup = new(path)
5
+ begin
6
+ yield(backup)
7
+ rescue StandardError, Interrupt
8
+ backup.restore
9
+ raise
10
+ end
11
+ end
12
+
13
+ def initialize(path)
14
+ @path = path
15
+ @original_contents = IO.read(path)
16
+ end
17
+
18
+ def restore
19
+ IO.write(path, original_contents)
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :path, :original_contents
25
+ end
26
+ end
@@ -1,86 +1,120 @@
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
5
+ Error = Class.new(StandardError)
6
+
7
+ include Colors
8
+ extend Forwardable
9
+ def_delegators :Bundleup, :commands, :logger
10
+
11
+ def initialize(args)
12
+ @args = args
16
13
  end
17
14
 
18
- private
15
+ def run # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
16
+ print_usage && return if (args & %w[-h --help]).any?
17
+
18
+ assert_gemfile_and_lock_exist!
19
+
20
+ logger.puts "Please wait a moment while I upgrade your Gemfile.lock..."
21
+ Backup.restore_on_error("Gemfile.lock") do |backup|
22
+ update_report, pin_report = perform_analysis
23
+
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?
19
33
 
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.")
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
26
40
  end
27
41
  end
28
42
 
29
- def review_pins
30
- return if pins.empty?
43
+ private
31
44
 
32
- puts "\nNote that the following gem(s) are being held back:\n\n"
33
- print_pins_table
34
- end
45
+ attr_reader :args
35
46
 
36
- def confirm_commit
37
- confirm("\nDo you want to apply these changes?")
38
- end
47
+ def print_usage # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
48
+ logger.puts(<<~USAGE.gsub(/^/, " "))
39
49
 
40
- def restore_lockfile
41
- return unless defined?(@upgrade)
42
- return unless upgrade.lockfile_changed?
50
+ Usage: #{green('bundleup')} #{yellow('[GEMS...] [OPTIONS]')}
43
51
 
44
- upgrade.undo
45
- puts "Your original Gemfile.lock has been restored."
46
- end
52
+ Use #{blue('bundleup')} in place of #{blue('bundle update')} to interactively update your project
53
+ Gemfile.lock to the latest gem versions. Bundleup will show what gems will
54
+ be updated, color-code them based on semver, and ask you to confirm the
55
+ updates before finalizing them. For example:
47
56
 
48
- def upgrade
49
- @upgrade ||= Upgrade.new(ARGV)
50
- end
57
+ The following gems will be updated:
51
58
 
52
- def upgrades
53
- upgrade.upgrades
54
- end
59
+ #{yellow('bundler-audit 0.6.1 → 0.7.0.1')}
60
+ i18n 1.8.2 → 1.8.5
61
+ #{red('json 2.2.0 → (removed)')}
62
+ parser 2.7.1.1 → 2.7.1.4
63
+ #{red('rails e063bef → 57a4ead')}
64
+ #{blue('rubocop-ast (new) → 0.3.0')}
65
+ #{red('thor 0.20.3 → 1.0.1')}
66
+ #{yellow('zeitwerk 2.3.0 → 2.4.0')}
55
67
 
56
- def pins
57
- upgrade.pins
58
- end
68
+ #{yellow('Do you want to apply these changes [Yn]?')}
59
69
 
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
67
- end
70
+ Bundleup will also let you know if there are gems that can't be updated
71
+ because they are pinned in the Gemfile. Any relevant comments from the
72
+ Gemfile will also be included, explaining the pins:
68
73
 
69
- def print_pins_table
70
- rows = tableize(pins) do |g|
71
- [g.name, g.new_version, "", g.newest_version, *pin_reason(g)]
72
- end
73
- puts rows.join("\n")
74
+ Note that the following gems are being held back:
75
+
76
+ rake 12.3.313.0.1 : pinned at ~> 12.0 #{gray('# Not ready for 13 yet')}
77
+ rubocop 0.89.0 → 0.89.1 : pinned at = 0.89.0
78
+
79
+ You may optionally specify one or more #{yellow('GEMS')} or pass #{yellow('OPTIONS')} to bundleup;
80
+ these will be passed through to bundler. See #{blue('bundle update --help')} for the
81
+ full list of the options that bundler supports.
82
+
83
+ Examples:
84
+
85
+ #{gray('# Update all gems')}
86
+ #{blue('$ bundleup')}
87
+
88
+ #{gray('# Only update gems in the development group')}
89
+ #{blue('$ bundleup --group=development')}
90
+
91
+ #{gray('# Only update the rake gem')}
92
+ #{blue('$ bundleup rake')}
93
+
94
+ USAGE
95
+ true
74
96
  end
75
97
 
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]
98
+ def assert_gemfile_and_lock_exist!
99
+ return if File.exist?("Gemfile") && File.exist?("Gemfile.lock")
100
+
101
+ raise Error, "Gemfile and Gemfile.lock must both be present."
80
102
  end
81
103
 
82
- def gemfile
83
- @gemfile ||= Gemfile.new
104
+ def perform_analysis # rubocop:disable Metrics/AbcSize
105
+ gem_comments = Gemfile.new.gem_comments
106
+ commands.check? || commands.install
107
+ old_versions = commands.list
108
+ commands.update(args)
109
+ new_versions = commands.list
110
+ outdated_gems = commands.outdated
111
+
112
+ logger.clear_line
113
+
114
+ update_report = UpdateReport.new(old_versions: old_versions, new_versions: new_versions)
115
+ pin_report = PinReport.new(gem_versions: new_versions, outdated_gems: outdated_gems, gem_comments: gem_comments)
116
+
117
+ [update_report, pin_report]
84
118
  end
85
119
  end
86
120
  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
@@ -4,14 +4,21 @@ module Bundleup
4
4
  @contents = IO.read(path)
5
5
  end
6
6
 
7
- def gem_comment(gem_name)
8
- inline_comment(gem_name) || prefix_comment(gem_name)
7
+ def gem_comments
8
+ gem_names.each_with_object({}) do |gem, hash|
9
+ comment = inline_comment(gem) || prefix_comment(gem)
10
+ hash[gem] = comment unless comment.nil?
11
+ end
9
12
  end
10
13
 
11
14
  private
12
15
 
13
16
  attr_reader :contents
14
17
 
18
+ def gem_names
19
+ contents.scan(/^\s*gem\s+["'](.+?)["']/).flatten.uniq
20
+ end
21
+
15
22
  def inline_comment(gem_name)
16
23
  contents[/#{gem_declaration_re(gem_name)}.*(#\s*\S+.*)/, 1]
17
24
  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