bundler-stats 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 9376d8577f516583cb84e92692cb33f053601c8f
4
+ data.tar.gz: d39378395171a90216a9762eb03a52ac511b5e2d
5
+ SHA512:
6
+ metadata.gz: 4dca024ba74426a410fd651219293e883c7b992dff4c9bee82aecc0c17c17e61dbcfd98ae97803829127cda9ee0d5b89fce2fab40b12712c10fdea65de9fb494
7
+ data.tar.gz: 2bcd652ce3a290611d40ef519f6490e3d2a2c7157ed75281985ddf582a30f3c5cb63ee716b3ace8658d5b018811fffb18f38587c133a59359bdb72fbc5df37cd
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,34 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ ## Uncomment and set this to only include directories you want to watch
5
+ # directories %w(app lib config test spec features) \
6
+ # .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")}
7
+
8
+ ## Note: if you are using the `directories` clause above and you are not
9
+ ## watching the project directory ('.'), then you will want to move
10
+ ## the Guardfile to a watched dir and symlink it back, e.g.
11
+ #
12
+ # $ mkdir config
13
+ # $ mv Guardfile config/
14
+ # $ ln -s config/Guardfile .
15
+ #
16
+ # and, you'll have to watch "config/Guardfile" instead of "Guardfile"
17
+
18
+ guard :bundler do
19
+ require 'guard/bundler'
20
+ require 'guard/bundler/verify'
21
+ helper = Guard::Bundler::Verify.new
22
+
23
+ files = ['Gemfile']
24
+ files += Dir['*.gemspec'] if files.any? { |f| helper.uses_gemspec?(f) }
25
+
26
+ # Assume files are symlinked from somewhere
27
+ files.each { |file| watch(helper.real_path(file)) }
28
+ end
29
+
30
+ guard :rspec, cmd: 'rspec' do
31
+ watch(%r{^spec/.+_spec\.rb$})
32
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
33
+ watch('spec/spec_helper.rb') { "spec" }
34
+ end
data/bin/bundle-stats ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+
4
+ require 'rubygems'
5
+
6
+ lib_dir = File.expand_path(File.join(File.dirname(__FILE__),'..','lib'))
7
+ $LOAD_PATH << lib_dir unless $LOAD_PATH.include?(lib_dir)
8
+
9
+ require 'bundler/stats/cli'
10
+
11
+ Bundler::Stats::CLI.start
@@ -0,0 +1,38 @@
1
+ # encoding: utf-8
2
+
3
+ lib_dir = File.join(File.dirname(__FILE__),'lib')
4
+ $LOAD_PATH << lib_dir unless $LOAD_PATH.include?(lib_dir)
5
+
6
+ require 'bundler/stats/version'
7
+
8
+ Gem::Specification.new do |gem|
9
+
10
+ gem.name = "bundler-stats"
11
+ gem.version = Bundler::Stats::VERSION
12
+
13
+ gem.summary = "Dependency investigation for Bundler"
14
+ gem.description = "Looks through your lockfile and tries to identify problematic use of dependencies"
15
+ gem.licenses = "MIT"
16
+ gem.authors = "Joseph Mastey"
17
+ gem.email = "jmmastey@gmail.com"
18
+ gem.homepage = "http://github.com/jmmastey/bundler-stats"
19
+
20
+ glob = lambda { |patterns| gem.files & Dir[*patterns] }
21
+
22
+ gem.files = `git ls-files`.split($/)
23
+ gem.executables = glob['bin/*'].map { |path| File.basename(path) }
24
+ gem.default_executable = gem.executables.first if Gem::VERSION < '1.7.'
25
+
26
+ gem.extensions = glob['ext/**/extconf.rb']
27
+ gem.test_files = glob['{spec/{**/}*_spec.rb']
28
+ gem.extra_rdoc_files = glob['*.{txt,md}']
29
+
30
+ gem.require_paths = %w[ext lib].select { |dir| File.directory?(dir) }
31
+
32
+ gem.add_dependency "bundler", "~> 1.9"
33
+ gem.add_dependency "thor", "~> 0.19"
34
+
35
+ gem.add_development_dependency "rspec", "~> 3.4"
36
+ gem.add_development_dependency "guard", "~> 2.13"
37
+ gem.add_development_dependency "pry", "~> 0.10"
38
+ end
@@ -0,0 +1,54 @@
1
+ require 'bundler'
2
+
3
+ module Bundler
4
+ module Stats
5
+ class Calculator
6
+ attr_reader :parser, :tree, :gemfile, :remover
7
+
8
+ def initialize(gemfile_path, lockfile_path, options = {})
9
+ raise ArgumentError unless File.readable?(lockfile_path)
10
+ raise ArgumentError unless File.readable?(gemfile_path)
11
+
12
+ @gemfile = Bundler::Dsl.new.eval_gemfile(gemfile_path)
13
+
14
+ lock_contents = File.read(lockfile_path)
15
+ @parser = Bundler::LockfileParser.new(lock_contents)
16
+
17
+ skiplist = options.fetch(:skiplist, [])
18
+ @tree = Bundler::Stats::Tree.new(@parser, skiplist: skiplist)
19
+
20
+ @remover = Bundler::Stats::Remover.new(@tree, @gemfile)
21
+ end
22
+
23
+ def summarize(target)
24
+ @tree.summarize(target).merge({
25
+ potential_removals: @remover.potential_removals(target)
26
+ })
27
+ end
28
+
29
+ def stats
30
+ { summary: summary,
31
+ gems: gem_stats
32
+ }
33
+ end
34
+ alias_method :to_h, :stats
35
+
36
+ def summary
37
+ { total_gems: @gemfile.count,
38
+ unpinned_gems: unpinned_gems.count,
39
+ }
40
+ end
41
+
42
+ def unpinned_gems
43
+ @gemfile.reject { |dep| dep.specific? }
44
+ end
45
+
46
+ def gem_stats
47
+ stats = @gemfile.map do |gem|
48
+ @tree.summarize(gem.name)
49
+ end
50
+ stats.sort_by { |row| row[:total_dependencies] }
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,99 @@
1
+ require 'json'
2
+ require 'thor'
3
+ require 'bundler'
4
+
5
+ require 'bundler/stats'
6
+
7
+ module Bundler
8
+ module Stats
9
+ class CLI < ::Thor
10
+ default_task :stats
11
+ map '--version' => :version, "-v" => :version
12
+
13
+ desc 'stats', 'Displays basic stats about the gems in your Gemfile'
14
+ method_option :format, aliases: "-f", description: "Output format, either JSON or text"
15
+ method_option :nofollow, description: "A comma delimited list of dependencies not to follow."
16
+ def stats
17
+ calculator = build_calculator(options)
18
+ stats = calculator.stats
19
+
20
+ if options[:format] =~ /json/i
21
+ say JSON.pretty_generate(stats)
22
+ else
23
+ draw_stats(stats[:gems], stats[:summary])
24
+ end
25
+ end
26
+
27
+ desc 'show TARGET', 'Prints the dependency tree for a single gem in your Gemfile'
28
+ method_option :format, aliases: "-f", description: "Output format, either JSON or text"
29
+ method_option :nofollow, description: "A comma delimited list of dependencies not to follow."
30
+ def show(target)
31
+ calculator = build_calculator(options)
32
+ stats = calculator.summarize(target)
33
+
34
+ if options[:format] =~ /json/i
35
+ say JSON.pretty_generate(stats)
36
+ else
37
+ draw_show(stats, target)
38
+ end
39
+ end
40
+
41
+ desc 'version', 'Prints the bundler-stats version'
42
+ def version
43
+ say "bundler-stats #{VERSION}"
44
+ end
45
+
46
+ private
47
+
48
+ def draw_stats(gem_stats, summary)
49
+ say "+------------------------------|-----------------|-----------------+"
50
+ say "| Name | Total Deps | 1st Level Deps |"
51
+ say "+------------------------------|-----------------|-----------------+"
52
+ gem_stats.each do |stat_line|
53
+ say "| %-28s | %-15s | %-15s |" % [stat_line[:name], stat_line[:total_dependencies], stat_line[:first_level_dependencies]]
54
+ end
55
+ say "+------------------------------|-----------------|-----------------+"
56
+ say ""
57
+ say "Declared Gems: %s, %s unpinned" % summary.values
58
+ say ""
59
+ end
60
+
61
+ def draw_show(stats, target)
62
+ say "bundle-stats for #{target}"
63
+ say ""
64
+ say "depended upon by (#{stats[:top_level_dependencies].count}) | #{stats[:top_level_dependencies].values.map(&:name).join(', ')}\n"
65
+ say "depends on (#{stats[:transitive_dependencies].count}) | #{stats[:transitive_dependencies].map(&:name).join(', ')}\n"
66
+ say "unique to this (#{stats[:potential_removals].count}) | #{stats[:potential_removals].map(&:name).join(', ')}\n"
67
+ say ""
68
+ end
69
+
70
+ def build_calculator(options)
71
+ if !options[:nofollow].nil?
72
+ skiplist = options[:nofollow].gsub(/\s+/, '').split(",")
73
+ else
74
+ skiplist = []
75
+ end
76
+
77
+ @calculator ||= Bundler::Stats::Calculator.new(gemfile_path, lockfile_path, skiplist: skiplist)
78
+ end
79
+
80
+ def gemfile_path
81
+ cwd = Pathname.new(".")
82
+ until cwd.realdirpath.root? do
83
+ return (cwd + "Gemfile") if File.exist?(cwd + "Gemfile")
84
+ cwd = cwd.parent
85
+ end
86
+ raise ArgumentError, "Couldn't find Gemfile in this directory or parents"
87
+ end
88
+
89
+ def lockfile_path
90
+ cwd = Pathname.new(".")
91
+ until cwd.realdirpath.root? do
92
+ return (cwd + "Gemfile.lock") if File.exist?(cwd + "Gemfile.lock")
93
+ cwd = cwd.parent
94
+ end
95
+ raise ArgumentError, "Couldn't find Gemfile in this directory or parents"
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,41 @@
1
+ module Bundler
2
+ module Stats
3
+ class Remover
4
+ def initialize(tree, top_level)
5
+ @tree = tree
6
+ @top_level = top_level
7
+ end
8
+
9
+ def potential_removals(target)
10
+ candidates = @tree.transitive_dependencies(target)
11
+ candidates.reject do |candidate|
12
+ still_used?(candidate.name, deleted: target)
13
+ end
14
+ end
15
+
16
+ # TODO: woo naive algorithm
17
+ # TODO: circular dependencies would be an issue here
18
+ # TODO: also probably use something like transitive_dependencies
19
+ # to leverage the abilities in tree...
20
+ def still_used?(target, deleted: nil)
21
+ modified_tree = @tree.tree.clone
22
+ modified_tree.delete(deleted)
23
+
24
+ deps_to_check = (@top_level - [Gem::Dependency.new(deleted)])
25
+
26
+ while !deps_to_check.empty? do
27
+ candidate = deps_to_check.pop.name
28
+
29
+ next if candidate == deleted
30
+ next if candidate == "bundler"
31
+ return true if candidate == target
32
+
33
+ deps_to_check += modified_tree[candidate].dependencies
34
+ end
35
+
36
+ false
37
+ end
38
+ end
39
+ end
40
+ end
41
+
@@ -0,0 +1,60 @@
1
+ require 'pry'
2
+
3
+ class Bundler::Stats::Tree
4
+ attr_accessor :tree
5
+
6
+ def initialize(parser, skiplist: [])
7
+ raise ArgumentError unless parser.respond_to?(:specs)
8
+
9
+ @parser = parser
10
+ @tree = specs_as_tree(@parser.specs)
11
+ @skiplist = skiplist
12
+ end
13
+
14
+ def summarize(target)
15
+ transitive_dependencies = transitive_dependencies(target)
16
+ { name: target,
17
+ total_dependencies: transitive_dependencies.count,
18
+ first_level_dependencies: first_level_dependencies(target).count,
19
+ top_level_dependencies: reverse_dependencies(target),
20
+ transitive_dependencies: transitive_dependencies,
21
+ }
22
+ end
23
+
24
+ def first_level_dependencies(target)
25
+ raise ArgumentError, "Unknown gem #{target}" unless @tree.has_key? target
26
+ @tree[target].dependencies
27
+ end
28
+
29
+ def transitive_dependencies(target)
30
+ raise ArgumentError, "Unknown gem #{target}" unless @tree.has_key? target
31
+
32
+ top_level = @tree[target].dependencies
33
+ all_level = top_level + top_level.inject([]) do |arr, dep|
34
+ # turns out bundler refuses to include itself in the dependency tree,
35
+ # which is sneaky
36
+ next arr if dep.name == "bundler"
37
+ next arr if @skiplist.include? dep.name
38
+
39
+ arr += transitive_dependencies(dep.name)
40
+ end
41
+
42
+ all_level.uniq { |d| d.name }
43
+ end
44
+
45
+ # TODO: this is a very stupid way to walk this tree
46
+ def reverse_dependencies(target)
47
+ @tree.select do |name, dep|
48
+ all_deps = transitive_dependencies(name)
49
+ all_deps.any? { |dep| dep.name == target }
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def specs_as_tree(specs)
56
+ specs.each_with_object({}) do |spec, hash|
57
+ hash[spec.name] = spec
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,6 @@
1
+ module Bundler
2
+ module Stats
3
+ # bundler-stats version
4
+ VERSION = '0.0.1'
5
+ end
6
+ end
@@ -0,0 +1,4 @@
1
+ require_relative 'stats/version'
2
+ require_relative 'stats/tree'
3
+ require_relative 'stats/remover'
4
+ require_relative 'stats/calculator'
@@ -0,0 +1,101 @@
1
+ require 'bundler'
2
+ require 'bundler/stats'
3
+
4
+ describe Bundler::Stats::Calculator do
5
+ subject { described_class }
6
+ let(:gemfile_path) { File.join(File.dirname(__FILE__), "../../../test_gemfile") }
7
+ let(:lockfile_path) { File.join(File.dirname(__FILE__), "../../../test_gemfile.lock") }
8
+ let(:calculator) { subject.new(lockfile_path) }
9
+
10
+ context "#new" do
11
+ it "is displeased about unreadable lockfiles" do
12
+ expect { subject.new(gemfile_path, "/not/a/file/path") }.to raise_error(ArgumentError)
13
+ end
14
+
15
+ it "is displeased about unreadable gemfiles, too" do
16
+ expect { subject.new("/not/a/file/path", lockfile_path) }.to raise_error(ArgumentError)
17
+ end
18
+
19
+ it "creates a parser instance" do
20
+ target = subject.new(gemfile_path, lockfile_path)
21
+
22
+ expect(target.parser).to be_a(Bundler::LockfileParser)
23
+ end
24
+
25
+ it "creates a tree instance" do
26
+ target = subject.new(gemfile_path, lockfile_path)
27
+
28
+ expect(target.tree).to be_a(Bundler::Stats::Tree)
29
+ end
30
+
31
+ it "creates a parsed gemfile instance" do
32
+ target = subject.new(gemfile_path, lockfile_path)
33
+
34
+ expect(target.gemfile).to be_a(Array)
35
+ end
36
+
37
+ it "passes on the skiplist argument" do
38
+ allow(Bundler::Stats::Tree).to receive(:new) {}
39
+ # this sucks. break this dependency further
40
+ allow(Bundler::LockfileParser).to receive(:new) { "parser" }
41
+
42
+ target = subject.new(gemfile_path, lockfile_path, skiplist: "abc")
43
+
44
+ expect(Bundler::Stats::Tree).to have_received(:new).with("parser", skiplist: "abc")
45
+ end
46
+ end
47
+
48
+ describe "#stats" do
49
+ it "returns a hash with some keys" do
50
+ calculator = subject.new(gemfile_path, lockfile_path)
51
+
52
+ target = calculator.stats
53
+
54
+ expect(target).to be_a(Hash)
55
+ expect(target.keys).to eq([:summary, :gems])
56
+ end
57
+
58
+ it "asks for gem stats" do
59
+ calculator = subject.new(gemfile_path, lockfile_path)
60
+ allow(calculator).to receive(:gem_stats).and_return(rbis: 35)
61
+
62
+ target = calculator.stats
63
+
64
+ expect(calculator).to have_received(:gem_stats)
65
+ expect(target[:gems]).to eq(rbis: 35)
66
+ end
67
+
68
+ it "asks for a summary" do
69
+ calculator = subject.new(gemfile_path, lockfile_path)
70
+ allow(calculator).to receive(:summary).and_return(rbis: 35)
71
+
72
+ target = calculator.stats
73
+
74
+ expect(calculator).to have_received(:summary)
75
+ expect(target[:summary]).to eq(rbis: 35)
76
+ end
77
+ end
78
+
79
+ context "#gem_stats" do
80
+ it "includes entries for each gem" do
81
+ calculator = subject.new(gemfile_path, lockfile_path)
82
+
83
+ target = calculator.gem_stats
84
+
85
+ expect(target).to be_a(Array)
86
+ expect(target.length).to eq(calculator.gemfile.length)
87
+ end
88
+ end
89
+
90
+ context "#summary" do
91
+ it "is a hash" do
92
+ calculator = subject.new(gemfile_path, lockfile_path)
93
+
94
+ target = calculator.summary
95
+
96
+ expect(target).to be_a(Hash)
97
+ expect(target).to include(:total_gems)
98
+ expect(target).to include(:unpinned_gems)
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,106 @@
1
+ require 'bundler'
2
+ require 'bundler/stats'
3
+
4
+ LazyLazySpec = Struct.new(:name, :dependencies)
5
+ FakeLockfileParser = Struct.new(:specs)
6
+
7
+ describe Bundler::Stats::Remover do
8
+ subject { described_class }
9
+
10
+ def dep(name)
11
+ Gem::Dependency.new(name)
12
+ end
13
+
14
+ let(:full_tree) do
15
+ { "rails" => LazyLazySpec.new("rails", [ dep("actionpack"), dep("rack") ]),
16
+ "sass-rails" => LazyLazySpec.new("sass-rails", [ dep("rails"), dep("rack") ]),
17
+ "actionpack" => LazyLazySpec.new("actionpack", [ dep("actionview") ]),
18
+ "actionview" => LazyLazySpec.new("actionview", []),
19
+ "rack" => LazyLazySpec.new("rack", [dep("rack-test")]),
20
+ "rack-test" => LazyLazySpec.new("rack-test", []),
21
+ }
22
+ end
23
+ let(:tree) { Bundler::Stats::Tree.new(FakeLockfileParser.new(full_tree.values)) }
24
+
25
+ let(:top_level) do
26
+ [ dep("rails"), dep("sass-rails") ]
27
+ end
28
+
29
+ context "#potential_removals" do
30
+ it "returns an empty array if this dependency has no children deps" do
31
+ remover = subject.new(tree, top_level)
32
+
33
+ target = remover.potential_removals("rack-test")
34
+
35
+ expect(target).to eq([])
36
+ end
37
+
38
+ it "doesn't return any child dependencies that are also top level dependencies themselves" do
39
+ remover = subject.new(tree, top_level)
40
+
41
+ target = remover.potential_removals("sass-rails")
42
+
43
+ expect(target).not_to include(dep("rails"))
44
+ end
45
+
46
+ it "returns only the direct dependencies that aren't used by any other dep" do
47
+ remover = subject.new(tree, top_level)
48
+
49
+ target = remover.potential_removals("rails")
50
+
51
+ expect(target).to include(dep("actionpack"))
52
+ expect(target).not_to include(dep("rack"))
53
+ end
54
+
55
+ it "returns only the indirect dependencies that aren't used by any other dep" do
56
+ remover = subject.new(tree, top_level)
57
+
58
+ target = remover.potential_removals("rails")
59
+
60
+ expect(target).to include(dep("actionview"))
61
+ expect(target).not_to include(dep("rack-test"))
62
+ end
63
+ end
64
+
65
+ context "#still_used?" do
66
+ it "returns false if the candidate isn't even in the tree" do
67
+ remover = subject.new(tree, top_level)
68
+
69
+ target = remover.still_used?("action-pants", deleted: "rails")
70
+
71
+ expect(target).to be_falsy
72
+ end
73
+
74
+ it "returns true if the candidate is used directly by another dep" do
75
+ remover = subject.new(tree, top_level)
76
+
77
+ target = remover.still_used?("rack", deleted: "rails")
78
+
79
+ expect(target).to be_truthy
80
+ end
81
+
82
+ it "returns true if the candidate is used indirectly by another dep" do
83
+ remover = subject.new(tree, top_level)
84
+
85
+ target = remover.still_used?("rack-test", deleted: "rails")
86
+
87
+ expect(target).to be_truthy
88
+ end
89
+
90
+ it "returns false if the candidate is only used directly by the deleted dep" do
91
+ remover = subject.new(tree, top_level)
92
+
93
+ target = remover.still_used?("actionpack", deleted: "rails")
94
+
95
+ expect(target).to be_falsy
96
+ end
97
+
98
+ it "returns false if the candidate is only used indirectly by the deleted dep" do
99
+ remover = subject.new(tree, top_level)
100
+
101
+ target = remover.still_used?("actionview", deleted: "rails")
102
+
103
+ expect(target).to be_falsy
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,144 @@
1
+ require 'bundler'
2
+ require 'bundler/stats'
3
+
4
+ describe Bundler::Stats::Tree do
5
+ subject { described_class }
6
+ let(:lock_path) { File.join(File.dirname(__FILE__), "../../../test_gemfile.lock") }
7
+ let(:parser) { Bundler::LockfileParser.new(File.read(lock_path)) }
8
+ let(:tree) { subject.new(lock_path) }
9
+
10
+ context "#new" do
11
+ it "initializes only with a parsed lockfile" do
12
+ expect { subject.new("whatever") }.to raise_error(ArgumentError)
13
+ end
14
+
15
+ it "initializes properly with the lockfile parser" do
16
+ expect { subject.new(parser) }.not_to raise_error
17
+ end
18
+ end
19
+
20
+ context "#first_level_dependencies" do
21
+ it "errors for non-existent dependencies" do
22
+ tree = subject.new(parser)
23
+
24
+ expect { tree.first_level_dependencies("none") }.to raise_error(ArgumentError)
25
+ end
26
+
27
+ it "returns empty array for bottom-level dependencies" do
28
+ tree = subject.new(parser)
29
+
30
+ target = tree.first_level_dependencies("depth-four")
31
+
32
+ expect(target.length).to eq(0)
33
+ end
34
+
35
+ it "returns the set of dependencies for second-level deps" do
36
+ tree = subject.new(parser)
37
+
38
+ target = tree.first_level_dependencies("depth-three")
39
+
40
+ expect(target.length).to eq(1)
41
+ end
42
+
43
+ it "does not recurse any further than the first level" do
44
+ tree = subject.new(parser)
45
+
46
+ target = tree.first_level_dependencies("depth-one")
47
+
48
+ expect(target.length).to eq(2)
49
+ end
50
+ end
51
+
52
+ context "#transitive_dependencies" do
53
+ it "errors for non-existent dependencies" do
54
+ tree = subject.new(parser)
55
+
56
+ expect { tree.transitive_dependencies("none") }.to raise_error(ArgumentError)
57
+ end
58
+
59
+ it "returns empty array for bottom-level dependencies" do
60
+ tree = subject.new(parser)
61
+
62
+ target = tree.transitive_dependencies("depth-four")
63
+
64
+ expect(target.length).to eq(0)
65
+ end
66
+
67
+ it "returns the set of dependencies for second-level deps" do
68
+ tree = subject.new(parser)
69
+
70
+ target = tree.transitive_dependencies("depth-three")
71
+
72
+ expect(target.length).to eq(1)
73
+ end
74
+
75
+ it "returns a recursive list of those dependencies" do
76
+ tree = subject.new(parser)
77
+
78
+ target = tree.transitive_dependencies("depth-one")
79
+
80
+ expect(target.length).to eq(3)
81
+ end
82
+ end
83
+
84
+ context "#summarize" do
85
+ it "is a hash with some keys" do
86
+ tree = subject.new(parser)
87
+
88
+ target = tree.summarize("depth-one")
89
+
90
+ expect(target).to be_a(Hash)
91
+ end
92
+
93
+ it "calls for other methods to do the actual work" do
94
+ tree = subject.new(parser)
95
+ allow(tree).to receive(:first_level_dependencies) { [] }
96
+ allow(tree).to receive(:transitive_dependencies) { [] }
97
+ allow(tree).to receive(:reverse_dependencies) { [] }
98
+
99
+ target = tree.summarize("depth-one")
100
+
101
+ expect(tree).to have_received(:first_level_dependencies)
102
+ expect(tree).to have_received(:transitive_dependencies)
103
+ expect(tree).to have_received(:reverse_dependencies)
104
+ end
105
+ end
106
+
107
+ context "#reverse_dependencies" do
108
+ it "returns nothing for a top-level dependency" do
109
+ tree = subject.new(parser)
110
+
111
+ target = tree.reverse_dependencies("depth-one")
112
+
113
+ expect(target).to be_a(Hash)
114
+ expect(target).to be_empty
115
+ end
116
+
117
+ it "returns the parent dependency for a second-level dep" do
118
+ tree = subject.new(parser)
119
+
120
+ target = tree.reverse_dependencies("depth-two")
121
+
122
+ expect(target).to be_a(Hash)
123
+ expect(target).to include("depth-one")
124
+ end
125
+ end
126
+
127
+ context "skip lists" do
128
+ it "still includes the skipped entry" do
129
+ tree = subject.new(parser, skiplist: ["depth-three"])
130
+
131
+ target = tree.transitive_dependencies("depth-one")
132
+
133
+ expect(target.map(&:name)).to include("depth-three")
134
+ end
135
+
136
+ it "stops processing the children of skipped entries" do
137
+ tree = subject.new(parser, skiplist: ["depth-three"])
138
+
139
+ target = tree.transitive_dependencies("depth-one")
140
+
141
+ expect(target.map(&:name)).not_to include("depth-four")
142
+ end
143
+ end
144
+ end
data/spec/test_gemfile ADDED
@@ -0,0 +1,64 @@
1
+ source 'https://rubygems.org'
2
+ # ruby '2.1.0', engine: 'rbx', engine_version: '2.2.10'
3
+
4
+ gem 'rails', '4.2.0'
5
+ gem 'sass-rails', '~> 4.0.2'
6
+ gem 'uglifier', '>= 1.3.0'
7
+ gem 'coffee-rails', '~> 4.0.0'
8
+ gem 'jquery-rails'
9
+ gem 'turbolinks'
10
+ gem 'jquery-turbolinks'
11
+ gem 'jbuilder'
12
+ gem 'bootstrap-sass'
13
+ gem 'devise'
14
+ gem 'haml-rails'
15
+ gem 'pg'
16
+ gem 'rolify'
17
+ gem 'state_machine'
18
+ gem 'gravatar_image_tag'
19
+ gem 'puma', ">= 2.0"
20
+ gem 'omniauth-github'
21
+ gem 'activerecord-session_store'
22
+ gem 'will_paginate'
23
+ gem 'will_paginate-bootstrap'
24
+ # gem 'rack-mini-profiler'
25
+ gem 'dotenv'
26
+
27
+ gem 'mail', '~> 2.5.4' # workaround for http://stackoverflow.com/questions/25984067/argumenterror-method-sort-given-0-expected-1-when-sending-a-confirmation
28
+
29
+ group :development do
30
+ gem 'foreman'
31
+ gem 'quiet_assets'
32
+ gem 'rubocop'
33
+ gem 'rubocop-rspec'
34
+ end
35
+
36
+ group :development, :test do
37
+ gem 'factory_girl_rails'
38
+ gem 'pry-rails', require: false
39
+ gem 'rspec-rails'
40
+ gem 'poltergeist'
41
+
42
+ # other test deps
43
+ gem 'rake'
44
+ gem 'capybara'
45
+ gem 'cucumber-rails', require: false
46
+ gem 'database_cleaner', '1.0.1'
47
+ gem 'email_spec'
48
+ gem 'spring'
49
+ gem 'spring-commands-rspec' # since we're using rspec
50
+
51
+ # guard stuff
52
+ gem 'guard-bundler'
53
+ gem 'guard-rails'
54
+ gem 'guard-rspec'
55
+ gem 'guard-cucumber'
56
+ gem 'rb-fchange', :require=>false
57
+ gem 'rb-fsevent', :require=>false
58
+ gem 'rb-inotify', :require=>false
59
+ end
60
+
61
+ group :production do
62
+ gem 'rails_12factor'
63
+ gem 'newrelic_rpm'
64
+ end
@@ -0,0 +1,407 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ depth-one (1.1.1)
5
+ depth-two (2.2.2)
6
+ depth-three (3.3.3)
7
+ depth-two (2.2.2)
8
+ depth-three (3.3.3)
9
+ depth-four (4.4.4)
10
+ depth-four (4.4.4)
11
+ actionmailer (4.2.0)
12
+ actionpack (= 4.2.0)
13
+ actionview (= 4.2.0)
14
+ activejob (= 4.2.0)
15
+ mail (~> 2.5, >= 2.5.4)
16
+ rails-dom-testing (~> 1.0, >= 1.0.5)
17
+ actionpack (4.2.0)
18
+ actionview (= 4.2.0)
19
+ activesupport (= 4.2.0)
20
+ rack (~> 1.6.0)
21
+ rack-test (~> 0.6.2)
22
+ rails-dom-testing (~> 1.0, >= 1.0.5)
23
+ rails-html-sanitizer (~> 1.0, >= 1.0.1)
24
+ actionview (4.2.0)
25
+ activesupport (= 4.2.0)
26
+ builder (~> 3.1)
27
+ erubis (~> 2.7.0)
28
+ rails-dom-testing (~> 1.0, >= 1.0.5)
29
+ rails-html-sanitizer (~> 1.0, >= 1.0.1)
30
+ activejob (4.2.0)
31
+ activesupport (= 4.2.0)
32
+ globalid (>= 0.3.0)
33
+ activemodel (4.2.0)
34
+ activesupport (= 4.2.0)
35
+ builder (~> 3.1)
36
+ activerecord (4.2.0)
37
+ activemodel (= 4.2.0)
38
+ activesupport (= 4.2.0)
39
+ arel (~> 6.0)
40
+ activerecord-session_store (0.1.1)
41
+ actionpack (>= 4.0.0, < 5)
42
+ activerecord (>= 4.0.0, < 5)
43
+ railties (>= 4.0.0, < 5)
44
+ activesupport (4.2.0)
45
+ i18n (~> 0.7)
46
+ json (~> 1.7, >= 1.7.7)
47
+ minitest (~> 5.1)
48
+ thread_safe (~> 0.3, >= 0.3.4)
49
+ tzinfo (~> 1.1)
50
+ addressable (2.3.8)
51
+ arel (6.0.3)
52
+ ast (2.1.0)
53
+ astrolabe (1.3.1)
54
+ parser (~> 2.2)
55
+ autoprefixer-rails (5.2.1.3)
56
+ execjs
57
+ json
58
+ bcrypt (3.1.10)
59
+ bootstrap-sass (3.3.5)
60
+ autoprefixer-rails (>= 5.0.0.1)
61
+ sass (>= 3.2.19)
62
+ builder (3.2.2)
63
+ capybara (2.5.0)
64
+ mime-types (>= 1.16)
65
+ nokogiri (>= 1.3.3)
66
+ rack (>= 1.0.0)
67
+ rack-test (>= 0.5.4)
68
+ xpath (~> 2.0)
69
+ cliver (0.3.2)
70
+ coderay (1.1.0)
71
+ coffee-rails (4.0.1)
72
+ coffee-script (>= 2.2.0)
73
+ railties (>= 4.0.0, < 5.0)
74
+ coffee-script (2.4.1)
75
+ coffee-script-source
76
+ execjs
77
+ coffee-script-source (1.9.1.1)
78
+ cucumber (1.3.20)
79
+ builder (>= 2.1.2)
80
+ diff-lcs (>= 1.1.3)
81
+ gherkin (~> 2.12)
82
+ multi_json (>= 1.7.5, < 2.0)
83
+ multi_test (>= 0.1.2)
84
+ cucumber-rails (1.4.2)
85
+ capybara (>= 1.1.2, < 3)
86
+ cucumber (>= 1.3.8, < 2)
87
+ mime-types (>= 1.16, < 3)
88
+ nokogiri (~> 1.5)
89
+ rails (>= 3, < 5)
90
+ database_cleaner (1.0.1)
91
+ devise (3.5.2)
92
+ bcrypt (~> 3.0)
93
+ orm_adapter (~> 0.1)
94
+ railties (>= 3.2.6, < 5)
95
+ responders
96
+ thread_safe (~> 0.1)
97
+ warden (~> 1.2.3)
98
+ diff-lcs (1.2.5)
99
+ dotenv (2.0.2)
100
+ email_spec (1.6.0)
101
+ launchy (~> 2.1)
102
+ mail (~> 2.2)
103
+ erubis (2.7.0)
104
+ execjs (2.6.0)
105
+ factory_girl (4.5.0)
106
+ activesupport (>= 3.0.0)
107
+ factory_girl_rails (4.5.0)
108
+ factory_girl (~> 4.5.0)
109
+ railties (>= 3.0.0)
110
+ faraday (0.9.1)
111
+ multipart-post (>= 1.2, < 3)
112
+ ffi (1.9.10)
113
+ foreman (0.78.0)
114
+ thor (~> 0.19.1)
115
+ formatador (0.2.5)
116
+ gherkin (2.12.2)
117
+ multi_json (~> 1.3)
118
+ globalid (0.3.6)
119
+ activesupport (>= 4.1.0)
120
+ gravatar_image_tag (1.2.0)
121
+ guard (2.13.0)
122
+ formatador (>= 0.2.4)
123
+ listen (>= 2.7, <= 4.0)
124
+ lumberjack (~> 1.0)
125
+ nenv (~> 0.1)
126
+ notiffany (~> 0.0)
127
+ pry (>= 0.9.12)
128
+ shellany (~> 0.0)
129
+ thor (>= 0.18.1)
130
+ guard-bundler (2.1.0)
131
+ bundler (~> 1.0)
132
+ guard (~> 2.2)
133
+ guard-compat (~> 1.1)
134
+ guard-compat (1.2.1)
135
+ guard-cucumber (1.5.4)
136
+ cucumber (>= 1.3.0)
137
+ guard-compat (~> 1.0)
138
+ nenv (~> 0.1)
139
+ guard-rails (0.7.1)
140
+ guard (~> 2.11)
141
+ guard-compat (~> 1.0)
142
+ guard-rspec (4.6.4)
143
+ guard (~> 2.1)
144
+ guard-compat (~> 1.1)
145
+ rspec (>= 2.99.0, < 4.0)
146
+ haml (4.0.7)
147
+ tilt
148
+ haml-rails (0.9.0)
149
+ actionpack (>= 4.0.1)
150
+ activesupport (>= 4.0.1)
151
+ haml (>= 4.0.6, < 5.0)
152
+ html2haml (>= 1.0.1)
153
+ railties (>= 4.0.1)
154
+ hashie (3.4.2)
155
+ hike (1.2.3)
156
+ html2haml (2.0.0)
157
+ erubis (~> 2.7.0)
158
+ haml (~> 4.0.0)
159
+ nokogiri (~> 1.6.0)
160
+ ruby_parser (~> 3.5)
161
+ i18n (0.7.0)
162
+ jbuilder (2.3.1)
163
+ activesupport (>= 3.0.0, < 5)
164
+ multi_json (~> 1.2)
165
+ jquery-rails (4.0.5)
166
+ rails-dom-testing (~> 1.0)
167
+ railties (>= 4.2.0)
168
+ thor (>= 0.14, < 2.0)
169
+ jquery-turbolinks (2.1.0)
170
+ railties (>= 3.1.0)
171
+ turbolinks
172
+ json (1.8.3)
173
+ jwt (1.5.1)
174
+ launchy (2.4.3)
175
+ addressable (~> 2.3)
176
+ listen (3.0.3)
177
+ rb-fsevent (>= 0.9.3)
178
+ rb-inotify (>= 0.9)
179
+ loofah (2.0.3)
180
+ nokogiri (>= 1.5.9)
181
+ lumberjack (1.0.9)
182
+ mail (2.5.4)
183
+ mime-types (~> 1.16)
184
+ treetop (~> 1.4.8)
185
+ method_source (0.8.2)
186
+ mime-types (1.25.1)
187
+ mini_portile (0.6.2)
188
+ minitest (5.8.0)
189
+ multi_json (1.11.2)
190
+ multi_test (0.1.2)
191
+ multi_xml (0.5.5)
192
+ multipart-post (2.0.0)
193
+ nenv (0.2.0)
194
+ newrelic_rpm (3.13.0.299)
195
+ nokogiri (1.6.6.2)
196
+ mini_portile (~> 0.6.0)
197
+ notiffany (0.0.7)
198
+ nenv (~> 0.1)
199
+ shellany (~> 0.0)
200
+ oauth2 (1.0.0)
201
+ faraday (>= 0.8, < 0.10)
202
+ jwt (~> 1.0)
203
+ multi_json (~> 1.3)
204
+ multi_xml (~> 0.5)
205
+ rack (~> 1.2)
206
+ omniauth (1.2.2)
207
+ hashie (>= 1.2, < 4)
208
+ rack (~> 1.0)
209
+ omniauth-github (1.1.2)
210
+ omniauth (~> 1.0)
211
+ omniauth-oauth2 (~> 1.1)
212
+ omniauth-oauth2 (1.3.1)
213
+ oauth2 (~> 1.0)
214
+ omniauth (~> 1.2)
215
+ orm_adapter (0.5.0)
216
+ parser (2.2.2.6)
217
+ ast (>= 1.1, < 3.0)
218
+ pg (0.18.2)
219
+ poltergeist (1.6.0)
220
+ capybara (~> 2.1)
221
+ cliver (~> 0.3.1)
222
+ multi_json (~> 1.0)
223
+ websocket-driver (>= 0.2.0)
224
+ polyglot (0.3.5)
225
+ powerpack (0.1.1)
226
+ pry (0.10.1)
227
+ coderay (~> 1.1.0)
228
+ method_source (~> 0.8.1)
229
+ slop (~> 3.4)
230
+ pry-rails (0.3.4)
231
+ pry (>= 0.9.10)
232
+ puma (2.13.4)
233
+ quiet_assets (1.1.0)
234
+ railties (>= 3.1, < 5.0)
235
+ rack (1.6.4)
236
+ rack-test (0.6.3)
237
+ rack (>= 1.0)
238
+ rails (4.2.0)
239
+ actionmailer (= 4.2.0)
240
+ actionpack (= 4.2.0)
241
+ actionview (= 4.2.0)
242
+ activejob (= 4.2.0)
243
+ activemodel (= 4.2.0)
244
+ activerecord (= 4.2.0)
245
+ activesupport (= 4.2.0)
246
+ bundler (>= 1.3.0, < 2.0)
247
+ railties (= 4.2.0)
248
+ sprockets-rails
249
+ rails-deprecated_sanitizer (1.0.3)
250
+ activesupport (>= 4.2.0.alpha)
251
+ rails-dom-testing (1.0.7)
252
+ activesupport (>= 4.2.0.beta, < 5.0)
253
+ nokogiri (~> 1.6.0)
254
+ rails-deprecated_sanitizer (>= 1.0.1)
255
+ rails-html-sanitizer (1.0.2)
256
+ loofah (~> 2.0)
257
+ rails_12factor (0.0.3)
258
+ rails_serve_static_assets
259
+ rails_stdout_logging
260
+ rails_serve_static_assets (0.0.4)
261
+ rails_stdout_logging (0.0.4)
262
+ railties (4.2.0)
263
+ actionpack (= 4.2.0)
264
+ activesupport (= 4.2.0)
265
+ rake (>= 0.8.7)
266
+ thor (>= 0.18.1, < 2.0)
267
+ rainbow (2.0.0)
268
+ rake (10.4.2)
269
+ rb-fchange (0.0.6)
270
+ ffi
271
+ rb-fsevent (0.9.5)
272
+ rb-inotify (0.9.5)
273
+ ffi (>= 0.5.0)
274
+ responders (2.1.0)
275
+ railties (>= 4.2.0, < 5)
276
+ rolify (4.1.1)
277
+ rspec (3.3.0)
278
+ rspec-core (~> 3.3.0)
279
+ rspec-expectations (~> 3.3.0)
280
+ rspec-mocks (~> 3.3.0)
281
+ rspec-core (3.3.2)
282
+ rspec-support (~> 3.3.0)
283
+ rspec-expectations (3.3.1)
284
+ diff-lcs (>= 1.2.0, < 2.0)
285
+ rspec-support (~> 3.3.0)
286
+ rspec-mocks (3.3.2)
287
+ diff-lcs (>= 1.2.0, < 2.0)
288
+ rspec-support (~> 3.3.0)
289
+ rspec-rails (3.3.3)
290
+ actionpack (>= 3.0, < 4.3)
291
+ activesupport (>= 3.0, < 4.3)
292
+ railties (>= 3.0, < 4.3)
293
+ rspec-core (~> 3.3.0)
294
+ rspec-expectations (~> 3.3.0)
295
+ rspec-mocks (~> 3.3.0)
296
+ rspec-support (~> 3.3.0)
297
+ rspec-support (3.3.0)
298
+ rubocop (0.33.0)
299
+ astrolabe (~> 1.3)
300
+ parser (>= 2.2.2.5, < 3.0)
301
+ powerpack (~> 0.1)
302
+ rainbow (>= 1.99.1, < 3.0)
303
+ ruby-progressbar (~> 1.4)
304
+ rubocop-rspec (1.3.0)
305
+ ruby-progressbar (1.7.5)
306
+ ruby_parser (3.7.1)
307
+ sexp_processor (~> 4.1)
308
+ sass (3.2.19)
309
+ sass-rails (4.0.5)
310
+ railties (>= 4.0.0, < 5.0)
311
+ sass (~> 3.2.2)
312
+ sprockets (~> 2.8, < 3.0)
313
+ sprockets-rails (~> 2.0)
314
+ sexp_processor (4.6.0)
315
+ shellany (0.0.1)
316
+ slop (3.6.0)
317
+ spring (1.3.6)
318
+ spring-commands-rspec (1.0.4)
319
+ spring (>= 0.9.1)
320
+ sprockets (2.12.4)
321
+ hike (~> 1.2)
322
+ multi_json (~> 1.0)
323
+ rack (~> 1.0)
324
+ tilt (~> 1.1, != 1.3.0)
325
+ sprockets-rails (2.3.2)
326
+ actionpack (>= 3.0)
327
+ activesupport (>= 3.0)
328
+ sprockets (>= 2.8, < 4.0)
329
+ state_machine (1.2.0)
330
+ thor (0.19.1)
331
+ thread_safe (0.3.5)
332
+ tilt (1.4.1)
333
+ treetop (1.4.15)
334
+ polyglot
335
+ polyglot (>= 0.3.1)
336
+ turbolinks (2.5.3)
337
+ coffee-rails
338
+ tzinfo (1.2.2)
339
+ thread_safe (~> 0.1)
340
+ uglifier (2.7.2)
341
+ execjs (>= 0.3.0)
342
+ json (>= 1.8.0)
343
+ warden (1.2.3)
344
+ rack (>= 1.0)
345
+ websocket-driver (0.6.2)
346
+ websocket-extensions (>= 0.1.0)
347
+ websocket-extensions (0.1.2)
348
+ will_paginate (3.0.7)
349
+ will_paginate-bootstrap (1.0.1)
350
+ will_paginate (>= 3.0.3)
351
+ xpath (2.0.0)
352
+ nokogiri (~> 1.3)
353
+
354
+ PLATFORMS
355
+ ruby
356
+
357
+ DEPENDENCIES
358
+ activerecord-session_store
359
+ bootstrap-sass
360
+ capybara
361
+ coffee-rails (~> 4.0.0)
362
+ cucumber-rails
363
+ database_cleaner (= 1.0.1)
364
+ devise
365
+ dotenv
366
+ email_spec
367
+ factory_girl_rails
368
+ foreman
369
+ gravatar_image_tag
370
+ guard-bundler
371
+ guard-cucumber
372
+ guard-rails
373
+ guard-rspec
374
+ haml-rails
375
+ jbuilder
376
+ jquery-rails
377
+ jquery-turbolinks
378
+ mail (~> 2.5.4)
379
+ newrelic_rpm
380
+ omniauth-github
381
+ pg
382
+ poltergeist
383
+ pry-rails
384
+ puma (>= 2.0)
385
+ quiet_assets
386
+ rails (= 4.2.0)
387
+ rails_12factor
388
+ rake
389
+ rb-fchange
390
+ rb-fsevent
391
+ rb-inotify
392
+ rolify
393
+ rspec-rails
394
+ rubocop
395
+ rubocop-rspec
396
+ sass-rails (~> 4.0.2)
397
+ spring
398
+ spring-commands-rspec
399
+ state_machine
400
+ turbolinks
401
+ uglifier (>= 1.3.0)
402
+ will_paginate
403
+ will_paginate-bootstrap
404
+ depth-one
405
+ depth-two
406
+ depth-three
407
+ depth-four
metadata ADDED
@@ -0,0 +1,131 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bundler-stats
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Joseph Mastey
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-04-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.9'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.9'
27
+ - !ruby/object:Gem::Dependency
28
+ name: thor
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '0.19'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '0.19'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '3.4'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '3.4'
55
+ - !ruby/object:Gem::Dependency
56
+ name: guard
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '2.13'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: '2.13'
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: '0.10'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ~>
81
+ - !ruby/object:Gem::Version
82
+ version: '0.10'
83
+ description: Looks through your lockfile and tries to identify problematic use of
84
+ dependencies
85
+ email: jmmastey@gmail.com
86
+ executables:
87
+ - bundle-stats
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - .gitignore
92
+ - Gemfile
93
+ - Guardfile
94
+ - bin/bundle-stats
95
+ - bundler-stats.gemspec
96
+ - lib/bundler/stats.rb
97
+ - lib/bundler/stats/calculator.rb
98
+ - lib/bundler/stats/cli.rb
99
+ - lib/bundler/stats/remover.rb
100
+ - lib/bundler/stats/tree.rb
101
+ - lib/bundler/stats/version.rb
102
+ - spec/lib/bundler/stats/calculator_spec.rb
103
+ - spec/lib/bundler/stats/remover_spec.rb
104
+ - spec/lib/bundler/stats/tree_spec.rb
105
+ - spec/test_gemfile
106
+ - spec/test_gemfile.lock
107
+ homepage: http://github.com/jmmastey/bundler-stats
108
+ licenses:
109
+ - MIT
110
+ metadata: {}
111
+ post_install_message:
112
+ rdoc_options: []
113
+ require_paths:
114
+ - lib
115
+ required_ruby_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - '>='
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - '>='
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ requirements: []
126
+ rubyforge_project:
127
+ rubygems_version: 2.0.14
128
+ signing_key:
129
+ specification_version: 4
130
+ summary: Dependency investigation for Bundler
131
+ test_files: []