bundler-interactive 0.1.2.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +37 -0
- data/README.md +54 -1
- data/bundler-interactive.gemspec +1 -1
- data/lib/bundler/interactive/gemfile_editor.rb +119 -0
- data/lib/bundler/interactive/outdated_gem.rb +134 -23
- data/lib/bundler/interactive/update.rb +38 -7
- data/lib/bundler/interactive/update_interactive.rb +302 -37
- data/lib/bundler/interactive/version.rb +1 -1
- data/lib/bundler/interactive/version_resolver.rb +95 -0
- data/plugins.rb +5 -3
- metadata +8 -10
- data/.semaphore/semaphore.yml +0 -26
- data/.travis.yml +0 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6209ba5bd75ad342ff569bcbc4a134c55b290f1e31b6073e2d8d1e9a8378ace0
|
|
4
|
+
data.tar.gz: 5065c826088d8c4123c69876d233767c136a7c24a75807d6b1fba8fd78599edc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1d18e351b8b0293f2737718e053b26d7eda3f72a76e888ddbb5761cf92d3127db4bcdb02d0b4601733f28efd1da5d04e6552c5f74f972ad6808ce0d2c03db486
|
|
7
|
+
data.tar.gz: dcb27e24c6b779ddc9d0a3eceecc6d1e5cb1ae759d7768068a53c1894183531134227a1c1d841c98aa4f3594c97c8eaff85ea393ccdc703228982f3a480ca5a0
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches:
|
|
5
|
+
- master
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
name: Ruby ${{ matrix.ruby }} / Bundler ${{ matrix.bundler }}
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
timeout-minutes: 10
|
|
13
|
+
strategy:
|
|
14
|
+
fail-fast: false
|
|
15
|
+
matrix:
|
|
16
|
+
# Bundler 4.x requires Ruby >= 3.2.
|
|
17
|
+
ruby: ["3.2", "3.3", "3.4", "4.0"]
|
|
18
|
+
# This is a Bundler plugin, so the Bundler version is the axis that
|
|
19
|
+
# matters most: "default" exercises the Bundler that ships with each
|
|
20
|
+
# Ruby, and "latest" guards against API drift and covers the cooldown
|
|
21
|
+
# feature (Bundler >= 4.0.13).
|
|
22
|
+
bundler: [default, latest]
|
|
23
|
+
|
|
24
|
+
steps:
|
|
25
|
+
- name: Checkout code
|
|
26
|
+
uses: actions/checkout@v5
|
|
27
|
+
|
|
28
|
+
- name: Set up Ruby and bundle install
|
|
29
|
+
uses: ruby/setup-ruby@v1
|
|
30
|
+
with:
|
|
31
|
+
ruby-version: ${{ matrix.ruby }}
|
|
32
|
+
bundler: ${{ matrix.bundler }}
|
|
33
|
+
bundler-cache: true
|
|
34
|
+
rubygems: latest
|
|
35
|
+
|
|
36
|
+
- name: Run tests
|
|
37
|
+
run: bundle exec rspec
|
data/README.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# Bundler::Interactive
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A Bundler plugin that turns updating your gems into a quick, interactive pass —
|
|
4
|
+
think [`yarn upgrade-interactive`], but for your Gemfile. Instead of guessing
|
|
5
|
+
which gems are behind and editing version numbers by hand, you get a list you
|
|
6
|
+
can walk through and pick exactly what to bump.
|
|
4
7
|
|
|
5
8
|
## Installation
|
|
6
9
|
|
|
@@ -8,8 +11,57 @@ This is a plugin to Bundler which provides an interactive interface to manage up
|
|
|
8
11
|
|
|
9
12
|
## Usage
|
|
10
13
|
|
|
14
|
+
From a project with a Gemfile, run:
|
|
15
|
+
|
|
11
16
|
$ bundle update-interactive
|
|
12
17
|
|
|
18
|
+
(`bundle upgrade-interactive` works too — it's an alias, for muscle memory
|
|
19
|
+
either way.)
|
|
20
|
+
|
|
21
|
+
You'll see one row per outdated gem, showing its group (development, test, …)
|
|
22
|
+
and the versions you can move to:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
Select gems and target versions to update:
|
|
26
|
+
* = newer version held by cooldown
|
|
27
|
+
❯ rake (default) 12.0.0 [12.3.3] 13.4.2
|
|
28
|
+
rexml (development,test) 3.4.0 [3.4.1] 3.4.4*
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Each row offers up to three targets:
|
|
32
|
+
|
|
33
|
+
- **current** — what's installed now
|
|
34
|
+
- **in-range** — the newest version your Gemfile constraint already allows
|
|
35
|
+
- **latest** — the newest version published, even if it's beyond your constraint
|
|
36
|
+
|
|
37
|
+
### Keys
|
|
38
|
+
|
|
39
|
+
| Key | Action |
|
|
40
|
+
| -------------- | ------------------------------------------------- |
|
|
41
|
+
| `↑` / `↓` | move between gems |
|
|
42
|
+
| `←` / `→` | choose a target version for the highlighted gem |
|
|
43
|
+
| `space` | skip a gem (or restore it) |
|
|
44
|
+
| `enter` | apply your selections |
|
|
45
|
+
| `q` / `Ctrl-C` | cancel without changing anything |
|
|
46
|
+
|
|
47
|
+
The selected version is shown in `[brackets]`. By default each gem is set to its
|
|
48
|
+
in-range latest; gems where the only newer option is **beyond** your constraint
|
|
49
|
+
start out skipped, so pressing `enter` never changes your Gemfile unless you ask
|
|
50
|
+
it to.
|
|
51
|
+
|
|
52
|
+
When you hit `enter`, Bundler updates the lockfile for everything you picked. If
|
|
53
|
+
you chose a version that's outside the current constraint, the matching line in
|
|
54
|
+
your Gemfile is rewritten to allow it first.
|
|
55
|
+
|
|
56
|
+
### Cooldown
|
|
57
|
+
|
|
58
|
+
If you've enabled Bundler's [cooldown] setting (Bundler 4.0.13+), which holds
|
|
59
|
+
back gem versions for a few days after release as a supply-chain safeguard,
|
|
60
|
+
`bundle update-interactive` respects it: the versions you can pick are the ones
|
|
61
|
+
Bundler will actually install. A version that's newer but still inside the
|
|
62
|
+
cooldown window is shown with an asterisk (`*`) so you know an update is coming
|
|
63
|
+
once it clears.
|
|
64
|
+
|
|
13
65
|
## Development
|
|
14
66
|
|
|
15
67
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
@@ -22,3 +74,4 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/wesric
|
|
|
22
74
|
|
|
23
75
|
|
|
24
76
|
[`yarn upgrade-interactive`]: https://yarnpkg.com/lang/en/docs/cli/upgrade-interactive/
|
|
77
|
+
[cooldown]: https://blog.rubygems.org/2026/06/03/cooldown-let-new-gems-be-vetted.html
|
data/bundler-interactive.gemspec
CHANGED
|
@@ -39,7 +39,7 @@ Gem::Specification.new do |spec|
|
|
|
39
39
|
spec.required_ruby_version = '>= 2.3.0'
|
|
40
40
|
|
|
41
41
|
spec.add_development_dependency 'bundler', '>= 1.16'
|
|
42
|
-
spec.add_development_dependency 'rake', '
|
|
42
|
+
spec.add_development_dependency 'rake', '>= 10.0'
|
|
43
43
|
spec.add_development_dependency 'rspec', '~> 3.0'
|
|
44
44
|
|
|
45
45
|
spec.add_dependency 'tty-prompt'
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bundler
|
|
4
|
+
module Interactive
|
|
5
|
+
# Rewrites a gem's version requirement in the Gemfile when the user picks a
|
|
6
|
+
# version beyond the declared constraint.
|
|
7
|
+
#
|
|
8
|
+
# Bundler's own Injector can only append or remove entries, so this performs
|
|
9
|
+
# a targeted, in-place edit of the existing `gem "name", ...` line, keeping
|
|
10
|
+
# any groups, options, and trailing comments intact. When no editable line
|
|
11
|
+
# is found it warns and returns false rather than risk corrupting the file.
|
|
12
|
+
class GemfileEditor
|
|
13
|
+
def initialize(name, target_version, gemfile_path: Bundler.default_gemfile)
|
|
14
|
+
@name = name
|
|
15
|
+
@target = Gem::Version.new(target_version.to_s)
|
|
16
|
+
@gemfile_path = gemfile_path
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def rewrite!
|
|
20
|
+
content = File.read(@gemfile_path)
|
|
21
|
+
updated = replace_requirement(content)
|
|
22
|
+
|
|
23
|
+
if updated.nil?
|
|
24
|
+
warn_unfound
|
|
25
|
+
return false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
File.write(@gemfile_path, updated)
|
|
29
|
+
true
|
|
30
|
+
rescue StandardError => e
|
|
31
|
+
warn_failure(e)
|
|
32
|
+
false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# A pessimistic requirement that admits the chosen version, pinned to its
|
|
36
|
+
# major.minor (e.g. 8.0.1 -> "~> 8.0").
|
|
37
|
+
def new_requirement
|
|
38
|
+
"~> #{@target.segments.first(2).join('.')}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def replace_requirement(content)
|
|
44
|
+
pattern = /^(?<indent>[ \t]*)gem\s+(?<q>['"])#{Regexp.escape(@name)}\k<q>(?<rest>[^\n]*)$/
|
|
45
|
+
match = content.match(pattern)
|
|
46
|
+
return nil unless match
|
|
47
|
+
|
|
48
|
+
content.sub(pattern) do
|
|
49
|
+
"#{match[:indent]}gem #{match[:q]}#{@name}#{match[:q]}#{rebuild_rest(match[:rest])}"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Strips any existing version-requirement string literals that follow the
|
|
54
|
+
# gem name and inserts the new requirement, preserving option pairs and a
|
|
55
|
+
# trailing comment.
|
|
56
|
+
def rebuild_rest(rest)
|
|
57
|
+
code, comment = split_comment(rest)
|
|
58
|
+
remainder = strip_leading_versions(code.strip.sub(/\A,\s*/, ''))
|
|
59
|
+
|
|
60
|
+
parts = ["\"#{new_requirement}\""]
|
|
61
|
+
parts << remainder unless remainder.empty?
|
|
62
|
+
|
|
63
|
+
result = ", #{parts.join(', ')}"
|
|
64
|
+
comment.empty? ? result : "#{result} #{comment}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def split_comment(rest)
|
|
68
|
+
if (index = comment_index(rest))
|
|
69
|
+
[rest[0...index], rest[index..-1].strip]
|
|
70
|
+
else
|
|
71
|
+
[rest, '']
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Index of a `#` that begins a comment (i.e. one not inside a string).
|
|
76
|
+
def comment_index(rest)
|
|
77
|
+
in_string = nil
|
|
78
|
+
rest.each_char.with_index do |char, index|
|
|
79
|
+
if in_string
|
|
80
|
+
in_string = nil if char == in_string
|
|
81
|
+
elsif char == '"' || char == "'"
|
|
82
|
+
in_string = char
|
|
83
|
+
elsif char == '#'
|
|
84
|
+
return index
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def strip_leading_versions(code)
|
|
91
|
+
loop do
|
|
92
|
+
match = code.match(/\A(?<q>['"])(?<req>[^'"]*)\k<q>\s*,?\s*/)
|
|
93
|
+
break unless match && version_requirement?(match[:req])
|
|
94
|
+
|
|
95
|
+
code = code[match[0].length..-1]
|
|
96
|
+
end
|
|
97
|
+
code.strip
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def version_requirement?(string)
|
|
101
|
+
Gem::Requirement::PATTERN.match?(string.strip)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def warn_unfound
|
|
105
|
+
Bundler.ui.warn(
|
|
106
|
+
"bundler-interactive: could not find an editable `gem \"#{@name}\"` line in " \
|
|
107
|
+
"#{@gemfile_path}; updating #{@name} within its existing constraint instead."
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def warn_failure(error)
|
|
112
|
+
Bundler.ui.warn(
|
|
113
|
+
"bundler-interactive: failed to rewrite the Gemfile for #{@name} " \
|
|
114
|
+
"(#{error.message}); updating within its existing constraint instead."
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -2,54 +2,165 @@
|
|
|
2
2
|
|
|
3
3
|
module Bundler
|
|
4
4
|
module Interactive
|
|
5
|
+
# View-model for a single outdated gem row in the interactive table.
|
|
6
|
+
#
|
|
7
|
+
# It exposes an ordered, de-duplicated list of selectable version +choices+
|
|
8
|
+
# (current -> in-range latest -> absolute latest) and tracks which one the
|
|
9
|
+
# user has highlighted. The renderer cycles the selection with
|
|
10
|
+
# +cycle_left+/+cycle_right+ and reads +selected_version+ when applying.
|
|
5
11
|
class OutdatedGem
|
|
6
|
-
|
|
12
|
+
Choice = Struct.new(:version, :role, :held, :git, :ref, keyword_init: true)
|
|
7
13
|
|
|
8
|
-
def initialize(current_spec
|
|
14
|
+
def initialize(current_spec:, resolved_spec:, dependency:,
|
|
15
|
+
latest_version: nil, pending_version: nil,
|
|
16
|
+
cooldown_active: false)
|
|
9
17
|
@current_spec = current_spec
|
|
10
|
-
@
|
|
18
|
+
@resolved_spec = resolved_spec
|
|
11
19
|
@dependency = dependency
|
|
20
|
+
@latest_version = latest_version || resolved_spec.version
|
|
21
|
+
@pending_version = pending_version
|
|
22
|
+
@cooldown_active = cooldown_active
|
|
23
|
+
@selected_index = default_index
|
|
12
24
|
end
|
|
13
25
|
|
|
26
|
+
attr_reader :dependency, :latest_version, :pending_version, :selected_index
|
|
27
|
+
|
|
14
28
|
def name(padding = 0)
|
|
15
29
|
current_spec.name.ljust(padding)
|
|
16
30
|
end
|
|
17
31
|
|
|
32
|
+
def groups
|
|
33
|
+
dependency ? Array(dependency.groups) : []
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Only the noteworthy groups; the implicit :default group renders as blank.
|
|
37
|
+
def group_label
|
|
38
|
+
(groups.map(&:to_s) - ['default']).join(',')
|
|
39
|
+
end
|
|
40
|
+
|
|
18
41
|
def current_version
|
|
19
42
|
current_spec.version
|
|
20
43
|
end
|
|
21
44
|
|
|
22
|
-
def
|
|
23
|
-
|
|
45
|
+
def resolved_version
|
|
46
|
+
resolved_spec.version
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def choices
|
|
50
|
+
@choices ||= build_choices
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# True when there is anything to pick beyond the installed version.
|
|
54
|
+
def upgradable?
|
|
55
|
+
choices.size > 1
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def selected
|
|
59
|
+
choices[@selected_index]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def selected_version
|
|
63
|
+
selected.version
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def cycle_left
|
|
67
|
+
@selected_index = (@selected_index - 1) % choices.size
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def cycle_right
|
|
71
|
+
@selected_index = (@selected_index + 1) % choices.size
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def select_current!
|
|
75
|
+
@selected_index = choices.index { |choice| choice.role == :current } || 0
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Jump to the best upgrade target (in-range latest, else absolute latest).
|
|
79
|
+
def select_upgrade!
|
|
80
|
+
index = choices.index { |choice| choice.role == :in_range } ||
|
|
81
|
+
choices.index { |choice| choice.role == :latest }
|
|
82
|
+
@selected_index = index if index
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# A row is "skipped" (left untouched) when the highlighted choice is the
|
|
86
|
+
# currently installed version.
|
|
87
|
+
def skip?
|
|
88
|
+
selected.role == :current
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def git_changed?
|
|
92
|
+
current_git != resolved_git && !(current_git.nil? && resolved_git.nil?)
|
|
24
93
|
end
|
|
25
94
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
95
|
+
# The selected target is newer than the Gemfile constraint allows, so the
|
|
96
|
+
# Gemfile requirement must be rewritten before updating.
|
|
97
|
+
def cross_constraint?
|
|
98
|
+
return false unless dependency
|
|
99
|
+
return false if git_changed?
|
|
100
|
+
|
|
101
|
+
!dependency.requirement.satisfied_by?(selected_version)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Informational note for an in-range update that is being withheld by the
|
|
105
|
+
# cooldown window but is not otherwise visible as a selectable choice.
|
|
106
|
+
def cooldown_note
|
|
107
|
+
return nil unless @cooldown_active
|
|
108
|
+
return nil unless pending_version
|
|
109
|
+
return nil if choices.any?(&:held)
|
|
110
|
+
|
|
111
|
+
"#{pending_version} held by cooldown"
|
|
31
112
|
end
|
|
32
113
|
|
|
33
114
|
private
|
|
34
115
|
|
|
35
|
-
attr_reader :current_spec, :
|
|
116
|
+
attr_reader :current_spec, :resolved_spec
|
|
117
|
+
|
|
118
|
+
def build_choices
|
|
119
|
+
list = [Choice.new(version: current_version, role: :current, held: false, git: false, ref: short_ref(current_spec))]
|
|
120
|
+
|
|
121
|
+
if git_changed?
|
|
122
|
+
list << Choice.new(version: resolved_version, role: :in_range, held: false, git: true, ref: short_ref(resolved_spec))
|
|
123
|
+
elsif resolved_version > current_version
|
|
124
|
+
list << Choice.new(version: resolved_version, role: :in_range, held: held?(resolved_version), git: false, ref: nil)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
if latest_version > resolved_version && latest_version > current_version
|
|
128
|
+
list << Choice.new(version: latest_version, role: :latest, held: held?(latest_version), git: false, ref: nil)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
list.uniq { |choice| [choice.version, choice.git] }
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# The short git revision for a git-sourced spec (e.g. "380c0bc"), or nil.
|
|
135
|
+
def short_ref(spec)
|
|
136
|
+
return nil unless spec.respond_to?(:git_version)
|
|
137
|
+
|
|
138
|
+
ref = spec.git_version
|
|
139
|
+
ref && !ref.strip.empty? ? ref.strip : nil
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Default to the current version (skip), matching `yarn
|
|
143
|
+
# upgrade-interactive` — nothing is touched until you opt a gem in.
|
|
144
|
+
def default_index
|
|
145
|
+
choices.index { |choice| choice.role == :current } || 0
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# A version is held by cooldown when it satisfies the Gemfile constraint
|
|
149
|
+
# (so it is not merely blocked by the declared range) yet sits above the
|
|
150
|
+
# version resolution was willing to pick.
|
|
151
|
+
def held?(version)
|
|
152
|
+
return false unless @cooldown_active
|
|
153
|
+
return false unless dependency
|
|
36
154
|
|
|
37
|
-
|
|
38
|
-
dependency ? "requested #{dependency.requirement}" : ''
|
|
155
|
+
version > resolved_version && dependency.requirement.satisfied_by?(version)
|
|
39
156
|
end
|
|
40
157
|
|
|
41
|
-
def
|
|
42
|
-
|
|
43
|
-
index ? VERSION_NAMES[index] : 'GIT'
|
|
158
|
+
def current_git
|
|
159
|
+
current_spec.respond_to?(:git_version) ? current_spec.git_version : nil
|
|
44
160
|
end
|
|
45
161
|
|
|
46
|
-
def
|
|
47
|
-
|
|
48
|
-
.segments
|
|
49
|
-
.zip(current_version.segments)
|
|
50
|
-
.index do |active_segment, current_segment|
|
|
51
|
-
active_segment.to_i > current_segment.to_i
|
|
52
|
-
end
|
|
162
|
+
def resolved_git
|
|
163
|
+
resolved_spec.respond_to?(:git_version) ? resolved_spec.git_version : nil
|
|
53
164
|
end
|
|
54
165
|
end
|
|
55
166
|
end
|
|
@@ -2,18 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
module Bundler
|
|
4
4
|
module Interactive
|
|
5
|
+
# Applies the version selections the user made in the interactive table.
|
|
6
|
+
#
|
|
7
|
+
# Selections whose target exceeds the Gemfile constraint have their
|
|
8
|
+
# requirement rewritten first (via GemfileEditor); then every selected gem
|
|
9
|
+
# is unlocked and re-resolved through a fresh Bundler definition so the
|
|
10
|
+
# lockfile is updated in one pass.
|
|
5
11
|
class Update
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def initialize(gems)
|
|
9
|
-
@gems = gems
|
|
12
|
+
def initialize(selections)
|
|
13
|
+
@selections = Array(selections)
|
|
10
14
|
end
|
|
11
15
|
|
|
12
16
|
def update!
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
return if selections.empty?
|
|
18
|
+
|
|
19
|
+
rewrite_gemfile!
|
|
20
|
+
names = selections.map(&:name)
|
|
21
|
+
|
|
22
|
+
Bundler.ui.info("Updating #{names.join(', ')}...")
|
|
23
|
+
definition = Bundler.definition(gems: names)
|
|
24
|
+
definition.resolve_remotely!
|
|
16
25
|
Bundler::Installer.install(Bundler.root, definition)
|
|
26
|
+
definition.lock
|
|
27
|
+
|
|
28
|
+
print_summary
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
attr_reader :selections
|
|
34
|
+
|
|
35
|
+
def rewrite_gemfile!
|
|
36
|
+
selections.select(&:cross_constraint?).each do |selection|
|
|
37
|
+
GemfileEditor.new(selection.name, selection.selected_version).rewrite!
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def print_summary
|
|
42
|
+
Bundler.ui.confirm('Updated:')
|
|
43
|
+
selections.each do |selection|
|
|
44
|
+
Bundler.ui.confirm(
|
|
45
|
+
" #{selection.name} #{selection.current_version} -> #{selection.selected_version}"
|
|
46
|
+
)
|
|
47
|
+
end
|
|
17
48
|
end
|
|
18
49
|
end
|
|
19
50
|
end
|
|
@@ -1,60 +1,322 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'io/console'
|
|
4
|
+
|
|
3
5
|
begin
|
|
4
|
-
require 'tty-
|
|
6
|
+
require 'tty-reader'
|
|
5
7
|
rescue LoadError
|
|
6
|
-
# tty-prompt probably wasn't loaded when we tried to
|
|
7
|
-
#
|
|
8
|
+
# tty-reader (shipped with tty-prompt) probably wasn't loaded when we tried to
|
|
9
|
+
# install the plugin. For now, this is fine.
|
|
8
10
|
nil
|
|
9
11
|
end
|
|
10
12
|
|
|
11
13
|
module Bundler
|
|
12
14
|
module Interactive
|
|
15
|
+
# The `bundle update-interactive` command.
|
|
16
|
+
#
|
|
17
|
+
# Renders a navigable table of outdated gems: up/down move between gems,
|
|
18
|
+
# left/right cycle a gem's target version (current / in-range latest /
|
|
19
|
+
# absolute latest), space skips or restores a row, enter applies the
|
|
20
|
+
# selection, and q (or Ctrl-C) cancels.
|
|
13
21
|
class UpdateInteractive < Bundler::Plugin::API
|
|
22
|
+
KEY_UP = ["\e[A", "\eOA"].freeze
|
|
23
|
+
KEY_DOWN = ["\e[B", "\eOB"].freeze
|
|
24
|
+
KEY_RIGHT = ["\e[C", "\eOC"].freeze
|
|
25
|
+
KEY_LEFT = ["\e[D", "\eOD"].freeze
|
|
26
|
+
KEY_RETURN = ["\r", "\n"].freeze
|
|
27
|
+
KEY_SPACE = [' '].freeze
|
|
28
|
+
KEY_QUIT = ['q', "\e"].freeze
|
|
29
|
+
|
|
30
|
+
# Fixed version columns, mirroring `yarn upgrade-interactive`.
|
|
31
|
+
ROLE_ORDER = %i[current in_range latest].freeze
|
|
32
|
+
COLUMN_LABELS = { current: 'Current', in_range: 'Range', latest: 'Latest' }.freeze
|
|
33
|
+
|
|
34
|
+
# Semver bump levels, coloured like `yarn upgrade-interactive`.
|
|
35
|
+
LEVEL_COLOR = { major: '31', minor: '33', patch: '32' }.freeze # red / yellow / green
|
|
36
|
+
|
|
14
37
|
def exec(_command, _args)
|
|
15
|
-
|
|
16
|
-
'
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
38
|
+
if outdated_gems.empty?
|
|
39
|
+
Bundler.ui.info('Bundle up to date!')
|
|
40
|
+
return
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
selection = interactive_select
|
|
44
|
+
return Bundler.ui.info('Canceling...') if selection.nil?
|
|
45
|
+
|
|
46
|
+
chosen = selection.reject(&:skip?)
|
|
47
|
+
return Bundler.ui.info('No gems selected.') if chosen.empty?
|
|
48
|
+
|
|
49
|
+
Update.new(chosen).update!
|
|
20
50
|
rescue TTY::Reader::InputInterrupt
|
|
21
|
-
|
|
51
|
+
show_cursor
|
|
52
|
+
Bundler.ui.info("\nCanceling...")
|
|
22
53
|
end
|
|
23
54
|
|
|
24
|
-
def
|
|
25
|
-
outdated_gems
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
55
|
+
def outdated_gems
|
|
56
|
+
@outdated_gems ||= VersionResolver.new(
|
|
57
|
+
current_specs: current_gems,
|
|
58
|
+
resolved_definition: definition,
|
|
59
|
+
dependencies: dependencies,
|
|
60
|
+
cooldown_days: cooldown_days
|
|
61
|
+
).outdated_gems
|
|
31
62
|
end
|
|
32
63
|
|
|
33
64
|
private
|
|
34
65
|
|
|
35
|
-
|
|
36
|
-
|
|
66
|
+
# --- interaction loop ----------------------------------------------------
|
|
67
|
+
|
|
68
|
+
def interactive_select
|
|
69
|
+
@gems = outdated_gems
|
|
70
|
+
reader = TTY::Reader.new(interrupt: :error)
|
|
71
|
+
@cursor = 0
|
|
72
|
+
@offset = 0
|
|
73
|
+
|
|
74
|
+
hide_cursor
|
|
75
|
+
render(first: true)
|
|
76
|
+
|
|
77
|
+
loop do
|
|
78
|
+
case reader.read_keypress
|
|
79
|
+
when *KEY_UP then @cursor = (@cursor - 1) % @gems.size
|
|
80
|
+
when *KEY_DOWN then @cursor = (@cursor + 1) % @gems.size
|
|
81
|
+
when *KEY_LEFT then @gems[@cursor].cycle_left
|
|
82
|
+
when *KEY_RIGHT then @gems[@cursor].cycle_right
|
|
83
|
+
when *KEY_SPACE then toggle_skip(@gems[@cursor])
|
|
84
|
+
when *KEY_RETURN then return @gems
|
|
85
|
+
when *KEY_QUIT then return nil
|
|
86
|
+
end
|
|
87
|
+
render
|
|
88
|
+
end
|
|
89
|
+
ensure
|
|
90
|
+
show_cursor
|
|
37
91
|
end
|
|
38
92
|
|
|
39
|
-
def
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
93
|
+
def toggle_skip(gem)
|
|
94
|
+
gem.skip? ? gem.select_upgrade! : gem.select_current!
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# --- rendering -----------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
# Renders the header, a scrolling window of gem rows sized to the
|
|
100
|
+
# terminal, and a footer. Keeping the block within the terminal height is
|
|
101
|
+
# what lets the in-place `cursor_up` redraw work instead of scrolling the
|
|
102
|
+
# whole list off-screen on every keypress.
|
|
103
|
+
def render(first: false)
|
|
104
|
+
total = @gems.size
|
|
105
|
+
page = [page_size, total].min
|
|
106
|
+
adjust_viewport(total, page)
|
|
107
|
+
|
|
108
|
+
lines = header_lines
|
|
109
|
+
lines << column_header_line
|
|
110
|
+
(@offset...(@offset + page)).each { |i| lines << row_line(@gems[i], i == @cursor) }
|
|
111
|
+
lines << ''
|
|
112
|
+
lines << footer_line(total, page)
|
|
113
|
+
|
|
114
|
+
output = +''
|
|
115
|
+
output << cursor_up(@line_count) if !first && @line_count
|
|
116
|
+
lines.each { |line| output << "\e[2K" << line.rstrip << "\n" }
|
|
117
|
+
@line_count = lines.size
|
|
118
|
+
|
|
119
|
+
$stdout.print(output)
|
|
120
|
+
$stdout.flush
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def adjust_viewport(total, page)
|
|
124
|
+
@offset = @cursor if @cursor < @offset
|
|
125
|
+
@offset = @cursor - page + 1 if @cursor > @offset + page - 1
|
|
126
|
+
max_offset = [total - page, 0].max
|
|
127
|
+
@offset = [[@offset, max_offset].min, 0].max
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def page_size
|
|
131
|
+
@page_size ||= [terminal_rows - header_lines.size - 4, 3].max
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def terminal_rows
|
|
135
|
+
(IO.console && IO.console.winsize.first) || 24
|
|
136
|
+
rescue StandardError
|
|
137
|
+
24
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def header_lines
|
|
141
|
+
lines = [
|
|
142
|
+
colorize('Select gems and target versions to update:', '1'),
|
|
143
|
+
dim('↑/↓ move · ←/→ choose version · space skip/restore · enter apply · q cancel'),
|
|
144
|
+
legend_line
|
|
145
|
+
]
|
|
146
|
+
lines << dim('* = newer version held by cooldown') if cooldown_present?
|
|
147
|
+
lines << ''
|
|
148
|
+
lines
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def legend_line
|
|
152
|
+
levels = %i[major minor patch].map { |level| color_for_level(level.to_s, level) }
|
|
153
|
+
dim('diff: ') + levels.join(dim(' · '))
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def cooldown_present?
|
|
157
|
+
return @cooldown_present if defined?(@cooldown_present)
|
|
158
|
+
|
|
159
|
+
@cooldown_present = outdated_gems.any? do |gem|
|
|
160
|
+
gem.cooldown_note || gem.choices.any?(&:held)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def column_header_line
|
|
165
|
+
name_blank = ' ' * (2 + name_padding)
|
|
166
|
+
cells = present_roles.map { |role| bold(COLUMN_LABELS[role].ljust(column_width(role))) }
|
|
167
|
+
cells << bold('Group') if show_group?
|
|
168
|
+
"#{name_blank} #{cells.join(' ')}".rstrip
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def row_line(gem, active)
|
|
172
|
+
pointer = active ? '❯ ' : ' '
|
|
173
|
+
cells = present_roles.map do |role|
|
|
174
|
+
choice = cell_for(gem, role)
|
|
175
|
+
version_cell(choice, choice && gem.selected.equal?(choice), column_width(role), gem.current_version)
|
|
49
176
|
end
|
|
177
|
+
cells << gem.group_label if show_group?
|
|
178
|
+
note = gem.cooldown_note ? " #{dim("← #{gem.cooldown_note}")}" : ''
|
|
179
|
+
|
|
180
|
+
"#{pointer}#{gem.name(name_padding)} #{cells.join(' ')}#{note}"
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Show the Group column only when at least one gem belongs to a group
|
|
184
|
+
# other than the implicit :default.
|
|
185
|
+
def show_group?
|
|
186
|
+
return @show_group if defined?(@show_group)
|
|
187
|
+
|
|
188
|
+
@show_group = outdated_gems.any? { |gem| !gem.group_label.empty? }
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Renders one version into a fixed-width column with a radio marker
|
|
192
|
+
# (● selected / ○ not), colouring the changed portion of the version by
|
|
193
|
+
# bump level so the table shows patch/minor/major at a glance. Padding is
|
|
194
|
+
# computed from the visible (uncoloured) text so columns stay aligned.
|
|
195
|
+
def version_cell(choice, selected, width, current)
|
|
196
|
+
return ' ' * width if choice.nil?
|
|
197
|
+
|
|
198
|
+
marker = selected ? '●' : '○'
|
|
199
|
+
ref = choice.ref ? " @#{choice.ref}" : ''
|
|
200
|
+
held = choice.held ? '*' : ''
|
|
201
|
+
visible = "#{marker} #{version_text(choice)}"
|
|
202
|
+
marker_out = selected ? color_for_level(marker, diff_level(choice.version, current)) : dim(marker)
|
|
203
|
+
|
|
204
|
+
"#{marker_out} #{colorize_diff(choice.version, current)}#{dim(ref)}#{held}" +
|
|
205
|
+
(' ' * [width - visible.length, 0].max)
|
|
50
206
|
end
|
|
51
207
|
|
|
208
|
+
# Returns the version string with the segments that differ from +current+
|
|
209
|
+
# (and everything after) coloured by bump level; the unchanged prefix is
|
|
210
|
+
# left plain. e.g. 8.1 -> 8.1.3 colours ".3" green, 7 -> 7.29.7 ".29.7"
|
|
211
|
+
# yellow, 4 -> 5.3.0 the whole thing red.
|
|
212
|
+
def colorize_diff(version, current)
|
|
213
|
+
index = diff_index(version, current)
|
|
214
|
+
return version.to_s if index.nil?
|
|
215
|
+
|
|
216
|
+
segments = version.segments
|
|
217
|
+
prefix = segments[0...index].join('.')
|
|
218
|
+
suffix = color_for_level(segments[index..-1].join('.'), diff_level(version, current))
|
|
219
|
+
prefix.empty? ? suffix : "#{prefix}.#{suffix}"
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def diff_index(version, current)
|
|
223
|
+
current_segments = current.segments
|
|
224
|
+
version.segments.each_index.find { |i| version.segments[i] != current_segments[i] }
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def diff_level(version, current)
|
|
228
|
+
case diff_index(version, current)
|
|
229
|
+
when nil then nil
|
|
230
|
+
when 0 then :major
|
|
231
|
+
when 1 then :minor
|
|
232
|
+
else :patch
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def color_for_level(text, level)
|
|
237
|
+
level ? colorize(text, LEVEL_COLOR[level]) : text
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def version_text(choice)
|
|
241
|
+
ref = choice.ref ? " @#{choice.ref}" : ''
|
|
242
|
+
"#{choice.version}#{ref}#{choice.held ? '*' : ''}"
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def cell_for(gem, role)
|
|
246
|
+
gem.choices.find { |choice| choice.role == role }
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def present_roles
|
|
250
|
+
@present_roles ||= ROLE_ORDER.select do |role|
|
|
251
|
+
outdated_gems.any? { |gem| cell_for(gem, role) }
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def column_width(role)
|
|
256
|
+
@column_widths ||= {}
|
|
257
|
+
# +2 for the "● " radio marker that precedes each version.
|
|
258
|
+
@column_widths[role] ||= outdated_gems.map do |gem|
|
|
259
|
+
(choice = cell_for(gem, role)) ? version_text(choice).length + 2 : 0
|
|
260
|
+
end.push(COLUMN_LABELS[role].length).max
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def footer_line(total, page)
|
|
264
|
+
count = @gems.count { |gem| !gem.skip? }
|
|
265
|
+
parts = ["#{count} of #{total} selected to update"]
|
|
266
|
+
parts << "showing #{@offset + 1}-#{@offset + page} of #{total}" if total > page
|
|
267
|
+
dim(parts.join(' · '))
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def bold(text)
|
|
271
|
+
colorize(text, '1')
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def cursor_up(count)
|
|
275
|
+
count.positive? ? "\e[#{count}A" : ''
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def hide_cursor
|
|
279
|
+
$stdout.print("\e[?25l") if color?
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def show_cursor
|
|
283
|
+
$stdout.print("\e[?25h") if color?
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def colorize(text, code)
|
|
287
|
+
color? ? "\e[#{code}m#{text}\e[0m" : text
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def dim(text)
|
|
291
|
+
colorize(text, '2')
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def color?
|
|
295
|
+
$stdout.tty?
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def name_padding
|
|
299
|
+
@name_padding ||= outdated_gems.map { |gem| gem.name.length }.max
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# --- bundler data --------------------------------------------------------
|
|
303
|
+
|
|
52
304
|
def current_gems
|
|
53
305
|
@current_gems ||= Bundler.definition.tap(&:validate_runtime!).resolve
|
|
54
306
|
end
|
|
55
307
|
|
|
308
|
+
# Adapts a resolved Bundler SpecSet to the lookup the resolver expects.
|
|
309
|
+
# (Bundler::Definition#find_resolved_spec was removed in Bundler 4.x.)
|
|
310
|
+
ResolvedSpecs = Struct.new(:spec_set) do
|
|
311
|
+
def find_resolved_spec(current_spec)
|
|
312
|
+
spec_set.find_by_name_and_platform(current_spec.name, current_spec.platform)
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
56
316
|
def definition
|
|
57
|
-
@definition ||=
|
|
317
|
+
@definition ||= ResolvedSpecs.new(
|
|
318
|
+
Bundler.definition(true).tap(&:resolve_remotely!).resolve
|
|
319
|
+
)
|
|
58
320
|
end
|
|
59
321
|
|
|
60
322
|
def dependencies
|
|
@@ -62,15 +324,18 @@ module Bundler
|
|
|
62
324
|
Bundler.load.dependencies.map { |dep| [dep.name, dep] }.to_h
|
|
63
325
|
end
|
|
64
326
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
puts "Gem #{current_spec.name}, couldn't find an active_spec"
|
|
70
|
-
end
|
|
327
|
+
# Cooldown is opt-in and only exists on Bundler >= 4.0.13, so read it
|
|
328
|
+
# defensively and treat anything unparseable as "off".
|
|
329
|
+
def cooldown_days
|
|
330
|
+
return @cooldown_days if defined?(@cooldown_days)
|
|
71
331
|
|
|
72
|
-
|
|
73
|
-
|
|
332
|
+
@cooldown_days =
|
|
333
|
+
begin
|
|
334
|
+
value = Bundler.settings[:cooldown]
|
|
335
|
+
value.nil? ? 0 : value.to_i
|
|
336
|
+
rescue StandardError
|
|
337
|
+
0
|
|
338
|
+
end
|
|
74
339
|
end
|
|
75
340
|
end
|
|
76
341
|
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bundler
|
|
4
|
+
module Interactive
|
|
5
|
+
# Gathers the candidate versions for every outdated gem.
|
|
6
|
+
#
|
|
7
|
+
# For each installed gem it computes:
|
|
8
|
+
# * the currently installed (locked) version,
|
|
9
|
+
# * the in-range latest that dependency resolution selected (which already
|
|
10
|
+
# respects the Gemfile constraint and, when configured, the cooldown
|
|
11
|
+
# window),
|
|
12
|
+
# * the absolute latest published version from the gem's source, and
|
|
13
|
+
# * the in-range version held back by cooldown, if any.
|
|
14
|
+
#
|
|
15
|
+
# Collaborators are injected so the resolver can be unit-tested without a
|
|
16
|
+
# live Bundler environment.
|
|
17
|
+
class VersionResolver
|
|
18
|
+
def initialize(current_specs:, resolved_definition:, dependencies:, cooldown_days: 0)
|
|
19
|
+
@current_specs = current_specs
|
|
20
|
+
@resolved_definition = resolved_definition
|
|
21
|
+
@dependencies = dependencies
|
|
22
|
+
@cooldown_days = cooldown_days.to_i
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def outdated_gems
|
|
26
|
+
current_specs.sort_by(&:name).each_with_object([]) do |current_spec, list|
|
|
27
|
+
resolved = resolved_definition.find_resolved_spec(current_spec)
|
|
28
|
+
next unless resolved
|
|
29
|
+
|
|
30
|
+
gem = build(current_spec, resolved)
|
|
31
|
+
# A gem is worth listing when it offers any upgrade target — an
|
|
32
|
+
# in-range bump, a newer git revision, or a version beyond the
|
|
33
|
+
# declared constraint (which `bundle outdated` surfaces too).
|
|
34
|
+
list << gem if gem.upgradable?
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
attr_reader :current_specs, :resolved_definition, :dependencies, :cooldown_days
|
|
41
|
+
|
|
42
|
+
def build(current_spec, resolved)
|
|
43
|
+
dependency = dependencies[current_spec.name]
|
|
44
|
+
available = available_versions(current_spec, resolved)
|
|
45
|
+
|
|
46
|
+
OutdatedGem.new(
|
|
47
|
+
current_spec: current_spec,
|
|
48
|
+
resolved_spec: resolved,
|
|
49
|
+
dependency: dependency,
|
|
50
|
+
latest_version: available.max || resolved.version,
|
|
51
|
+
pending_version: pending_version(resolved, dependency, available),
|
|
52
|
+
cooldown_active: cooldown_active?
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# All published versions for the gem, straight from the source index (this
|
|
57
|
+
# list is *not* cooldown-filtered, which is what lets us detect held
|
|
58
|
+
# versions). Prereleases are dropped unless the installed gem is itself a
|
|
59
|
+
# prerelease, mirroring `bundle outdated`.
|
|
60
|
+
def available_versions(current_spec, resolved)
|
|
61
|
+
specs = resolved.source.specs.search(resolved.name)
|
|
62
|
+
specs = specs.select { |spec| installable?(spec, current_spec) }
|
|
63
|
+
versions = specs.map(&:version)
|
|
64
|
+
versions = versions.reject(&:prerelease?) unless current_spec.version.prerelease?
|
|
65
|
+
versions.uniq.sort
|
|
66
|
+
rescue StandardError
|
|
67
|
+
[]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def installable?(spec, current_spec)
|
|
71
|
+
return true unless spec.respond_to?(:installable_on_platform?)
|
|
72
|
+
|
|
73
|
+
spec.installable_on_platform?(current_spec.platform)
|
|
74
|
+
rescue StandardError
|
|
75
|
+
true
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# The highest in-range version available in the source that resolution
|
|
79
|
+
# refused to pick — i.e. a version waiting out the cooldown window.
|
|
80
|
+
def pending_version(resolved, dependency, available)
|
|
81
|
+
return nil unless cooldown_active?
|
|
82
|
+
return nil unless dependency
|
|
83
|
+
|
|
84
|
+
candidate = available.select { |version| dependency.requirement.satisfied_by?(version) }.max
|
|
85
|
+
return nil unless candidate
|
|
86
|
+
|
|
87
|
+
candidate > resolved.version ? candidate : nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def cooldown_active?
|
|
91
|
+
cooldown_days.positive?
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
data/plugins.rb
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
require 'bundler/interactive'
|
|
4
4
|
|
|
5
|
-
Bundler
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
# `update-interactive` matches Bundler's own verb (`bundle update`);
|
|
6
|
+
# `upgrade-interactive` is an alias for parity with `yarn upgrade-interactive`.
|
|
7
|
+
%w[update-interactive upgrade-interactive].each do |command|
|
|
8
|
+
Bundler::Plugin::API.command(command, Bundler::Interactive::UpdateInteractive)
|
|
9
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bundler-interactive
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Wes Rich
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: exe
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: bundler
|
|
@@ -28,14 +27,14 @@ dependencies:
|
|
|
28
27
|
name: rake
|
|
29
28
|
requirement: !ruby/object:Gem::Requirement
|
|
30
29
|
requirements:
|
|
31
|
-
- - "
|
|
30
|
+
- - ">="
|
|
32
31
|
- !ruby/object:Gem::Version
|
|
33
32
|
version: '10.0'
|
|
34
33
|
type: :development
|
|
35
34
|
prerelease: false
|
|
36
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
36
|
requirements:
|
|
38
|
-
- - "
|
|
37
|
+
- - ">="
|
|
39
38
|
- !ruby/object:Gem::Version
|
|
40
39
|
version: '10.0'
|
|
41
40
|
- !ruby/object:Gem::Dependency
|
|
@@ -73,11 +72,10 @@ executables: []
|
|
|
73
72
|
extensions: []
|
|
74
73
|
extra_rdoc_files: []
|
|
75
74
|
files:
|
|
75
|
+
- ".github/workflows/ci.yml"
|
|
76
76
|
- ".gitignore"
|
|
77
77
|
- ".rspec"
|
|
78
78
|
- ".rubocop.yml"
|
|
79
|
-
- ".semaphore/semaphore.yml"
|
|
80
|
-
- ".travis.yml"
|
|
81
79
|
- Gemfile
|
|
82
80
|
- README.md
|
|
83
81
|
- Rakefile
|
|
@@ -85,10 +83,12 @@ files:
|
|
|
85
83
|
- bin/setup
|
|
86
84
|
- bundler-interactive.gemspec
|
|
87
85
|
- lib/bundler/interactive.rb
|
|
86
|
+
- lib/bundler/interactive/gemfile_editor.rb
|
|
88
87
|
- lib/bundler/interactive/outdated_gem.rb
|
|
89
88
|
- lib/bundler/interactive/update.rb
|
|
90
89
|
- lib/bundler/interactive/update_interactive.rb
|
|
91
90
|
- lib/bundler/interactive/version.rb
|
|
91
|
+
- lib/bundler/interactive/version_resolver.rb
|
|
92
92
|
- plugins.rb
|
|
93
93
|
homepage: https://github.com/wesrich/Bundler-Interactive
|
|
94
94
|
licenses: []
|
|
@@ -97,7 +97,6 @@ metadata:
|
|
|
97
97
|
homepage_uri: https://github.com/wesrich/Bundler-Interactive
|
|
98
98
|
source_code_uri: https://github.com/wesrich/Bundler-Interactive
|
|
99
99
|
changelog_uri: https://github.com/wesrich/Bundler-Interactive
|
|
100
|
-
post_install_message:
|
|
101
100
|
rdoc_options: []
|
|
102
101
|
require_paths:
|
|
103
102
|
- lib
|
|
@@ -112,8 +111,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
112
111
|
- !ruby/object:Gem::Version
|
|
113
112
|
version: '0'
|
|
114
113
|
requirements: []
|
|
115
|
-
rubygems_version:
|
|
116
|
-
signing_key:
|
|
114
|
+
rubygems_version: 4.0.10
|
|
117
115
|
specification_version: 4
|
|
118
116
|
summary: Interactive prompt for Bundler
|
|
119
117
|
test_files: []
|
data/.semaphore/semaphore.yml
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
version: v1.0
|
|
2
|
-
name: Bundler::Interactive
|
|
3
|
-
agent:
|
|
4
|
-
machine:
|
|
5
|
-
type: e1-standard-2
|
|
6
|
-
os_image: ubuntu1804
|
|
7
|
-
|
|
8
|
-
blocks:
|
|
9
|
-
- name: Build
|
|
10
|
-
execution_time_limit:
|
|
11
|
-
minutes: 1
|
|
12
|
-
task:
|
|
13
|
-
jobs:
|
|
14
|
-
- name: Tests
|
|
15
|
-
matrix:
|
|
16
|
-
- env_var: RUBY_VERSION
|
|
17
|
-
values: ['2.3', '2.4', '2.5', '2.6.3']
|
|
18
|
-
commands:
|
|
19
|
-
- checkout
|
|
20
|
-
- sem-version ruby $RUBY_VERSION
|
|
21
|
-
|
|
22
|
-
# - cache restore gems-$RUBY_VERSION,gems-
|
|
23
|
-
# - bundle check --path=vendor/bundle || bundle install
|
|
24
|
-
# - cache store gems-$RUBY_VERSION vendor/bundle
|
|
25
|
-
- bundle install
|
|
26
|
-
- bundle exec rspec --format documentation
|