bundle_update_interactive 0.8.1 → 0.9.1

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: '092dad2541b41d82be0c6d26b8dc6a4b72c9555fcef8743919849f8b57b67a24'
4
- data.tar.gz: b73e37e2d76480af7fbbbc22a2d8957f5a364bdacdefd2c9122830895c9de929
3
+ metadata.gz: f45b67dd4da1f56e5bad0b154acfb7dce40b4f518eb1cf9c1101302ec3ee71f4
4
+ data.tar.gz: 9d763c27460be163b2dc6eaa818e770e81448244bcd0c2c94dee368b148d4a1d
5
5
  SHA512:
6
- metadata.gz: e4dcf48f02f99ee78f8a08e23d0168846c344f0ff7d6c6d5c29c34c6cbf690e16ad2465a299bbf9a2dd8a349f8960737a14ba3d96fbc66864a857ddfabb0736d
7
- data.tar.gz: 9789ed423ff5c6a30d2410e873af33126268825dd3571252d087149ab28154d55de04f5027ceb2fcc3ade3bdc04d6f877b55a7e0084ed4d73edc4dac50a2d12e
6
+ metadata.gz: 5bf17f343e90aa16f244c470ad9f58590702d61f610e9ac6bb4623990b997f103ea52448433f8c5273880b3f110e467c72c841fa25274c9770e21805c3ce403a
7
+ data.tar.gz: 248b8e8b066fa56574297c76ce25eab8bbd6b14e10ca1a8d83444679a82dae76ae7117775fa6df9753a1c847a337d2c4730270d87ec55c33186e249e9d002a02
data/README.md CHANGED
@@ -42,6 +42,7 @@ bundle ui
42
42
 
43
43
  ## Options
44
44
 
