diff --git a/lib/bundler.rb b/lib/bundler.rb index 68d5dc3368a..d695708d2b2 100644 --- a/lib/bundler.rb +++ b/lib/bundler.rb @@ -33,6 +33,7 @@ module Bundler autoload :Fetcher, "bundler/fetcher" autoload :FeatureFlag, "bundler/feature_flag" autoload :GemHelper, "bundler/gem_helper" + autoload :Gemfile, "bundler/gemfile" autoload :GemHelpers, "bundler/gem_helpers" autoload :GemRemoteFetcher, "bundler/gem_remote_fetcher" autoload :GemVersionPromoter, "bundler/gem_version_promoter" diff --git a/lib/bundler/cli.rb b/lib/bundler/cli.rb index f1ff3f45543..f386e685178 100644 --- a/lib/bundler/cli.rb +++ b/lib/bundler/cli.rb @@ -355,6 +355,16 @@ def add(*gems) Add.new(options.dup, gems).run end + desc "canonical [OPTIONS]", "Prettifies the Gemfile" + long_desc <<-D + Prettifies the Gemfile by giving it consistent ordering and formatting. + D + method_option "view", :type => :boolean + def canonical + require "bundler/cli/canonical" + Canonical.new(options).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/canonical.rb b/lib/bundler/cli/canonical.rb new file mode 100644 index 00000000000..e0bb9e904bd --- /dev/null +++ b/lib/bundler/cli/canonical.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Bundler + class CLI::Canonical + def initialize(options) + @options = options + end + + def run + contents = Gemfile.full_gemfile(:show_summary => true, :as_string => true) + + if @options[:view] + puts contents + else + SharedHelpers.write_to_gemfile(Bundler.default_gemfile, contents) + end + end + end +end diff --git a/lib/bundler/gemfile.rb b/lib/bundler/gemfile.rb new file mode 100644 index 00000000000..a977e9a4700 --- /dev/null +++ b/lib/bundler/gemfile.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +module Bundler + class Gemfile + def self.full_gemfile(options = {}) + gemfile = new(options) + gemfile.full_gemfile + end + + def initialize(options = {}) + @options = options + @resolve = nil + end + + def full_gemfile + definition = Bundler.definition + @resolve = definition.resolve if @options[:show_summary] + + gemfile = [] + Array(definition.send(:sources).global_rubygems_source).each do |s| + s.remotes.each {|r| gemfile << "source #{r.to_s.dump}" } + end + + gemfile << nil + + definition.dependencies.group_by(&:groups).each_key(&:sort!).sort_by(&:first).each do |groups, deps| + gemfile << groups_wise(deps, groups) + end + + if @options[:as_string] + gemfile.join("\n").gsub(/\n{3,}/, "\n\n").strip + else + gemfile + end + end + + # @param [Bundler::Dependency] dep Dependency instance of the gem + # @param [Boolean] show_groups Whether groups be shown in gem contents + # @return [[String]] + def gem_contents(dep, show_groups = false) + contents = [] + contents << "gem " << dep.name.dump + + contents << ", " << dep.requirement.as_list.map(&:inspect).join(", ") unless dep.requirement.none? + + if show_groups || @options[:inline_groups] + groups = dep.groups.reject {|g| g.to_s == "default" }.uniq + + unless groups.empty? + contents << if groups.size == 1 + ", :group => :#{groups[0]}" + else + ", :groups => #{groups.inspect}" + end + end + end + + contents << ", :source => \"" << dep.source.remotes << "\"" unless dep.source.nil? + # contents = ["gemspec"] if @dep.source.options["gemspec"] + + contents << ", :platforms => " << dep.platforms.inspect unless dep.platforms.empty? + + env = dep.instance_variable_get(:@env) + contents << ", :env => " << env.inspect if env + + if (req = dep.autorequire) && !req.empty? + req = req.first if req.size == 1 + contents << ", :require => " << req.inspect + end + + contents + end + + def groups_wise(deps, groups) + gemfile = [] + groups = nil if groups.empty? || groups.include?(:default) + + group_block = groups && !@options[:inline_groups] + inside_group = !groups.nil? && !@options[:inline_groups] + gemfile << "group #{groups.map(&:inspect).uniq.join(", ")} do" if group_block + gemfile << deps_contents(deps.sort_by(&:name), inside_group) + gemfile << "end" if group_block + gemfile << nil + + gemfile + end + + private + + # @param [[Bundler::Dependency]] deps Array of dependency instances of gems + # @param [Boolean] inside_group Whether gems to be shown are inside group + # @return [[String]] + def deps_contents(deps, inside_group = false) + contents = [] + deps.each do |dep| + if @options[:show_summary] + spec = @resolve[dep.name].first.__materialize__ + contents << "#{" " if inside_group}# #{spec.summary}" + end + + gem = [] + gem << " " if inside_group + gem << gem_contents(dep) + contents << gem.join + end + + contents + end + end +end diff --git a/man/bundle-canonical.ronn b/man/bundle-canonical.ronn new file mode 100644 index 00000000000..d4bfc780bba --- /dev/null +++ b/man/bundle-canonical.ronn @@ -0,0 +1,17 @@ +bundle-canonical(1) -- Prettifies the Gemfile +=========================================================================== + +## SYNOPSIS + +`bundle canonical` [--view] + +## DESCRIPTION + +Prettifies the Gemfile by giving it consistent ordering and formatting. + +If not, the first missing gem is listed and Bundler exits status 1. + +## OPTIONS + +* `--view`: + Displays what the Gemfile would look like on change but does not change the Gemfile. diff --git a/spec/bundler/gemfile_spec.rb b/spec/bundler/gemfile_spec.rb new file mode 100644 index 00000000000..c78ca641d53 --- /dev/null +++ b/spec/bundler/gemfile_spec.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +RSpec.describe Bundler::Gemfile do + before :each do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "weakling", "~> 0.0.1" + gem "rack-test", :group => :test + gem "rack", :groups => [:prod, :dev] + gem "rspec", :group => :test + G + end + + context "#gem_contents" do + it "with show_groups true" do + definition = Bundler.definition + contents = subject.gem_contents(definition.dependencies.find {|d| d.name == "rack" }, true) + contents2 = subject.gem_contents(definition.dependencies.find {|d| d.name == "rack-test" }, true) + expect(contents.join).to eq("gem \"rack\", :groups => [:prod, :dev]") + expect(contents2.join).to eq("gem \"rack-test\", :group => :test") + end + + it "with show_groups false" do + definition = Bundler.definition + contents = subject.gem_contents(definition.dependencies.find {|d| d.name == "rack" }, false) + expect(contents.join).to eq("gem \"rack\"") + end + end + + context "#groups_wise" do + it "returns group wise gems" do + definition = Bundler.definition + a = definition.dependencies.group_by(&:groups).each_key(&:sort!).sort_by(&:first) + expected = <<-E + group :test do + gem "rack-test" + gem "rspec" + end + E + + deps = a[2][1] + groups = a[2][0] + expect(subject.groups_wise(deps, groups).join("\n").gsub(/\n{3,}/, "\n\n")).to eq(strip_whitespace(expected)) + end + end + + describe "#full_gemfile" do + context "without show_summary" do + subject { Bundler::Gemfile.new(:as_string => true) } + + it "does not show summary" do + expected = <<-E + source "file://#{gem_repo1}" + + gem "weakling", "~> 0.0.1" + + group :dev, :prod do + gem "rack" + end + + group :test do + gem "rack-test" + gem "rspec" + end + E + expect(subject.full_gemfile).to eq(strip_whitespace(expected)) + end + end + + context "with show_summary" do + subject { Bundler::Gemfile.new(:as_string => true, :show_summary => true) } + it "shows summary" do + expected = <<-E + source "file://#{gem_repo1}" + + # This is just a fake gem for testing + gem "weakling", "~> 0.0.1" + + group :dev, :prod do + # This is just a fake gem for testing + gem "rack" + end + + group :test do + # This is just a fake gem for testing + gem "rack-test" + # This is just a fake gem for testing + gem "rspec" + end + E + expect(subject.full_gemfile).to eq(strip_whitespace(expected)) + end + end + + context "with inline_groups" do + subject { Bundler::Gemfile.new(:as_string => true, :inline_groups => true) } + it "shows summary" do + expected = <<-E + source "file://#{gem_repo1}" + + gem "weakling", "~> 0.0.1" + + gem "rack", :groups => [:dev, :prod] + + gem "rack-test", :group => :test + gem "rspec", :group => :test + E + expect(subject.full_gemfile).to eq(strip_whitespace(expected)) + end + end + end +end diff --git a/spec/commands/canonical_spec.rb b/spec/commands/canonical_spec.rb new file mode 100644 index 00000000000..911f13ef223 --- /dev/null +++ b/spec/commands/canonical_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +RSpec.describe "bundle canonical" do + before :each do + install_gemfile <<-G + source "file://#{gem_repo1}" + gem "weakling", "~> 0.0.1" + gem "rack-test", :group => :test + gem "rack", :groups => [:prod, :test] + G + end + + context "with --view option" do + it "does not update gemfile but displays expected gemfile" do + bundle! "canonical --view" + output = <<-G + source "file://#{gem_repo1}" + + # This is just a fake gem for testing + gem "weakling", "~> 0.0.1" + + group :prod, :test do + # This is just a fake gem for testing + gem "rack" + end + + group :test do + # This is just a fake gem for testing + gem "rack-test" + end + G + + expect(out).to eq(strip_whitespace(output)) + gemfile_should_be <<-G + source "file://#{gem_repo1}" + gem "weakling", "~> 0.0.1" + gem "rack-test", :group => :test + gem "rack", :groups => [:prod, :test] + G + end + end + + context "without --view option" do + it "updates gemfile" do + bundle! "canonical" + + gemfile_should_be <<-G + source "file://#{gem_repo1}" + + # This is just a fake gem for testing + gem "weakling", "~> 0.0.1" + + group :prod, :test do + # This is just a fake gem for testing + gem "rack" + end + + group :test do + # This is just a fake gem for testing + gem "rack-test" + end + G + end + end +end