diff --git a/lib/bundler/cli.rb b/lib/bundler/cli.rb index f1ff3f45543..44881dad124 100644 --- a/lib/bundler/cli.rb +++ b/lib/bundler/cli.rb @@ -350,11 +350,24 @@ def binstubs(*gems) "Adds gem to the Gemfile but does not install it" method_option "optimistic", :type => :boolean, :banner => "Adds optimistic declaration of version to gem" method_option "strict", :type => :boolean, :banner => "Adds strict declaration of version to gem" + method_option "pessimistic", :type => :boolean, :banner => "Adds pessimistic declaration of version to gem" def add(*gems) require "bundler/cli/add" Add.new(options.dup, gems).run end + desc "change GEM [OPTIONS]", "Changes properties of a gem" + long_desc <<-D + Provide flexibilty of editing gemfile from command line by providing option to change gem properties. + D + method_option "version", :type => :string + method_option "group", :type => :string + method_option "source", :type => :string + def change(gem_name) + require "bundler/cli/change" + Change.new(options.dup, gem_name).run + end + desc "outdated GEM [OPTIONS]", "List installed gems with newer versions available" long_desc <<-D Outdated lists the names and versions of gems that have a newer version available diff --git a/lib/bundler/cli/add.rb b/lib/bundler/cli/add.rb index 9709e71be04..be4f39d5780 100644 --- a/lib/bundler/cli/add.rb +++ b/lib/bundler/cli/add.rb @@ -5,7 +5,7 @@ class CLI::Add def initialize(options, gems) @gems = gems @options = options - @options[:group] = @options[:group].split(",").map(&:strip) if !@options[:group].nil? && !@options[:group].empty? + @options["group"] = @options["group"].split(",").map(&:strip) if !@options["group"].nil? && !@options["group"].empty? end def run @@ -27,7 +27,8 @@ def run Injector.inject(dependencies, :conservative_versioning => @options[:version].nil?, # Perform conservative versioning only when version is not specified :optimistic => @options[:optimistic], - :strict => @options[:strict]) + :strict => @options[:strict], + :pessimistic => @options[:pessimistic]) Installer.install(Bundler.root, Bundler.definition) unless @options["skip-install"] end diff --git a/lib/bundler/cli/change.rb b/lib/bundler/cli/change.rb new file mode 100644 index 00000000000..490fc8b7432 --- /dev/null +++ b/lib/bundler/cli/change.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +module Bundler + class CLI::Change + def initialize(options, gem_name) + @gem_name = gem_name + @options = options + end + + def run + raise InvalidOption, "Please supply at least one option to change." unless @options[:group] || @options[:version] || @options[:source] + + dep = Bundler.definition.dependencies.find {|d| d.name == @gem_name } + + raise InvalidOption, "`#{@gem_name}` could not be found in the Gemfile." unless dep + + check_for_unsupported_options(dep) + + add_options = {} + + initial_gemfile = IO.readlines(Bundler.default_gemfile) + + set_group_options(dep.groups, add_options) + + set_version_options(dep.requirement, add_options) + + set_source_options(dep.options[:source], add_options) + + begin + require "bundler/cli/remove" + CLI::Remove.new([@gem_name], {}).run + + require "bundler/cli/add" + CLI::Add.new(add_options, [@gem_name]).run + rescue StandardError => e + SharedHelpers.write_file(Bundler.default_gemfile, initial_gemfile) + raise e + end + end + + private + + # If version of the gem is specified in Gemfile then we preserve + # it and the prefix + # else if @options[:version] is present then we prefer strict version + # and for a empty version we let resolver get version and set as pessimistic + # + # @param [requirement] requirement requirement of the gem. + # @param [Hash] add_options Options to pass to add command + # @return + def set_version_options(requirement, add_options) + req = requirement.requirements[0] + version_prefix = req[0] + version = req[1].to_s + case version_prefix + when "=" + add_options[:strict] = true + when ">=" + add_options[:optimistic] = true unless version == "0" + else + add_options[:pessimistic] = true + end + + add_options[:version] = if @options[:version].nil? + version.to_i.zero? ? nil : version + else + @options[:version] + end + end + + # @param [groups] groups Groups of the gem. + # @param [Hash] add_options Options to pass to add command + # @return + def set_group_options(groups, add_options) + if @options[:group] + uniq_groups = @options[:group].split(",").uniq + common_groups = uniq_groups & groups.map(&:to_s) + + Bundler.ui.warn "`#{@gem_name}` is already present in `#{common_groups.join(",")}`." unless common_groups.empty? + + add_options["group"] = uniq_groups.join(",") + else + add_options["group"] = groups.map(&:to_s).join(",") + end + end + + def set_source_options(source, add_options) + add_options["source"] = source.options["remotes"].first if source.is_a?(Bundler::Source) + + add_options["source"] = @options[:source] if @options[:source] + end + + def check_for_unsupported_options(dep) + gem_options = dep.options.delete_if {|_k, v| v.nil? || (v.is_a?(Array) && v.empty?) } + + raise InvalidOption, "`git` is not yet supported." if dep.options[:source].is_a?(Bundler::Source::Git) + + raise InvalidOption, "`platforms` is not yet supported." if gem_options[:platforms] + + raise InvalidOption, "`env` is not yet supported." if gem_options[:env] + end + end +end diff --git a/lib/bundler/dependency.rb b/lib/bundler/dependency.rb index ec5081dee01..53be4bb8aca 100644 --- a/lib/bundler/dependency.rb +++ b/lib/bundler/dependency.rb @@ -6,8 +6,7 @@ module Bundler class Dependency < Gem::Dependency - attr_reader :autorequire - attr_reader :groups, :platforms, :gemfile + attr_reader :groups, :platforms, :gemfile, :autorequire PLATFORM_MAP = { :ruby => Gem::Platform::RUBY, @@ -134,5 +133,9 @@ def specific? rescue NoMethodError requirement != ">= 0" end + + def options + { :name => @name, :requirement => @requirement, :groups => @groups.reject {|g| g == :default }, :platforms => @platforms, :env => @env, :source => @source } + end end end diff --git a/lib/bundler/injector.rb b/lib/bundler/injector.rb index 1bb29f0b367..550bf0bc9e8 100644 --- a/lib/bundler/injector.rb +++ b/lib/bundler/injector.rb @@ -47,7 +47,7 @@ def inject(gemfile_path, 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? + append_to(gemfile_path, build_gem_lines(@options[:conservative_versioning] || @options[:pessimistic] || @options[:optimistic])) if @deps.any? # since we resolved successfully, write out the lockfile @definition.lock(Bundler.default_lockfile) @@ -142,7 +142,7 @@ def remove_deps(gemfile_path) cleaned_gemfile = remove_gems_from_gemfile(@deps, gemfile_path) - SharedHelpers.write_to_gemfile(gemfile_path, cleaned_gemfile) + SharedHelpers.write_file(gemfile_path, cleaned_gemfile) # check for errors # including extra gems being removed @@ -232,7 +232,7 @@ def cross_check_for_errors(gemfile_path, original_deps, removed_deps, initial_ge # 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) + SharedHelpers.write_file(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 diff --git a/lib/bundler/shared_helpers.rb b/lib/bundler/shared_helpers.rb index 7ff391ab601..1f1d2ec69db 100644 --- a/lib/bundler/shared_helpers.rb +++ b/lib/bundler/shared_helpers.rb @@ -226,8 +226,10 @@ def digest(name) Digest(name) end - def write_to_gemfile(gemfile_path, contents) - filesystem_access(gemfile_path) {|g| File.open(g, "w") {|file| file.puts contents } } + # @param [Pathname] path Path to file. + # @param [String] contents Content to written to file. + def write_file(path, contents) + filesystem_access(path) {|g| File.open(g, "w") {|file| file.puts contents } } end private diff --git a/man/bundle-change.ronn b/man/bundle-change.ronn new file mode 100644 index 00000000000..476cfda68fc --- /dev/null +++ b/man/bundle-change.ronn @@ -0,0 +1,31 @@ +bundle-change(1) -- Changes properties of a gem +================================================================ + +## SYNOPSIS + +`bundle change` [--group=GROUP] [--version=VERSION] [--source=SOURCE] + +## DESCRIPTION +Provide flexibilty of editing gemfile from command line by providing option to change gem properties. + +Example: + +$ cat Gemfile | grep "rails" + +gem "rails" + +$ bundle change rails --group dev --version 5.1 + +$ cat Gemfile | grep "rails" + +gem "rails", "= 5.1", :group => [:dev] + +## OPTIONS +* `--version`: + Specify version requirements for the added gem. + +* `--group`: + Specify the groups for the added gem. Multiple groups should be separated by commas. + +* `--source`: + Specify the source for the added gem. diff --git a/spec/commands/add_spec.rb b/spec/commands/add_spec.rb index 9f11adbcf80..d8a04b8a77c 100644 --- a/spec/commands/add_spec.rb +++ b/spec/commands/add_spec.rb @@ -141,6 +141,14 @@ end end + describe "with --pessimistic option" do + it "adds pessimistic version" do + bundle! "add 'foo' --pessimistic" + expect(bundled_app("Gemfile").read).to include %(gem "foo", "~> 2.0") + expect(the_bundle).to include_gems "foo 2.0" + end + end + describe "with no option" do it "adds pessimistic version" do bundle! "add 'foo'" diff --git a/spec/commands/change_spec.rb b/spec/commands/change_spec.rb new file mode 100644 index 00000000000..a19f5e5f0fb --- /dev/null +++ b/spec/commands/change_spec.rb @@ -0,0 +1,195 @@ +# frozen_string_literal: true + +RSpec.describe "bundle change" do + before :each do + install_gemfile <<-G + source "file://#{gem_repo1}" + + gem "rack", "~> 1.0", :group => :dev + gem "weakling", ">= 0.0.1" + gem "platform_specific", :platforms => [:jruby] + + group :test do + gem "rack-test", "= 1.0" + gem "rspec" + end + G + end + + describe "when gem is not present" do + it "throws error" do + bundle "change rake --group dev1" + + expect(out).to include("`rake` could not be found in the Gemfile.") + gemfile_should_be <<-G + source "file://#{gem_repo1}" + + gem "rack", "~> 1.0", :group => :dev + gem "weakling", ">= 0.0.1" + gem "platform_specific", :platforms => [:jruby] + + group :test do + gem "rack-test", "= 1.0" + gem "rspec" + end + G + end + end + + describe "when an unsupported option is present" do + it "throws error" do + bundle "change platform_specific --group dev1" + + expect(out).to include("`platforms` is not yet supported.") + gemfile_should_be <<-G + source "file://#{gem_repo1}" + + gem "rack", "~> 1.0", :group => :dev + gem "weakling", ">= 0.0.1" + gem "platform_specific", :platforms => [:jruby] + + group :test do + gem "rack-test", "= 1.0" + gem "rspec" + end + G + end + end + + describe "without options" do + it "throws error" do + bundle "change rack" + + expect(out).to include("Please supply at least one option to change.") + end + end + + describe "with --group option" do + context "when group is present as inline" do + it "changes group of the gem" do + bundle! "change rack --group dev1" + + gemfile_should_be <<-G + source "file://#{gem_repo1}" + + gem "weakling", ">= 0.0.1" + gem "platform_specific", :platforms => [:jruby] + + group :test do + gem "rack-test", "= 1.0" + gem "rspec" + end + + gem "rack", "~> 1.0", :group => :dev1 + G + end + end + + context "when gem is present in the group block" do + it "removes gem from the block" do + bundle! "change rack-test --group test1" + + gemfile_should_be <<-G + source "file://#{gem_repo1}" + + gem "rack", "~> 1.0", :group => :dev + gem "weakling", ">= 0.0.1" + gem "platform_specific", :platforms => [:jruby] + + group :test do + gem "rspec" + end + + gem "rack-test", "= 1.0", :group => :test1 + G + end + end + + context "when mutiple groups are specified" do + it "adds mutiple groups" do + bundle! "change rack --group=dev,dev1" + + gemfile_should_be <<-G + source "file://#{gem_repo1}" + + gem "weakling", ">= 0.0.1" + gem "platform_specific", :platforms => [:jruby] + + group :test do + gem "rack-test", "= 1.0" + gem "rspec" + end + + gem "rack", "~> 1.0", :groups => [:dev, :dev1] + G + end + end + + context "when gem is already in one or more groups" do + it "shows warning that gem is present" do + bundle! "change rack --group=dev,dev1" + + expect(out).to include("`rack` is already present in `dev`") + gemfile_should_be <<-G + source "file://#{gem_repo1}" + + gem "weakling", ">= 0.0.1" + gem "platform_specific", :platforms => [:jruby] + + group :test do + gem "rack-test", "= 1.0" + gem "rspec" + end + + gem "rack", "~> 1.0", :groups => [:dev, :dev1] + G + end + end + end + + describe "with --version option" do + context "when specified version exists" do + it "changes version of the gem" do + bundle! "change rack --version 0.9.1" + + expect(bundled_app("Gemfile").read).to include('gem "rack", "~> 0.9.1", :group => :dev') + end + end + + context "when specified version does not exist" do + it "throws error" do + bundle "change rack --version 42.0.0" + + expect(bundled_app("Gemfile").read).to include('gem "rack", "~> 1.0", :group => :dev') + expect(out).to include("Could not find gem 'rack (= 42.0.0)'") + end + end + + context "when other options are updated for gem whose version requirements are not specified" do + it "adds pessimistic version to gem" do + bundle! "change rspec --group test1" + + expect(bundled_app("Gemfile").read).to include('gem "rspec", "~> 1.2", :group => :test1') + end + end + + context "when other options are changed for gem which has optimistic version requirement" do + it "retains the optimistic version prefix" do + bundle! "change weakling --group dev1" + + expect(bundled_app("Gemfile").read).to include('gem "weakling", ">= 0.0.3", :group => :dev1') + end + end + end + + describe "with --source option" do + context "when source uri is correct" do + it "changes source uri of the gem" do + build_repo2 + bundle! "change rack --source=file://#{gem_repo2}" + + expect(bundled_app("Gemfile").read).to include("gem \"rack\", \"~> 1.0\", :group => :dev, :source => \"file://#{gem_repo2}\"") + end + end + end +end