summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorThe Bundler Bot <bot@bundler.io>2018-07-02 04:27:33 +0000
committerThe Bundler Bot <bot@bundler.io>2018-07-02 04:27:33 +0000
commit431a3f76868bdb9d4c94be25b28b0c7f26ee9f6e (patch)
tree37cfae8033789fb21d9b955d910db124014ba262 /lib
parent3a54aa9cc5bb7be003b963dd17ff47f52f069901 (diff)
parentabaa283466376d858436147495f011a331393d8d (diff)
downloadbundler-431a3f76868bdb9d4c94be25b28b0c7f26ee9f6e.tar.gz
Auto merge of #6513 - agrim123:agr-bundler-remove, r=indirect
Add `bundle remove` Features of the command implemented: - Multiple gems support ```bash $ bundle remove rack rails ``` - Remove any empty block that might occur after removing the gem or otherwise present Things yet to implement: - Add `rm` alias. _Optional_ - [x] Add `--install` flag to remove gems from `.bundle`. - [x] Handling multiple gems on the same line. - [x] Handle gem spec - [x] Handle eval_gemfile cases ([one](https://github.com/bundler/bundler/pull/6513#discussion_r195632603) case left) Closes #6506
Diffstat (limited to 'lib')
-rw-r--r--lib/bundler/cli.rb11
-rw-r--r--lib/bundler/cli/remove.rb18
-rw-r--r--lib/bundler/dependency.rb4
-rw-r--r--lib/bundler/dsl.rb3
-rw-r--r--lib/bundler/injector.rb167
-rw-r--r--lib/bundler/shared_helpers.rb6
6 files changed, 195 insertions, 14 deletions
diff --git a/lib/bundler/cli.rb b/lib/bundler/cli.rb
index 466fb0135a..f1ff3f4554 100644
--- a/lib/bundler/cli.rb
+++ b/lib/bundler/cli.rb
@@ -166,6 +166,17 @@ module Bundler
Check.new(options).run
end
+ desc "remove [GEM [GEM ...]]", "Removes gems from the Gemfile"
+ long_desc <<-D
+ Removes the given gems from the Gemfile while ensuring that the resulting Gemfile is still valid. If the gem is not found, Bundler prints a error message and if gem could not be removed due to any reason Bundler will display a warning.
+ D
+ method_option "install", :type => :boolean, :banner =>
+ "Runs 'bundle install' after removing the gems from the Gemfile"
+ def remove(*gems)
+ require "bundler/cli/remove"
+ Remove.new(gems, options).run
+ end
+
desc "install [OPTIONS]", "Install the current environment to the system"
long_desc <<-D
Install will install all of the gems in the current bundle, making them available
diff --git a/lib/bundler/cli/remove.rb b/lib/bundler/cli/remove.rb
new file mode 100644
index 0000000000..cd6a2cec28
--- /dev/null
+++ b/lib/bundler/cli/remove.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Bundler
+ class CLI::Remove
+ def initialize(gems, options)
+ @gems = gems
+ @options = options
+ end
+
+ def run
+ raise InvalidOption, "Please specify gems to remove." if @gems.empty?
+
+ Injector.remove(@gems, {})
+
+ Installer.install(Bundler.root, Bundler.definition) if @options["install"]
+ end
+ end
+end
diff --git a/lib/bundler/dependency.rb b/lib/bundler/dependency.rb
index 24257bc113..ec5081dee0 100644
--- a/lib/bundler/dependency.rb
+++ b/lib/bundler/dependency.rb
@@ -7,8 +7,7 @@ require "bundler/rubygems_ext"
module Bundler
class Dependency < Gem::Dependency
attr_reader :autorequire
- attr_reader :groups
- attr_reader :platforms
+ attr_reader :groups, :platforms, :gemfile
PLATFORM_MAP = {
:ruby => Gem::Platform::RUBY,
@@ -87,6 +86,7 @@ module Bundler
@platforms = Array(options["platforms"])
@env = options["env"]
@should_include = options.fetch("should_include", true)
+ @gemfile = options["gemfile"]
@autorequire = Array(options["require"] || []) if options.key?("require")
end
diff --git a/lib/bundler/dsl.rb b/lib/bundler/dsl.rb
index 37bfe36740..7467b5a537 100644
--- a/lib/bundler/dsl.rb
+++ b/lib/bundler/dsl.rb
@@ -16,7 +16,7 @@ module Bundler
VALID_PLATFORMS = Bundler::Dependency::PLATFORM_MAP.keys.freeze
VALID_KEYS = %w[group groups git path glob name branch ref tag require submodules
- platform platforms type source install_if].freeze
+ platform platforms type source install_if gemfile].freeze
attr_reader :gemspecs
attr_accessor :dependencies
@@ -93,6 +93,7 @@ module Bundler
def gem(name, *args)
options = args.last.is_a?(Hash) ? args.pop.dup : {}
+ options["gemfile"] = @gemfile
version = args || [">= 0"]
normalize_options(name, version, options)
diff --git a/lib/bundler/injector.rb b/lib/bundler/injector.rb
index 08f2ba90fd..5bbb6aeae0 100644
--- a/lib/bundler/injector.rb
+++ b/lib/bundler/injector.rb
@@ -2,13 +2,18 @@
module Bundler
class Injector
- def self.inject(new_deps, options = {})
- injector = new(new_deps, options)
+ def self.inject(deps, options = {})
+ injector = new(deps, options)
injector.inject(Bundler.default_gemfile, Bundler.default_lockfile)
end
- def initialize(new_deps, options = {})
- @new_deps = new_deps
+ 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
@@ -28,19 +33,18 @@ module Bundler
builder.eval_gemfile(gemfile_path)
# don't inject any gems that are already in the Gemfile
- @new_deps -= builder.dependencies
+ @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 @new_deps.any?
+ # 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 @new_deps.any?
+ 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)
@@ -49,7 +53,21 @@ module Bundler
Bundler.reset_paths!
# return an array of the deps that we added
- @new_deps
+ @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
@@ -76,7 +94,7 @@ module Bundler
end
def build_gem_lines(conservative_versioning)
- @new_deps.map do |d|
+ @deps.map do |d|
name = d.name.dump
requirement = if conservative_versioning
@@ -101,5 +119,132 @@ module Bundler
f.puts new_gem_lines
end
end
+
+ # evalutes 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 opertion if no gems were removed
+ # no need to operate on gemfile furthur
+ 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] path of the Gemfile
+ # @return [Array] removed_deps 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.include?(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)
+ # evalute 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
diff --git a/lib/bundler/shared_helpers.rb b/lib/bundler/shared_helpers.rb
index 2b9fb8d6a7..93e2a35b54 100644
--- a/lib/bundler/shared_helpers.rb
+++ b/lib/bundler/shared_helpers.rb
@@ -198,10 +198,12 @@ module Bundler
def pretty_dependency(dep, print_source = false)
msg = String.new(dep.name)
msg << " (#{dep.requirement})" unless dep.requirement == Gem::Requirement.default
+
if dep.is_a?(Bundler::Dependency)
platform_string = dep.platforms.join(", ")
msg << " " << platform_string if !platform_string.empty? && platform_string != Gem::Platform::RUBY
end
+
msg << " from the `#{dep.source}` source" if print_source && dep.source
msg
end
@@ -224,6 +226,10 @@ module Bundler
Digest(name)
end
+ def write_to_gemfile(gemfile_path, contents)
+ filesystem_access(gemfile_path) {|g| File.open(g, "w") {|file| file.puts contents } }
+ end
+
private
def validate_bundle_path