bundle_update_interactive 0.8.1 → 0.9.1

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: '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