bundleup 1.3.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: 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