1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
|
# frozen_string_literal: true
module Bundler
class Injector
INJECTED_GEMS = "injected gems".freeze
def self.inject(new_deps, options = {})
injector = new(new_deps, options)
injector.inject(Bundler.default_gemfile, Bundler.default_lockfile)
end
def self.remove(gems, options = {})
injector = new(gems, options)
injector.remove(Bundler.default_gemfile, Bundler.default_lockfile)
end
def initialize(deps, options = {})
@deps = deps
@options = options
end
# @param [Pathname] gemfile_path The Gemfile in which to inject the new dependency.
# @param [Pathname] lockfile_path The lockfile in which to inject the new dependency.
# @return [Array]
def inject(gemfile_path, lockfile_path)
if Bundler.frozen_bundle?
# ensure the lock and Gemfile are synced
Bundler.definition.ensure_equivalent_gemfile_and_lockfile(true)
end
# temporarily unfreeze
Bundler.settings.temporary(:deployment => false, :frozen => false) do
# evaluate the Gemfile we have now
builder = Dsl.new
builder.eval_gemfile(gemfile_path)
# don't inject any gems that are already in the Gemfile
@deps -= builder.dependencies
# add new deps to the end of the in-memory Gemfile
# Set conservative versioning to false because
# we want to let the resolver resolve the version first
builder.eval_gemfile(INJECTED_GEMS, build_gem_lines(false)) if @deps.any?
# resolve to see if the new deps broke anything
@definition = builder.to_definition(lockfile_path, {})
@definition.resolve_remotely!
# since nothing broke, we can add those gems to the gemfile
append_to(gemfile_path, build_gem_lines(@options[:conservative_versioning])) if @deps.any?
# since we resolved successfully, write out the lockfile
@definition.lock(Bundler.default_lockfile)
# invalidate the cached Bundler.definition
Bundler.reset_paths!
# return an array of the deps that we added
@deps
end
end
# @param [Pathname] gemfile_path The Gemfile from which to remove dependencies.
# @param [Pathname] lockfile_path The lockfile from which to remove dependencies.
# @return [Array]
def remove(gemfile_path, lockfile_path)
# remove gems from each gemfiles we have
Bundler.definition.gemfiles.each do |path|
deps = remove_deps(path)
show_warning("No gems were removed from the gemfile.") if deps.empty?
deps.each {|dep| Bundler.ui.confirm "#{SharedHelpers.pretty_dependency(dep, false)} was removed." }
end
end
private
def conservative_version(spec)
version = spec.version
return ">= 0" if version.nil?
segments = version.segments
seg_end_index = version >= Gem::Version.new("1.0") ? 1 : 2
prerelease_suffix = version.to_s.gsub(version.release.to_s, "") if version.prerelease?
"#{version_prefix}#{segments[0..seg_end_index].join(".")}#{prerelease_suffix}"
end
def version_prefix
if @options[:strict]
"= "
elsif @options[:optimistic]
">= "
else
"~> "
end
end
def build_gem_lines(conservative_versioning)
@deps.map do |d|
name = d.name.dump
requirement = if conservative_versioning
", \"#{conservative_version(@definition.specs[d.name][0])}\""
else
", #{d.requirement.as_list.map(&:dump).join(", ")}"
end
if d.groups != Array(:default)
group = d.groups.size == 1 ? ", :group => #{d.groups.first.inspect}" : ", :groups => #{d.groups.inspect}"
end
source = ", :source => \"#{d.source}\"" unless d.source.nil?
git = ", :git => \"#{d.git}\"" unless d.git.nil?
branch = ", :branch => \"#{d.branch}\"" unless d.branch.nil?
%(gem #{name}#{requirement}#{group}#{source}#{git}#{branch})
end.join("\n")
end
def append_to(gemfile_path, new_gem_lines)
gemfile_path.open("a") do |f|
f.puts
f.puts new_gem_lines
end
end
# evaluates a gemfile to remove the specified gem
# from it.
def remove_deps(gemfile_path)
initial_gemfile = IO.readlines(gemfile_path)
Bundler.ui.info "Removing gems from #{gemfile_path}"
# evaluate the Gemfile we have
builder = Dsl.new
builder.eval_gemfile(gemfile_path)
removed_deps = remove_gems_from_dependencies(builder, @deps, gemfile_path)
# abort the operation if no gems were removed
# no need to operate on gemfile further
return [] if removed_deps.empty?
cleaned_gemfile = remove_gems_from_gemfile(@deps, gemfile_path)
SharedHelpers.write_to_gemfile(gemfile_path, cleaned_gemfile)
# check for errors
# including extra gems being removed
# or some gems not being removed
# and return the actual removed deps
cross_check_for_errors(gemfile_path, builder.dependencies, removed_deps, initial_gemfile)
end
# @param [Dsl] builder Dsl object of current Gemfile.
# @param [Array] gems Array of names of gems to be removed.
# @param [Pathname] gemfile_path Path of the Gemfile.
# @return [Array] Array of removed dependencies.
def remove_gems_from_dependencies(builder, gems, gemfile_path)
removed_deps = []
gems.each do |gem_name|
deleted_dep = builder.dependencies.find {|d| d.name == gem_name }
if deleted_dep.nil?
raise GemfileError, "`#{gem_name}` is not specified in #{gemfile_path} so it could not be removed."
end
builder.dependencies.delete(deleted_dep)
removed_deps << deleted_dep
end
removed_deps
end
# @param [Array] gems Array of names of gems to be removed.
# @param [Pathname] gemfile_path The Gemfile from which to remove dependencies.
def remove_gems_from_gemfile(gems, gemfile_path)
patterns = /gem\s+(['"])#{Regexp.union(gems)}\1|gem\s*\((['"])#{Regexp.union(gems)}\2\)/
# remove lines which match the regex
new_gemfile = IO.readlines(gemfile_path).reject {|line| line.match(patterns) }
# remove lone \n and append them with other strings
new_gemfile.each_with_index do |_line, index|
if new_gemfile[index + 1] == "\n"
new_gemfile[index] += new_gemfile[index + 1]
new_gemfile.delete_at(index + 1)
end
end
%w[group source env install_if].each {|block| remove_nested_blocks(new_gemfile, block) }
new_gemfile.join.chomp
end
# @param [Array] gemfile Array of gemfile contents.
# @param [String] block_name Name of block name to look for.
def remove_nested_blocks(gemfile, block_name)
nested_blocks = 0
# count number of nested blocks
gemfile.each_with_index {|line, index| nested_blocks += 1 if !gemfile[index + 1].nil? && gemfile[index + 1].include?(block_name) && line.include?(block_name) }
while nested_blocks >= 0
nested_blocks -= 1
gemfile.each_with_index do |line, index|
next unless !line.nil? && line.strip.start_with?(block_name)
if gemfile[index + 1] =~ /^\s*end\s*$/
gemfile[index] = nil
gemfile[index + 1] = nil
end
end
gemfile.compact!
end
end
# @param [Pathname] gemfile_path The Gemfile from which to remove dependencies.
# @param [Array] original_deps Array of original dependencies.
# @param [Array] removed_deps Array of removed dependencies.
# @param [Array] initial_gemfile Contents of original Gemfile before any operation.
def cross_check_for_errors(gemfile_path, original_deps, removed_deps, initial_gemfile)
# evaluate the new gemfile to look for any failure cases
builder = Dsl.new
builder.eval_gemfile(gemfile_path)
# record gems which were removed but not requested
extra_removed_gems = original_deps - builder.dependencies
# if some extra gems were removed then raise error
# and revert Gemfile to original
unless extra_removed_gems.empty?
SharedHelpers.write_to_gemfile(gemfile_path, initial_gemfile.join)
raise InvalidOption, "Gems could not be removed. #{extra_removed_gems.join(", ")} would also have been removed. Bundler cannot continue."
end
# record gems which could not be removed due to some reasons
errored_deps = builder.dependencies.select {|d| d.gemfile == gemfile_path } & removed_deps.select {|d| d.gemfile == gemfile_path }
show_warning "#{errored_deps.map(&:name).join(", ")} could not be removed." unless errored_deps.empty?
# return actual removed dependencies
removed_deps - errored_deps
end
def show_warning(message)
Bundler.ui.info Bundler.ui.add_color(message, :yellow)
end
end
end
|