bundleup 1.3.0 → 2.0.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: db5d58970165c2381d963205eae11b610d57f35e9654bee2edff54f2d1ff861f
4
- data.tar.gz: 508d1cba2d3ab7c2167b60ec38fd1bf49e0252f614e07fe7cce3ed36de3dc32e
3
+ metadata.gz: 82ad26069759aeaa4516bb5305da2d0d75a38e1c30d8efd275b3fd09df6f2697
4
+ data.tar.gz: 1cec30ec08cf62b3332c6b8b6539c4231fbf6b1bc29f2c8a4069b5afff2d0b83
5
5
  SHA512:
6
- metadata.gz: c8133402453232bfd33b17a9c3c5f903874f92f190063bc84e7f29723514ab5333d791d0ac2e6eb4c8deb75a5d5f734a20cb47d63101a79ff879b8e566c63952
7
- data.tar.gz: c508a0e3360b51d537c948e1cd95790fd69ad01d43d86a97976729fea4737a84aa25fedd69513f379c26ba82c0abcea3f71bc3f25ad1e802845b24c81f3deb9e
6
+ metadata.gz: 388e5e6c1ca95b073319c7cd5954348190c2ff1a2e63836659a966eebc7c4da8583474434c33a979d0f70bc69e02686d8a0c4829fcba4123e25b675665af2ee4
7
+ data.tar.gz: 651c38cc47925f3bafbbc30b50d777c58dfc2918ca3a1329c182ae38735d6e33fb287cb6ba9096b878c4c137746cdc6baa18726362a02eb036ab7a63db9e77a7
data/README.md CHANGED
@@ -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
 
@@ -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,8 +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/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
+
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
@@ -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,34 @@
1
+ require "forwardable"
2
+
3
+ module Bundleup
4
+ class Report
5
+ extend Forwardable
6
+ def_delegators :rows, :empty?
7
+
8
+ def to_s
9
+ [
10
+ title,
11
+ tableize(rows).map { |row| row.join(" ").rstrip }.join("\n"),
12
+ ""
13
+ ].join("\n\n")
14
+ end
15
+
16
+ private
17
+
18
+ def tableize(rows)
19
+ widths = max_length_of_each_column(rows)
20
+ rows.map do |row|
21
+ row.zip(widths).map do |value, width|
22
+ padding = " " * (width - Colors.strip(value).length)
23
+ "#{value}#{padding}"
24
+ end
25
+ end
26
+ end
27
+
28
+ def max_length_of_each_column(rows)
29
+ Array.new(rows.first.count) do |i|
30
+ rows.map { |values| Colors.strip(values[i]).length }.max
31
+ end
32
+ end
33
+ end
34
+ 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.0.0".freeze
3
3
  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.0.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: 2020-08-08 00:00:00.000000000 Z
11
+ date: 2020-09-03 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,13 +27,16 @@ 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
38
41
  homepage: https://github.com/mattbrictson/bundleup
39
42
  licenses:
@@ -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