bundleup 1.2.0 → 2.1.2

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: 34d18c6d9e39ccd56bb47b656745a19d022981a7a8c735a277eb2a782b106df8
4
- data.tar.gz: 787cba0d2ec134b82de3dbae1819f609ebd55c98bfbdbc9fe58609eb35e861ab
3
+ metadata.gz: feeb625fa4809af16d2bd8432e0a346dbcc842b513ede9bf5359209d8048eed9
4
+ data.tar.gz: d7fd509ab3ec67d7edda8e7ae99ceeb7d22e9ec913fddc6f56da554e2faa82a4
5
5
  SHA512:
6
- metadata.gz: efcf4a177c7dc9b7e774b6f800f811abf6351d5e884858d9d4d2e626dddb28c2d09164485812032b61a8c15161ff809891b1a918c574ab63c46c57e96fe28108
7
- data.tar.gz: 2bd482d805cf7f0e0c340d1c5e0c5bf0e12c018e5055533481aa882b792f9fa342ca70fbd57f6b51f13ec21b113bca9f98919b51f8867fb796f91492e35de14c
6
+ metadata.gz: 4bc171b6589fc4a7c34a7de34e89ac81581d029060323627f36fc8fe863f5fc942a00178c84ab1821f4d4148dcc4ba25d131dec70c52146f50890d7490abc59b
7
+ data.tar.gz: 1a760272468231a61eccb246111bd42b4e0af8bd6185a3a715fbcc4a4d885d4e754ca11cf50ec5387b03c5ba5347e5211d37d6ad1391095359de5a13965c9418
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,9 +44,39 @@ 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
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
+ > 💡 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
78
+
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
 
51
81
  Finally, bundleup runs `bundle outdated` to see the gems that were _not_ updated due to Gemfile restrictions.
52
82
 
@@ -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