diff options
author | The Bundler Bot <bot@bundler.io> | 2018-07-02 04:27:33 +0000 |
---|---|---|
committer | The Bundler Bot <bot@bundler.io> | 2018-07-02 04:27:33 +0000 |
commit | 431a3f76868bdb9d4c94be25b28b0c7f26ee9f6e (patch) | |
tree | 37cfae8033789fb21d9b955d910db124014ba262 /lib | |
parent | 3a54aa9cc5bb7be003b963dd17ff47f52f069901 (diff) | |
parent | abaa283466376d858436147495f011a331393d8d (diff) | |
download | bundler-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.rb | 11 | ||||
-rw-r--r-- | lib/bundler/cli/remove.rb | 18 | ||||
-rw-r--r-- | lib/bundler/dependency.rb | 4 | ||||
-rw-r--r-- | lib/bundler/dsl.rb | 3 | ||||
-rw-r--r-- | lib/bundler/injector.rb | 167 | ||||
-rw-r--r-- | lib/bundler/shared_helpers.rb | 6 |
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 |