45
+ - `--commit` [applies each gem update in a discrete git commit](#git-commits)
45
46
  - `--latest` [modifies the Gemfile if necessary to allow the latest gem versions](#allow-latest-versions)
46
47
  - `-D` / `--exclusively=GROUP` [limits updatable gems by Gemfile groups](#limit-impact-by-gemfile-groups)
47
48
 
@@ -69,6 +70,27 @@ Some gems, notably `rails`, are composed of smaller gems like `actionpack`, `act
69
70
 
70
71
  Therefore, if any Rails component has a security vulnerability, `bundle update-interactive` will automatically roll up that information into a single `rails` line item, so you can select it and upgrade all of its components in one shot.
71
72
 
73
+ ### Git commits
74
+
75
+ Sometimes, updating gems can lead to bugs or regressions. To facilitate troubleshooting, `update-interactive` offers the ability to commit each selected gem update in its own git commit, complete with a descriptive commit message. You can then make use of tools like `git bisect` to more easily find the update that introduced the problem.
76
+
77
+ To enable this behavior, pass the `--commit` option:
78
+
79
+ ```
80
+ bundle update-interactive --commit
81
+ ```
82
+
83
+ The gems you select to be updated will be applied in separate commits, like this:
84
+
85
+ ```
86
+ * c9801382 Update activeadmin 3.2.2 → 3.2.3
87
+ * 9957254b Update rexml 3.3.5 → 3.3.6
88
+ * 4a4f2072 Update sass 1.77.6 → 1.77.8
89
+ ```
90
+
91
+ > [!NOTE]
92
+ > In rare cases, Bundler may not be able to update a gem separately, due to interdependencies between gem versions. If this happens, you will see a message like "attempted to update [GEM] but its version stayed the same."
93
+
72
94
  ### Held back gems
73
95
 
74
96
  When a newer version of a gem is available, but updating is not allowed due to a Gemfile requirement, `update-interactive` will report that the gem has been held back.
@@ -5,87 +5,89 @@ require "pastel"
5
5
  require "tty/prompt"
6
6
  require "tty/screen"
7
7
 
8
- class BundleUpdateInteractive::CLI
9
- class MultiSelect
10
- extend BundleUpdateInteractive::StringHelper
8
+ module BundleUpdateInteractive
9
+ class CLI
10
+ class MultiSelect
11
+ extend BundleUpdateInteractive::StringHelper
11
12
 
12
- class List < TTY::Prompt::MultiList
13
- def initialize(prompt, **options)
14
- @opener = options.delete(:opener)
15
- defaults = {
16
- cycle: true,
17
- help_color: :itself.to_proc,
18
- per_page: [TTY::Prompt::Paginator::DEFAULT_PAGE_SIZE, TTY::Screen.height.to_i - 3].max,
19
- quiet: true,
20
- show_help: :always
21
- }
22
- super(prompt, **defaults.merge(options))
23
- end
13
+ class List < TTY::Prompt::MultiList
14
+ def initialize(prompt, **options)
15
+ @opener = options.delete(:opener)
16
+ defaults = {
17
+ cycle: true,
18
+ help_color: :itself.to_proc,
19
+ per_page: [TTY::Prompt::Paginator::DEFAULT_PAGE_SIZE, TTY::Screen.height.to_i - 3].max,
20
+ quiet: true,
21
+ show_help: :always
22
+ }
23
+ super(prompt, **defaults.merge(options))
24
+ end
24
25
 
25
- def selected_names
26
- ""
27
- end
26
+ def selected_names
27
+ ""
28
+ end
28
29
 
29
- # Unregister tty-prompt's default ctrl-a and ctrl-r bindings
30
- alias select_all keyctrl_a
31
- alias reverse_selection keyctrl_r
32
- def keyctrl_a(*); end
33
- def keyctrl_r(*); end
30
+ # Unregister tty-prompt's default ctrl-a and ctrl-r bindings
31
+ alias select_all keyctrl_a
32
+ alias reverse_selection keyctrl_r
33
+ def keyctrl_a(*); end
34
+ def keyctrl_r(*); end
34
35
 
35
- def keypress(event)
36
- case event.value
37
- when "k", "p" then keyup
38
- when "j", "n" then keydown
39
- when "a" then select_all
40
- when "r" then reverse_selection
41
- when "o" then opener&.call(choices[@active - 1].value)
36
+ def keypress(event)
37
+ case event.value
38
+ when "k", "p" then keyup
39
+ when "j", "n" then keydown
40
+ when "a" then select_all
41
+ when "r" then reverse_selection
42
+ when "o" then opener&.call(choices[@active - 1].value)
43
+ end
42
44
  end
43
- end
44
45
 
45
- private
46
+ private
46
47
 
47
- attr_reader :opener
48
- end
48
+ attr_reader :opener
49
+ end
49
50
 
50
- def self.prompt_for_gems_to_update(outdated_gems, prompt: nil)
51
- table = Table.updatable(outdated_gems)
52
- title = "#{pluralize(outdated_gems.length, 'gem')} can be updated."
53
- opener = lambda do |gem|
54
- url = outdated_gems[gem].changelog_uri
55
- Launchy.open(url) unless url.nil?
51
+ def self.prompt_for_gems_to_update(outdated_gems, prompt: nil)
52
+ table = Table.updatable(outdated_gems)
53
+ title = "#{pluralize(outdated_gems.length, 'gem')} can be updated."
54
+ opener = lambda do |gem|
55
+ url = outdated_gems[gem].changelog_uri
56
+ Launchy.open(url) unless url.nil?
57
+ end
58
+ chosen = new(title: title, table: table, prompt: prompt, opener: opener).prompt
59
+ outdated_gems.slice(*chosen)
56
60
  end
57
- chosen = new(title: title, table: table, prompt: prompt, opener: opener).prompt
58
- outdated_gems.slice(*chosen)
59
- end
60
61
 
61
- def initialize(title:, table:, opener: nil, prompt: nil)
62
- @title = title
63
- @table = table
64
- @opener = opener
65
- @tty_prompt = prompt || TTY::Prompt.new(
66
- interrupt: lambda {
67
- puts
68
- exit(130)
69
- }
70
- )
71
- @pastel = BundleUpdateInteractive.pastel
72
- end
62
+ def initialize(title:, table:, opener: nil, prompt: nil)
63
+ @title = title
64
+ @table = table
65
+ @opener = opener
66
+ @tty_prompt = prompt || TTY::Prompt.new(
67
+ interrupt: lambda {
68
+ puts
69
+ exit(130)
70
+ }
71
+ )
72
+ @pastel = BundleUpdateInteractive.pastel
73
+ end
73
74
 
74
- def prompt
75
- choices = table.gem_names.to_h { |name| [table.render_gem(name), name] }
76
- tty_prompt.invoke_select(List, title, choices, help: help, opener: opener)
77
- end
75
+ def prompt
76
+ choices = table.gem_names.to_h { |name| [table.render_gem(name), name] }
77
+ tty_prompt.invoke_select(List, title, choices, help: help, opener: opener)
78
+ end
78
79
 
79
- private
80
+ private
80
81
 
81
- attr_reader :pastel, :table, :opener, :tty_prompt, :title
82
+ attr_reader :pastel, :table, :opener, :tty_prompt, :title
82
83
 
83
- def help
84
- [
85
- pastel.dim("\nPress <space> to select, ↑/↓ move, <a> all, <r> reverse, <o> open url, <enter> to finish."),
86
- "\n ",
87
- table.render_header
88
- ].join
84
+ def help
85
+ [
86
+ pastel.dim("\nPress <space> to select, ↑/↓ move, <a> all, <r> reverse, <o> open url, <enter> to finish."),
87
+ "\n ",
88
+ table.render_header
89
+ ].join
90
+ end
89
91
  end
90
92
  end
91
93
  end
@@ -3,102 +3,112 @@
3
3
  require "optparse"
4
4
 
5
5
  module BundleUpdateInteractive
6
- class CLI::Options
7
- class << self
8
- def parse(argv=ARGV)
9
- options = new
10
- remaining = build_parser(options).parse!(argv.dup)
11
- raise Error, "update-interactive does not accept arguments. See --help for available options." if remaining.any?
12
-
13
- options.freeze
14
- end
6
+ class CLI
7
+ class Options
8
+ class << self
9
+ def parse(argv=ARGV)
10
+ options = new
11
+ remain = build_parser(options).parse!(argv.dup)
12
+ raise Error, "update-interactive does not accept arguments. See --help for available options." if remain.any?
13
+
14
+ options.freeze
15
+ end
15
16
 
16
- def summary
17
- build_parser(new).summarize.join.gsub(/^\s+-.*? /, pastel.yellow('\0'))
18
- end
17
+ def summary
18
+ build_parser(new).summarize.join.gsub(/^\s+-.*? /, pastel.yellow('\0'))
19
+ end
19
20
 
20
- def help # rubocop:disable Metrics/AbcSize
21
- <<~HELP
22
- Provides an easy way to update gems to their latest versions.
21
+ def help # rubocop:disable Metrics/AbcSize
22
+ <<~HELP
23
+ Provides an easy way to update gems to their latest versions.
23
24
 
24
- #{pastel.bold.underline('USAGE')}
25
- #{pastel.green('bundle update-interactive')} #{pastel.yellow('[options]')}
26
- #{pastel.green('bundle ui')} #{pastel.yellow('[options]')}
25
+ #{pastel.bold.underline('USAGE')}
26
+ #{pastel.green('bundle update-interactive')} #{pastel.yellow('[options]')}
27
+ #{pastel.green('bundle ui')} #{pastel.yellow('[options]')}
27
28
 
28
- #{pastel.bold.underline('OPTIONS')}
29
- #{summary}
30
- #{pastel.bold.underline('DESCRIPTION')}
31
- Displays the list of gems that would be updated by `bundle update`, allowing you
32
- to navigate them by keyboard and pick which ones to update. A changelog URL,
33
- when available, is displayed alongside each update. Gems with known security
34
- vulnerabilities are also highlighted.
29
+ #{pastel.bold.underline('OPTIONS')}
30
+ #{summary}
31
+ #{pastel.bold.underline('DESCRIPTION')}
32
+ Displays the list of gems that would be updated by `bundle update`, allowing you
33
+ to navigate them by keyboard and pick which ones to update. A changelog URL,
34
+ when available, is displayed alongside each update. Gems with known security
35
+ vulnerabilities are also highlighted.
35
36
 
36
- Your Gemfile.lock will be updated conservatively based on the gems you select.
37
- Transitive dependencies are not affected.
37
+ Your Gemfile.lock will be updated conservatively based on the gems you select.
38
+ Transitive dependencies are not affected.
38
39
 
39
- More information: #{pastel.blue('https://github.com/mattbrictson/bundle_update_interactive')}
40
+ More information: #{pastel.blue('https://github.com/mattbrictson/bundle_update_interactive')}
40
41
 
41
- #{pastel.bold.underline('EXAMPLES')}
42
- Show all gems that can be updated.
43
- #{pastel.green('bundle update-interactive')}
42
+ #{pastel.bold.underline('EXAMPLES')}
43
+ Show all gems that can be updated.
44
+ #{pastel.green('bundle update-interactive')}
44
45
 
45
- The "ui" command alias can also be used.
46
- #{pastel.green('bundle ui')}
46
+ The "ui" command alias can also be used.
47
+ #{pastel.green('bundle ui')}
47
48
 
48
- Show updates for development and test gems only, leaving production gems untouched.
49
- #{pastel.green('bundle update-interactive')} #{pastel.yellow('-D')}
49
+ Show updates for development and test gems only, leaving production gems untouched.
50
+ #{pastel.green('bundle update-interactive')} #{pastel.yellow('-D')}
50
51
 
51
- Allow the latest gem versions, ignoring Gemfile pins. May modify the Gemfile.
52
- #{pastel.green('bundle update-interactive')} #{pastel.yellow('--latest')}
52
+ Allow the latest gem versions, ignoring Gemfile pins. May modify the Gemfile.
53
+ #{pastel.green('bundle update-interactive')} #{pastel.yellow('--latest')}
53
54
 
54
- HELP
55
- end
55
+ HELP
56
+ end
56
57
 
57
- private
58
+ private
58
59
 
59
- def pastel
60
- BundleUpdateInteractive.pastel
61
- end
60
+ def pastel
61
+ BundleUpdateInteractive.pastel
62
+ end
62
63
 
63
- def build_parser(options) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
64
- OptionParser.new do |parser|
65
- parser.summary_indent = " "
66
- parser.summary_width = 24
67
- parser.on("--latest", "Modify the Gemfile to allow the latest gem versions") do
68
- options.latest = true
69
- end
70
- parser.on(
71
- "--exclusively=GROUP",
72
- "Update gems exclusively belonging to the specified Gemfile GROUP(s)"
73
- ) do |value|
74
- options.exclusively = value.split(",").map(&:strip).reject(&:empty?).map(&:to_sym)
75
- end
76
- parser.on("-D", "Shorthand for --exclusively=development,test") do
77
- options.exclusively = %i[development test]
78
- end
79
- parser.on("-v", "--version", "Display version") do
80
- require "bundler"
81
- puts "bundle_update_interactive/#{VERSION} bundler/#{Bundler::VERSION} #{RUBY_DESCRIPTION}"
82
- exit
83
- end
84
- parser.on("-h", "--help", "Show this help") do
85
- puts help
86
- exit
64
+ def build_parser(options) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
65
+ OptionParser.new do |parser| # rubocop:disable Metrics/BlockLength
66
+ parser.summary_indent = " "
67
+ parser.summary_width = 24
68
+ parser.on("--commit", "Create a git commit for each selected gem update") do
69
+ options.commit = true
70
+ end
71
+ parser.on("--latest", "Modify the Gemfile to allow the latest gem versions") do
72
+ options.latest = true
73
+ end
74
+ parser.on(
75
+ "--exclusively=GROUP",
76
+ "Update gems exclusively belonging to the specified Gemfile GROUP(s)"
77
+ ) do |value|
78
+ options.exclusively = value.split(",").map(&:strip).reject(&:empty?).map(&:to_sym)
79
+ end
80
+ parser.on("-D", "Shorthand for --exclusively=development,test") do
81
+ options.exclusively = %i[development test]
82
+ end
83
+ parser.on("-v", "--version", "Display version") do
84
+ require "bundler"
85
+ puts "bundle_update_interactive/#{VERSION} bundler/#{Bundler::VERSION} #{RUBY_DESCRIPTION}"
86
+ exit
87
+ end
88
+ parser.on("-h", "--help", "Show this help") do
89
+ puts help
90
+ exit
91
+ end
87
92
  end
88
93
  end
89
94
  end
90
- end
91
95
 
92
- attr_accessor :exclusively
93
- attr_writer :latest
96
+ attr_accessor :exclusively
97
+ attr_writer :commit, :latest
94
98
 
95
- def initialize
96
- @exclusively = []
97
- @latest = false
98
- end
99
+ def initialize
100
+ @exclusively = []
101
+ @commit = false
102
+ @latest = false
103
+ end
104
+
105
+ def commit?
106
+ @commit
107
+ end
99
108
 
100
- def latest?
101
- @latest
109
+ def latest?
110
+ @latest
111
+ end
102
112
  end
103
113
  end
104
114
  end
@@ -3,53 +3,55 @@
3
3
  require "delegate"
4
4
  require "pastel"
5
5
 
6
- class BundleUpdateInteractive::CLI
7
- class Row < SimpleDelegator
8
- SEMVER_COLORS = {
9
- major: :red,
10
- minor: :yellow,
11
- patch: :green
12
- }.freeze
13
-
14
- def initialize(outdated_gem)
15
- super
16
- @pastel = BundleUpdateInteractive.pastel
17
- end
6
+ module BundleUpdateInteractive
7
+ class CLI
8
+ class Row < SimpleDelegator
9
+ SEMVER_COLORS = {
10
+ major: :red,
11
+ minor: :yellow,
12
+ patch: :green
13
+ }.freeze
18
14
 
19
- def formatted_gem_name
20
- vulnerable? ? pastel.white.on_red(name) : apply_semver_highlight(name)
21
- end
15
+ def initialize(outdated_gem)
16
+ super
17
+ @pastel = BundleUpdateInteractive.pastel
18
+ end
22
19
 
23
- def formatted_current_version
24
- [current_version.to_s, current_git_version].compact.join(" ")
25
- end
20
+ def formatted_gem_name
21
+ vulnerable? ? pastel.white.on_red(name) : apply_semver_highlight(name)
22
+ end
26
23
 
27
- def formatted_updated_version
28
- version = semver_change.format { |part| apply_semver_highlight(part) }
29
- git_version = apply_semver_highlight(updated_git_version)
24
+ def formatted_current_version
25
+ [current_version.to_s, current_git_version].compact.join(" ")
26
+ end
30
27
 
31
- [version, git_version].compact.join(" ")
32
- end
28
+ def formatted_updated_version
29
+ version = semver_change.format { |part| apply_semver_highlight(part) }
30
+ git_version = apply_semver_highlight(updated_git_version)
33
31
 
34
- def formatted_gemfile_groups
35
- gemfile_groups&.map(&:inspect)&.join(", ")
36
- end
32
+ [version, git_version].compact.join(" ")
33
+ end
37
34
 
38
- def formatted_gemfile_requirement
39
- gemfile_requirement.to_s == ">= 0" ? "" : gemfile_requirement.to_s
40
- end
35
+ def formatted_gemfile_groups
36
+ gemfile_groups&.map(&:inspect)&.join(", ")
37
+ end
41
38
 
42
- def formatted_changelog_uri
43
- pastel.blue(changelog_uri)
44
- end
39
+ def formatted_gemfile_requirement
40
+ gemfile_requirement.to_s == ">= 0" ? "" : gemfile_requirement.to_s
41
+ end
45
42
 
46
- def apply_semver_highlight(value)
47
- color = git_version_changed? ? :cyan : SEMVER_COLORS.fetch(semver_change.severity)
48
- pastel.decorate(value, color)
49
- end
43
+ def formatted_changelog_uri
44
+ pastel.blue(changelog_uri)
45
+ end
50
46
 
51
- private
47
+ def apply_semver_highlight(value)
48
+ color = git_version_changed? ? :cyan : SEMVER_COLORS.fetch(semver_change.severity)
49
+ pastel.decorate(value, color)
50
+ end
52
51
 
53
- attr_reader :pastel
52
+ private
53
+
54
+ attr_reader :pastel
55
+ end
54
56
  end
55
57
  end
@@ -2,83 +2,85 @@
2
2
 
3
3
  require "pastel"
4
4
 
5
- class BundleUpdateInteractive::CLI
6
- class Table
7
- class << self
8
- def withheld(gems)
9
- columns = [
10
- ["name", :formatted_gem_name],
11
- ["requirement", :formatted_gemfile_requirement],
12
- ["current", :formatted_current_version],
13
- ["latest", :formatted_updated_version],
14
- ["group", :formatted_gemfile_groups],
15
- ["url", :formatted_changelog_uri]
16
- ]
17
- new(gems, columns)
18
- end
5
+ module BundleUpdateInteractive
6
+ class CLI
7
+ class Table
8
+ class << self
9
+ def withheld(gems)
10
+ columns = [
11
+ ["name", :formatted_gem_name],
12
+ ["requirement", :formatted_gemfile_requirement],
13
+ ["current", :formatted_current_version],
14
+ ["latest", :formatted_updated_version],
15
+ ["group", :formatted_gemfile_groups],
16
+ ["url", :formatted_changelog_uri]
17
+ ]
18
+ new(gems, columns)
19
+ end
19
20
 
20
- def updatable(gems)
21
- columns = [
22
- ["name", :formatted_gem_name],
23
- ["from", :formatted_current_version],
24
- [nil, "→"],
25
- ["to", :formatted_updated_version],
26
- ["group", :formatted_gemfile_groups],
27
- ["url", :formatted_changelog_uri]
28
- ]
29
- new(gems, columns)
21
+ def updatable(gems)
22
+ columns = [
23
+ ["name", :formatted_gem_name],
24
+ ["from", :formatted_current_version],
25
+ [nil, "→"],
26
+ ["to", :formatted_updated_version],
27
+ ["group", :formatted_gemfile_groups],
28
+ ["url", :formatted_changelog_uri]
29
+ ]
30
+ new(gems, columns)
31
+ end
30
32
  end
31
- end
32
33
 
33
- def initialize(gems, columns)
34
- @pastel = BundleUpdateInteractive.pastel
35
- @headers = columns.map { |header, _| pastel.dim.underline(header) }
36
- @rows = gems.transform_values do |gem|
37
- row = Row.new(gem)
38
- columns.map do |_, col|
39
- case col
40
- when Symbol then row.public_send(col).to_s
41
- when String then col
34
+ def initialize(gems, columns)
35
+ @pastel = BundleUpdateInteractive.pastel
36
+ @headers = columns.map { |header, _| pastel.dim.underline(header) }
37
+ @rows = gems.transform_values do |gem|
38
+ row = Row.new(gem)
39
+ columns.map do |_, col|
40
+ case col
41
+ when Symbol then row.public_send(col).to_s
42
+ when String then col
43
+ end
42
44
  end
43
45
  end
46
+ @column_widths = calculate_column_widths
44
47
  end
45
- @column_widths = calculate_column_widths
46
- end
47
48
 
48
- def gem_names
49
- rows.keys
50
- end
49
+ def gem_names
50
+ rows.keys
51
+ end
51
52
 
52
- def render_header
53
- render_row(headers)
54
- end
53
+ def render_header
54
+ render_row(headers)
55
+ end
55
56
 
56
- def render_gem(name)
57
- row = rows.fetch(name)
58
- render_row(row)
59
- end
57
+ def render_gem(name)
58
+ row = rows.fetch(name)
59
+ render_row(row)
60
+ end
60
61
 
61
- def render
62
- lines = [render_header]
63
- rows.keys.sort.each { |name| lines << render_gem(name) }
64
- lines.join("\n")
65
- end
62
+ def render
63
+ lines = [render_header]
64
+ rows.keys.sort.each { |name| lines << render_gem(name) }
65
+ lines.join("\n")
66
+ end
66
67
 
67
- private
68
+ private
68
69
 
69
- attr_reader :column_widths, :pastel, :rows, :headers
70
+ attr_reader :column_widths, :pastel, :rows, :headers
70
71
 
71
- def render_row(row)
72
- row.zip(column_widths).map do |value, width|
73
- padding = width && (" " * (width - pastel.strip(value).length))
74
- "#{value}#{padding}"
75
- end.join(" ")
76
- end
72
+ def render_row(row)
73
+ row.zip(column_widths).map do |value, width|
74
+ padding = width && (" " * (width - pastel.strip(value).length))
75
+ "#{value}#{padding}"
76
+ end.join(" ")
77
+ end
77
78
 
78
- def calculate_column_widths
79
- rows_with_header = [headers, *rows.values]
80
- Array.new(headers.length - 1) do |i|
81
- rows_with_header.map { |values| pastel.strip(values[i]).length }.max
79
+ def calculate_column_widths
80
+ rows_with_header = [headers, *rows.values]
81
+ Array.new(headers.length - 1) do |i|
82
+ rows_with_header.map { |values| pastel.strip(values[i]).length }.max
83
+ end
82
84
  end
83
85
  end
84
86
  end
@@ -17,7 +17,13 @@ module BundleUpdateInteractive
17
17
  puts "Updating the following gems."
18
18
  puts Table.updatable(selected_gems).render
19
19
  puts
20
- updater.apply_updates(*selected_gems.keys)
20
+
21
+ if options.commit?
22
+ GitCommitter.new(updater).apply_updates_as_individual_commits(*selected_gems.keys)
23
+ else
24
+ updater.apply_updates(*selected_gems.keys)
25
+ end
26
+
21
27
  puts_gemfile_modified_notice if updater.modified_gemfile?
22
28
  rescue Exception => e # rubocop:disable Lint/RescueException
23
29
  handle_exception(e)
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shellwords"
4
+
5
+ module BundleUpdateInteractive
6
+ class GitCommitter
7
+ def initialize(updater)
8
+ @updater = updater
9
+ end
10
+
11
+ def apply_updates_as_individual_commits(*gem_names)
12
+ assert_git_executable!
13
+ assert_working_directory_clean!
14
+
15
+ gem_names.flatten.each do |name|
16
+ updates = updater.apply_updates(name)
17
+ updated_gem = updates[name] || updates.values.first
18
+ next if updated_gem.nil?
19
+
20
+ commit_message = format_commit_message(updated_gem)
21
+ system "git add Gemfile Gemfile.lock", exception: true
22
+ system "git commit -m #{commit_message.shellescape}", exception: true
23
+ end
24
+ end
25
+
26
+ def format_commit_message(outdated_gem)
27
+ [
28
+ "Update",
29
+ outdated_gem.name,
30
+ outdated_gem.current_version.to_s,
31
+ outdated_gem.current_git_version,
32
+ "→",
33
+ outdated_gem.updated_version.to_s,
34
+ outdated_gem.updated_git_version
35
+ ].compact.join(" ")
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :updater
41
+
42
+ def assert_git_executable!
43
+ success = begin
44
+ `git --version`
45
+ Process.last_status.success?
46
+ rescue SystemCallError
47
+ false
48
+ end
49
+ raise Error, "git could not be executed" unless success
50
+ end
51
+
52
+ def assert_working_directory_clean!
53
+ status = `git status --untracked-files=no --porcelain`.strip
54
+ return if status.empty?
55
+
56
+ raise Error, "`git status` reports uncommitted changes; please commit or stash them them first!\n#{status}"
57
+ end
58
+ end
59
+ end
@@ -18,6 +18,11 @@ module BundleUpdateInteractive
18
18
  def apply_updates(*gem_names)
19
19
  expanded_names = expand_gems_with_exact_dependencies(*gem_names)
20
20
  BundlerCommands.update_gems_conservatively(*expanded_names)
21
+
22
+ # Return the gems that were actually updated based on observed changes to the lock file
23
+ updated_gems = build_outdated_gems(File.read("Gemfile.lock"))
24
+ @current_lockfile = Lockfile.parse
25
+ updated_gems
21
26
  end
22
27
 
23
28
  # Overridden by Latest::Updater subclass
@@ -32,7 +37,11 @@ module BundleUpdateInteractive
32
37
  def find_updatable_gems
33
38
  return {} if candidate_gems && candidate_gems.empty?
34
39
 
35
- updated_lockfile = Lockfile.parse(BundlerCommands.read_updated_lockfile(*Array(candidate_gems)))
40
+ build_outdated_gems(BundlerCommands.read_updated_lockfile(*Array(candidate_gems)))
41
+ end
42
+
43
+ def build_outdated_gems(lockfile_contents)
44
+ updated_lockfile = Lockfile.parse(lockfile_contents)
36
45
  current_lockfile.entries.each_with_object({}) do |current_lockfile_entry, hash|
37
46
  name = current_lockfile_entry.name
38
47
  updated_lockfile_entry = updated_lockfile && updated_lockfile[name]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BundleUpdateInteractive
4
- VERSION = "0.8.1"
4
+ VERSION = "0.9.1"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bundle_update_interactive
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.1
4
+ version: 0.9.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Brictson
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-09-01 00:00:00.000000000 Z
11
+ date: 2024-11-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -131,6 +131,7 @@ files:
131
131
  - lib/bundle_update_interactive/cli/table.rb
132
132
  - lib/bundle_update_interactive/error.rb
133
133
  - lib/bundle_update_interactive/gemfile.rb
134
+ - lib/bundle_update_interactive/git_committer.rb
134
135
  - lib/bundle_update_interactive/http.rb
135
136
  - lib/bundle_update_interactive/latest/gem_requirement.rb
136
137
  - lib/bundle_update_interactive/latest/gemfile_editor.rb
@@ -167,7 +168,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
167
168
  - !ruby/object:Gem::Version
168
169
  version: '0'
169
170
  requirements: []
170
- rubygems_version: 3.5.18
171
+ rubygems_version: 3.5.23
171
172
  signing_key:
172
173
  specification_version: 4
173
174
  summary: Adds an update-interactive command to Bundler