Skip to content
This repository was archived by the owner on Apr 14, 2021. It is now read-only.
1 change: 1 addition & 0 deletions lib/bundler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
10 changes: 10 additions & 0 deletions lib/bundler/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we''l need to come up with a better name for this, but we can do so in the RFC

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was thinking maybe prettify or something on those lines.

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
Expand Down
19 changes: 19 additions & 0 deletions lib/bundler/cli/canonical.rb
Original file line number Diff line number Diff line change
@@ -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
110 changes: 110 additions & 0 deletions lib/bundler/gemfile.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# frozen_string_literal: true

module Bundler
class Gemfile
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this class definitely should be named something different GemfileRewriter or something

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think GemfileRewriterwould suit because we are not changing gemfile anywhere. We are extracting things out of it. Need to think of something else 🤔

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
17 changes: 17 additions & 0 deletions man/bundle-canonical.ronn
Original file line number Diff line number Diff line change
@@ -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.
112 changes: 112 additions & 0 deletions spec/bundler/gemfile_spec.rb
Original file line number Diff line number Diff line change
@@ -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
65 changes: 65 additions & 0 deletions spec/commands/canonical_spec.rb
Original file line number Diff line number Diff line change
@@ -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