bundler-stats 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []