├── .gitignore ├── .gitmodules ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── Rakefile ├── Readme.md ├── assets ├── simple.png └── travis-internal.png ├── bin └── repo-dependency-graph ├── lib ├── repo_dependency_graph.rb └── repo_dependency_graph │ ├── cli.rb │ ├── output.rb │ └── version.rb ├── repo_dependency_graph.gemspec └── spec ├── private.example.yml ├── repo_dependency_graph ├── cli_spec.rb └── output_spec.rb ├── repo_dependency_graph_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | spec/private.yml 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "spec/repo-test-user/repo_a"] 2 | path = spec/repo-test-user/repo_a 3 | url = https://github.com/repo-test-user/repo_a.git 4 | [submodule "spec/repo-test-user/repo_b"] 5 | path = spec/repo-test-user/repo_b 6 | url = https://github.com/repo-test-user/repo_b.git 7 | [submodule "spec/repo-test-user/repo_c"] 8 | path = spec/repo-test-user/repo_c 9 | url = https://github.com/repo-test-user/repo_c.git 10 | [submodule "spec/repo-test-user/chef_a"] 11 | path = spec/repo-test-user/chef_a 12 | url = https://github.com/repo-test-user/chef_a.git 13 | [submodule "spec/repo-test-user/chef_b"] 14 | path = spec/repo-test-user/chef_b 15 | url = https://github.com/repo-test-user/chef_b.git 16 | [submodule "spec/repo-test-user/chef_c"] 17 | path = spec/repo-test-user/chef_c 18 | url = https://github.com/repo-test-user/chef_c.git 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | bundler_args: "" 2 | before_script: 3 | - sudo apt-get update 4 | - sudo apt-get install graphviz 5 | rvm: 6 | - 2.3 7 | - 2.4 8 | - 2.5 9 | - 2.6 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gemspec 3 | 4 | gem "bump" 5 | gem "rake" 6 | gem "rspec", "~>2" 7 | gem "ruby-graphviz" 8 | gem "byebug", :platform => :ruby_21 9 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | repo_dependency_graph (0.6.0) 5 | organization_audit 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | bump (0.7.0) 11 | byebug (10.0.2) 12 | diff-lcs (1.3) 13 | json (2.1.0) 14 | organization_audit (2.0.0) 15 | json 16 | rake (12.3.2) 17 | rspec (2.99.0) 18 | rspec-core (~> 2.99.0) 19 | rspec-expectations (~> 2.99.0) 20 | rspec-mocks (~> 2.99.0) 21 | rspec-core (2.99.2) 22 | rspec-expectations (2.99.2) 23 | diff-lcs (>= 1.1.3, < 2.0) 24 | rspec-mocks (2.99.4) 25 | ruby-graphviz (1.2.4) 26 | 27 | PLATFORMS 28 | ruby 29 | 30 | DEPENDENCIES 31 | bump 32 | byebug 33 | rake 34 | repo_dependency_graph! 35 | rspec (~> 2) 36 | ruby-graphviz 37 | 38 | BUNDLED WITH 39 | 1.16.1 40 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "bump/tasks" 3 | 4 | task :default do 5 | sh "rspec spec/" 6 | end 7 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | Graph the dependency of your repositories 2 | 3 | Install 4 | ======= 5 | 6 | ```Bash 7 | gem install repo_dependency_graph 8 | ``` 9 | 10 | Usage 11 | ===== 12 | Install [graphviz](http://www.graphviz.org/Download_macos.php) 13 | 14 | 15 | --token TOKEN Use token 16 | --user USER Use user 17 | --draw TYPE png, html, table (default: png) 18 | --organization ORGANIZATION Use organization 19 | --private Only show private repos 20 | --external Also include external projects in graph (can get super-messy) 21 | --map SEARCH=REPLACE Replace in project name to find them as internal: 'foo=bar' -> replace foo in repo names to bar 22 | --only TYPE Only this type (chef,gem), default: all 23 | --max-pages PAGES 24 | --select REGEX Only include repos with matching names 25 | --reject REGEX Exclude repos with matching names 26 | -h, --help Show this. 27 | -v, --version Show Version 28 | 29 | 30 | ### Public user 31 | 32 | ```Bash 33 | repo-dependency-graph --user repo-test-user 34 | repo_a: repo_b, repo_c 35 | repo_b: repo_d 36 | repo_d: repo_c 37 | repo_c: repo_b 38 | repo_e: repo_a, repo_b, repo_c, repo_d 39 | repo_f: repo_c, repo_d 40 | ``` 41 | 52 | ![Simple](assets/simple.png?raw=true) 53 | ![Travis](assets/travis-internal.png?raw=true) 54 | 55 | ### Private organization 56 | 57 | ```Bash 58 | # create a token that has access to your repositories 59 | curl -v -u your-user-name -X POST https://api.github.com/authorizations --data '{"scopes":["repo"]}' 60 | enter your password -> TOKEN 61 | 62 | git config --global github.token ttttoookkkeeeennn 63 | 64 | OR 65 | 66 | repo-dependency-graph --organization xyz --token ttttoookkkeeeennn 67 | ``` 68 | 69 | Author 70 | ====== 71 | [Michael Grosser](http://grosser.it)
72 | michael@grosser.it
73 | License: MIT
74 | [![Build Status](https://travis-ci.org/grosser/repo_dependency_graph.png)](https://travis-ci.org/grosser/repo_dependency_graph) 75 | -------------------------------------------------------------------------------- /assets/simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grosser/repo_dependency_graph/50e9a81151ce3b4b29bc3a1fa63d7535c219fbf9/assets/simple.png -------------------------------------------------------------------------------- /assets/travis-internal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grosser/repo_dependency_graph/50e9a81151ce3b4b29bc3a1fa63d7535c219fbf9/assets/travis-internal.png -------------------------------------------------------------------------------- /bin/repo-dependency-graph: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "optparse" 3 | 4 | $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib') 5 | require "repo_dependency_graph/cli" 6 | 7 | exit RepoDependencyGraph::CLI.run(ARGV) 8 | -------------------------------------------------------------------------------- /lib/repo_dependency_graph.rb: -------------------------------------------------------------------------------- 1 | require "repo_dependency_graph/version" 2 | require "organization_audit/repo" 3 | 4 | require "bundler" # get all dependency for lockfile_parser 5 | require "bundler/lockfile_parser" 6 | 7 | module RepoDependencyGraph 8 | class << self 9 | def dependencies(options) 10 | if options[:map] && options[:external] 11 | raise ArgumentError, "Map only makes sense when searching for internal repos" 12 | end 13 | 14 | # replace with Hash#slice when we are on 2.5+ only 15 | slice = [:user, :organization, :token, :max_pages].each_with_object({}) do |k, h| 16 | h[k] = options[k] if options.key?(k) 17 | end 18 | 19 | all = OrganizationAudit::Repo.all(slice).sort_by(&:name) 20 | all.select!(&:private?) if options[:private] 21 | all.select! { |r| r.name =~ options[:select] } if options[:select] 22 | all.reject! { |r| r.name =~ options[:reject] } if options[:reject] 23 | 24 | possible = all.map(&:name) 25 | possible.map! { |p| p.sub(options[:map][0], options[:map][1].to_s) } if options[:map] 26 | 27 | dependencies = all.map do |repo| 28 | found = dependent_repos(repo, options) 29 | found.select! { |f| possible.include?(f.first) } unless options[:external] 30 | next if found.empty? 31 | puts "#{repo.name}: #{found.map { |n,v| "#{n}: #{v}" }.join(", ")}" 32 | [repo.name, found] 33 | end.compact 34 | Hash[dependencies] 35 | end 36 | 37 | private 38 | 39 | def dependent_repos(repo, options) 40 | repos = [] 41 | 42 | if !options[:only] || options[:only] == "chef" 43 | if content = repo.content("metadata.rb") 44 | repos.concat scan_chef_metadata(repo.name, content) 45 | end 46 | end 47 | 48 | if !options[:only] || options[:only] == "gem" 49 | gems = 50 | if repo.gem? 51 | scan_gemspec(repo.name, repo.gemspec_content) 52 | elsif content = content_from_any(repo, ["gems.locked", "Gemfile.lock"]) 53 | scan_gemfile_lock(repo.name, content) 54 | elsif content = content_from_any(repo, ["gems.rb", "Gemfile"]) 55 | scan_gemfile(repo.name, content) 56 | end 57 | repos.concat gems if gems 58 | end 59 | 60 | repos 61 | end 62 | 63 | def content_from_any(repo, files) 64 | (file = (repo.file_list & files).first) && repo.content(file) 65 | end 66 | 67 | def scan_chef_metadata(_, content) 68 | content.scan(/^\s*depends ['"](.*?)['"](?:,\s?['"](.*?)['"])?/).map(&:compact) 69 | end 70 | 71 | def scan_gemfile(_, content) 72 | content.scan(/^\s*gem ['"](.*?)['"](?:,\s?['"](.*?)['"]|.*\bref(?::|\s*=>)\s*['"](.*)['"])?/).map(&:compact) 73 | end 74 | 75 | def scan_gemfile_lock(repo_name, content) 76 | content = content.gsub(/BUNDLED WITH\n.*\n/, "") 77 | Bundler::LockfileParser.new(content).specs.map { |d| [d.name, d.version.to_s] } 78 | rescue 79 | $stderr.puts "Error parsing #{repo_name} Gemfile.lock:\n#{content}\n\n#{$!}" 80 | nil 81 | end 82 | 83 | def scan_gemspec(_, content) 84 | content.scan(/add(?:_runtime)?_dependency[\s(]+['"]([^'"]*)['"](?:,\s*['"]([^'"]*)['"])*/).map(&:compact) 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/repo_dependency_graph/cli.rb: -------------------------------------------------------------------------------- 1 | require 'repo_dependency_graph' 2 | require 'repo_dependency_graph/output' 3 | 4 | module RepoDependencyGraph 5 | module CLI 6 | class << self 7 | def run(argv) 8 | options = parse_options(argv) 9 | RepoDependencyGraph::Output.draw( 10 | RepoDependencyGraph.dependencies(options), options 11 | ) 12 | 0 13 | end 14 | 15 | private 16 | 17 | def parse_options(argv) 18 | options = { 19 | :user => git_config("github.user") 20 | } 21 | OptionParser.new do |opts| 22 | opts.banner = <<-BANNER.gsub(/^ /, "") 23 | Draw repo dependency graph from your organization 24 | 25 | Usage: 26 | repo-dependency-graph 27 | 28 | Options: 29 | BANNER 30 | opts.on("--token TOKEN", "Use token") { |token| options[:token] = token } 31 | opts.on("--user USER", "Use user") { |user| options[:user] = user } 32 | opts.on("--draw TYPE", "png, html, table (default: png)") { |draw| options[:draw] = draw } 33 | opts.on("--organization ORGANIZATION", "Use organization") { |organization| options[:organization] = organization } 34 | opts.on("--private", "Only show private repos") { options[:private] = true } 35 | opts.on("--external", "Also include external projects in graph (can get super-messy)") { options[:external] = true } 36 | opts.on("--map SEARCH=REPLACE", "Replace in project name to find them as internal: 'foo=bar' -> replace foo in repo names to bar") do |map| 37 | options[:map] = map.split("=") 38 | options[:map][0] = Regexp.new(options[:map][0]) 39 | options[:map][1] = options[:map][1].to_s 40 | end 41 | opts.on("--only TYPE", String, "Only this type (chef,gem), default: all") { |t| options[:only] = t } 42 | opts.on("--max-pages PAGES", Integer, "") { |p| options[:max_pages] = p } 43 | opts.on("--select REGEX", "Only include repos with matching names") { |regex| options[:select] = Regexp.new(regex) } 44 | opts.on("--reject REGEX", "Exclude repos with matching names") { |regex| options[:reject] = Regexp.new(regex) } 45 | opts.on("-h", "--help", "Show this.") { puts opts; exit } 46 | opts.on("-v", "--version", "Show Version"){ puts RepoDependencyGraph::VERSION; exit} 47 | end.parse!(argv) 48 | 49 | options[:token] ||= begin 50 | token = `git config github.token`.strip 51 | token if $?.success? 52 | end 53 | 54 | options 55 | end 56 | 57 | def git_config(thing) 58 | result = `git config #{thing}`.strip 59 | result.empty? ? nil : result 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/repo_dependency_graph/output.rb: -------------------------------------------------------------------------------- 1 | module RepoDependencyGraph 2 | module Output 3 | MAX_HEX = 255 4 | 5 | class << self 6 | def draw(dependencies, options) 7 | case options[:draw] 8 | when "html" 9 | draw_js(dependencies) 10 | when "table" 11 | draw_table(dependencies) 12 | else 13 | draw_png(dependencies) 14 | end 15 | end 16 | 17 | private 18 | 19 | def draw_js(dependencies) 20 | nodes, edges = convert_to_graphviz(dependencies) 21 | html = <<-HTML.gsub(/^ /, "") 22 | 23 | 24 | 25 | Network 26 | 34 | 35 | 36 | 37 | 38 | 57 | 58 | 59 | 60 |
61 | 62 | 63 | HTML 64 | File.write("out.html", html) 65 | end 66 | 67 | def draw_table(dependencies) 68 | tables = dependencies.map do |name, uses| 69 | used = dependencies.map do |d, uses| 70 | used = uses.detect { |d| d.first == name } 71 | [d, used.last] if used 72 | end.compact 73 | size = [used.size, uses.size, 1].max 74 | table = [] 75 | size.times do |i| 76 | table[i] = [ 77 | (used[i] || []).join(": "), 78 | (name if i == 0), 79 | (uses[i] || []).join(": ") 80 | ] 81 | end 82 | table.unshift ["Used", "", "Uses"] 83 | table 84 | end 85 | tables.map! { |t| "\n#{t.map { |t| "#{t.map { |t| "" }.join("")}" }.join("\n")}\n
#{t}
" } 86 | 87 | html = <<-HTML.gsub(/^ /, "") 88 | 89 | 90 | 91 | Network 92 | 95 | 96 | 97 | #{tables.join("
\n
\n")} 98 | 99 | 100 | HTML 101 | File.write("out.html", html) 102 | end 103 | 104 | def draw_png(dependencies) 105 | nodes, edges = convert_to_graphviz(dependencies) 106 | require 'graphviz' 107 | g = GraphViz.new(:G, :type => :digraph) 108 | 109 | nodes = Hash[nodes.map do |_, data| 110 | node = g.add_node(data[:id], :color => data[:color], :style => "filled") 111 | [data[:id], node] 112 | end] 113 | 114 | edges.each do |edge| 115 | g.add_edge(nodes[edge[:from]], nodes[edge[:to]], :label => edge[:label]) 116 | end 117 | 118 | g.output(:png => "out.png") 119 | end 120 | 121 | def convert_to_graphviz(dependencies) 122 | counts = dependency_counts(dependencies) 123 | range = counts.values.min..counts.values.max 124 | nodes = Hash[counts.each_with_index.map do |(name, count), i| 125 | [name, {:id => name, :color => color(count, range)}] 126 | end] 127 | edges = dependencies.map do |name, dependencies| 128 | dependencies.map do |dependency, version| 129 | {:from => nodes[name][:id], :to => nodes[dependency][:id], :label => (version || '')} 130 | end 131 | end.flatten 132 | [nodes, edges] 133 | end 134 | 135 | 136 | def color(value, range) 137 | value -= range.min # lowest -> green 138 | max = range.max - range.min 139 | 140 | i = (value * MAX_HEX / max); 141 | i *= 0.6 # green-blue gradient instead of green-green 142 | half = MAX_HEX * 0.5 143 | values = [0,2,4].map { |v| (Math.sin(0.024 * i + v) * half + half).round.to_s(16).rjust(2, "0") } 144 | "##{values.join}" 145 | end 146 | 147 | def dependency_counts(dependencies) 148 | all = (dependencies.keys + dependencies.values.map { |v| v.map(&:first) }).flatten.uniq 149 | Hash[all.map do |k| 150 | [k, dependencies.values.map(&:first).count { |name, _| name == k } ] 151 | end] 152 | end 153 | end 154 | end 155 | end 156 | -------------------------------------------------------------------------------- /lib/repo_dependency_graph/version.rb: -------------------------------------------------------------------------------- 1 | module RepoDependencyGraph 2 | VERSION = "0.6.0" 3 | end 4 | -------------------------------------------------------------------------------- /repo_dependency_graph.gemspec: -------------------------------------------------------------------------------- 1 | name = "repo_dependency_graph" 2 | require "./lib/#{name.gsub("-","/")}/version" 3 | 4 | Gem::Specification.new name, RepoDependencyGraph::VERSION do |s| 5 | s.summary = "Graphw the dependency of your repositories" 6 | s.authors = ["Michael Grosser"] 7 | s.email = "michael@grosser.it" 8 | s.homepage = "https://github.com/grosser/#{name}" 9 | s.files = `git ls-files lib/ bin/`.split("\n") 10 | s.license = "MIT" 11 | s.executables = ["repo-dependency-graph"] 12 | s.required_ruby_version = '>= 2.3.0' 13 | s.add_runtime_dependency "organization_audit" 14 | end 15 | -------------------------------------------------------------------------------- /spec/private.example.yml: -------------------------------------------------------------------------------- 1 | token: your-token-see-readme 2 | 3 | user: your-user 4 | expected_user: your-private-repo 5 | 6 | organization: org 7 | expected_organization: org-private-repo 8 | expected_organization_dependencies: 9 | - thing-that-depends-on-org-private-repo 10 | expected_organization_select: something 11 | -------------------------------------------------------------------------------- /spec/repo_dependency_graph/cli_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "repo_dependency_graph/cli" 3 | 4 | describe RepoDependencyGraph::CLI do 5 | def audit(command, options={}) 6 | sh("bin/repo-dependency-graph #{command}", options) 7 | end 8 | 9 | def sh(command, options={}) 10 | result = `#{command} #{"2>&1" unless options[:keep_output]}` 11 | raise "FAILED #{command}\n#{result}" if $?.success? == !!options[:fail] 12 | result 13 | end 14 | 15 | it "shows --version" do 16 | audit("--version").should include(RepoDependencyGraph::VERSION) 17 | end 18 | 19 | it "shows --help" do 20 | audit("--help").should include("Draw repo dependency graph from your organization") 21 | end 22 | 23 | context ".parse_options" do 24 | def call(argv, keep=[]) 25 | result = RepoDependencyGraph::CLI.send(:parse_options, argv) 26 | result.delete(:user) unless keep == :user 27 | result.delete(:token) unless keep == :token 28 | result 29 | end 30 | 31 | it "uses current user by default" do 32 | result = call([], :user) 33 | result.keys.should == [:user] 34 | end 35 | 36 | it "parses --user" do 37 | call(["--user", "foo"], :user).should == {:user => "foo"} 38 | end 39 | 40 | it "parses --organization" do 41 | call(["--organization", "foo"]).should == {:organization => "foo"} 42 | end 43 | 44 | it "parses --token" do 45 | call(["--token", "foo"], :token).should == {:token => "foo"} 46 | end 47 | 48 | it "parses --private" do 49 | call(["--private"]).should == {:private => true} 50 | end 51 | 52 | it "parses --external" do 53 | call(["--external"]).should == {:external => true} 54 | end 55 | 56 | it "parses --only" do 57 | call(["--only", "chef"]).should == {:only => "chef"} 58 | end 59 | 60 | it "parses simple --map" do 61 | call(["--map", "A=B"]).should == {:map => [/A/, "B"]} 62 | end 63 | 64 | it "parses empty --map" do 65 | call(["--map", "A="]).should == {:map => [/A/, ""]} 66 | end 67 | 68 | it "parses regex --map" do 69 | call(["--map", "A.?=B"]).should == {:map => [/A.?/, "B"]} 70 | end 71 | 72 | it "parses --select" do 73 | call(["--select", "A.?B"]).should == {:select => /A.?B/} 74 | end 75 | 76 | it "parses --reject" do 77 | call(["--reject", "A.?B"]).should == {:reject => /A.?B/} 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /spec/repo_dependency_graph/output_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "repo_dependency_graph/output" 3 | 4 | describe RepoDependencyGraph::Output do 5 | context ".draw" do 6 | it "draws" do 7 | Dir.mktmpdir do 8 | Dir.chdir do 9 | RepoDependencyGraph::Output.send(:draw, {"foo" => [["bar"]]}, {}) 10 | File.exist?("out.png").should == true 11 | end 12 | end 13 | end 14 | end 15 | 16 | context ".color" do 17 | it "calculates for 1 to max" do 18 | values = [1,2,25,50,51] 19 | values.map do |k,v| 20 | [k, RepoDependencyGraph::Output.send(:color, k, values.min..values.max)] 21 | end.should == [[1, "#80f31f"], [2, "#89ef19"], [25, "#fd363f"], [50, "#492efa"], [51, "#3f36fd"]] 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/repo_dependency_graph_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe RepoDependencyGraph do 4 | it "has a VERSION" do 5 | RepoDependencyGraph::VERSION.should =~ /^[.\da-z]+$/ 6 | end 7 | 8 | context ".dependencies" do 9 | let(:defaults) {{ only: "gem", user: "repo-test-user", token: config["token"] }} 10 | 11 | def call(options={}) 12 | RepoDependencyGraph.send(:dependencies, defaults.merge(options)) 13 | end 14 | 15 | before do 16 | RepoDependencyGraph.stub(:puts) 17 | end 18 | 19 | it "gathers dependencies for private organizations" do 20 | skip "cannot test without configured user" unless config["user"] 21 | graph = call( 22 | organization: config["organization"], 23 | select: Regexp.new(config["expected_organization_select"]) 24 | ) 25 | expected = graph[config["expected_organization"]] 26 | expected.should == config["expected_organization_dependencies"] 27 | end 28 | 29 | it "gathers dependencies for a user" do 30 | call.should == {"repo_a" => [["repo_b"], ["repo_c"]], "repo_c" => [["repo_b"]]} 31 | end 32 | 33 | it "finds nothing for private when all repos are public" do 34 | call(private: true).should == {} 35 | end 36 | 37 | it "can filter" do 38 | call(select: /_b|a/).should == {"repo_a" => [["repo_b"]]} 39 | end 40 | 41 | it "can reject" do 42 | call(reject: /_c/).should == {"repo_a" => [["repo_b"]]} 43 | end 44 | 45 | it "gathers chef dependencies for a user" do 46 | call(only: "chef").should == {"chef_a" => [["chef_b", "~> 0.1"], ["chef_c", "~> 0.1"]], "chef_c" => [["chef_b", "~> 0.1"]]} 47 | end 48 | 49 | it "can include external dependencies" do 50 | call(external: true).should == {"repo_a" => [["repo_b"], ["repo_c"]], "repo_c" => [["repo_b"], ["activesupport"]]} 51 | end 52 | 53 | it "can map repo names so misnamed repos can be found as internal" do 54 | call(map: [/repo_(c|d)/, "activesupport"]).should == {"repo_a" => [["repo_b"]], "repo_c" => [["repo_b"], ["activesupport"]]} 55 | end 56 | 57 | it "can map repo names to nothing" do 58 | call(map: [/repo_/]).should == {} 59 | end 60 | 61 | it "prevents silly map and external" do 62 | expect { 63 | call(map: [/repo_[cd]/, "activesupport"], external: true) 64 | }.to raise_error(/internal/) 65 | end 66 | end 67 | 68 | context ".scan_gemfile" do 69 | def call(*args) 70 | RepoDependencyGraph.send(:scan_gemfile, 'foo', *args) 71 | end 72 | 73 | it "finds nothing" do 74 | call("").should == [] 75 | end 76 | 77 | it "finds without version" do 78 | call("gem 'foo'").should == [["foo"]] 79 | end 80 | 81 | it "finds with version" do 82 | call("gem 'foo', '1.2.3'").should == [["foo", "1.2.3"]] 83 | end 84 | 85 | it "finds ref with 1.8 syntax" do 86 | call("gem 'foo', ref: 'abcd'").should == [["foo", "abcd"]] 87 | call("gem 'foo' ,:ref=>'abcd'").should == [["foo", "abcd"]] 88 | end 89 | 90 | it "finds ref with 1.9 syntax" do 91 | call("gem 'foo', ref: 'abcd'").should == [["foo", "abcd"]] 92 | call("gem 'foo',ref:'abcd'").should == [["foo", "abcd"]] 93 | end 94 | end 95 | 96 | context ".scan_chef_metadata" do 97 | def call(*args) 98 | RepoDependencyGraph.send(:scan_chef_metadata, 'foo', *args) 99 | end 100 | 101 | it "finds nothing" do 102 | call("").should == [] 103 | end 104 | 105 | it "finds without version" do 106 | call("depends 'foo'").should == [["foo"]] 107 | end 108 | 109 | it "finds with version" do 110 | call("depends 'foo', '1.2.3'").should == [["foo", "1.2.3"]] 111 | end 112 | end 113 | 114 | context ".scan_gemfile_lock" do 115 | def call(*args) 116 | RepoDependencyGraph.send(:scan_gemfile_lock, 'foo', *args) 117 | end 118 | 119 | it "finds without version" do 120 | content = <<~LOCK 121 | GEM 122 | remote: https://rubygems.org/ 123 | specs: 124 | bump (0.5.0) 125 | diff-lcs (1.2.5) 126 | json (1.8.1) 127 | organization_audit (1.0.4) 128 | json 129 | rspec (2.14.1) 130 | rspec-core (~> 2.14.0) 131 | rspec-expectations (~> 2.14.0) 132 | rspec-mocks (~> 2.14.0) 133 | rspec-core (2.14.7) 134 | rspec-expectations (2.14.5) 135 | diff-lcs (>= 1.1.3, < 2.0) 136 | rspec-mocks (2.14.5) 137 | 138 | PLATFORMS 139 | ruby 140 | 141 | DEPENDENCIES 142 | bump 143 | rspec (~> 2) 144 | organization_audit 145 | LOCK 146 | call(content).should == [ 147 | ["bump", "0.5.0"], 148 | ["diff-lcs", "1.2.5"], 149 | ["json", "1.8.1"], 150 | ["organization_audit", "1.0.4"], 151 | ["rspec", "2.14.1"], 152 | ["rspec-core", "2.14.7"], 153 | ["rspec-expectations", "2.14.5"], 154 | ["rspec-mocks", "2.14.5"] 155 | ] 156 | end 157 | 158 | it "finds ref" do 159 | content = <<~LOCK 160 | GIT 161 | remote: git@github.com:foo/bar.git 162 | revision: 891e256a0364079a46259b3fda9c68f816bbe24c 163 | specs: 164 | barz (0.0.4) 165 | json 166 | 167 | GEM 168 | remote: https://rubygems.org/ 169 | specs: 170 | json (1.8.1) 171 | 172 | PLATFORMS 173 | ruby 174 | 175 | DEPENDENCIES 176 | barz! 177 | LOCK 178 | call(content).should == [ 179 | ["barz", "0.0.4"], 180 | ["json", "1.8.1"] 181 | ] 182 | end 183 | 184 | it "ignores new bundler versions" do 185 | content = <<~LOCK 186 | GEM 187 | remote: https://rubygems.org/ 188 | specs: 189 | bump (0.5.0) 190 | 191 | PLATFORMS 192 | ruby 193 | 194 | DEPENDENCIES 195 | bump 196 | 197 | BUNDLED WITH 198 | 2.0.1 199 | LOCK 200 | call(content).should == [ 201 | ["bump", "0.5.0"] 202 | ] 203 | end 204 | 205 | it "returns nil on error" do 206 | Bundler::LockfileParser.should_receive(:new).and_raise 207 | silence_stderr do 208 | call("bad").should == nil 209 | end 210 | end 211 | end 212 | 213 | context ".scan_gemspec" do 214 | def call(*args) 215 | RepoDependencyGraph.send(:scan_gemspec, 'foo', *args) 216 | end 217 | 218 | it "loads simple spec" do 219 | spec = call(<<-RUBY) 220 | Gem::Specification.new "foo" do |s| 221 | s.add_runtime_dependency "xxx", "1.1.1" 222 | end 223 | RUBY 224 | spec.should == [["xxx", "1.1.1"]] 225 | end 226 | 227 | it "loads add_dependency spec" do 228 | spec = call(<<-RUBY) 229 | Gem::Specification.new "foo" do |s| 230 | s.add_dependency "xxx", "1.1.1" 231 | end 232 | RUBY 233 | spec.should == [["xxx", "1.1.1"]] 234 | end 235 | 236 | it "does not load development dependency spec" do 237 | spec = call(<<-RUBY) 238 | Gem::Specification.new "foo" do |s| 239 | s.add_development_dependency "xxx", "1.1.1" 240 | end 241 | RUBY 242 | spec.should == [] 243 | end 244 | 245 | it "loads without version" do 246 | spec = call(<<-RUBY) 247 | Gem::Specification.new "foo" do |s| 248 | s.add_runtime_dependency "xxx" 249 | end 250 | RUBY 251 | spec.should == [["xxx"]] 252 | end 253 | 254 | it "loads with (" do 255 | spec = call(<<-RUBY) 256 | Gem::Specification.new "foo" do |s| 257 | s.add_runtime_dependency('xxx','1.2.3') 258 | end 259 | RUBY 260 | spec.should == [["xxx", "1.2.3"]] 261 | end 262 | 263 | it "loads multiple requirements" do 264 | skip "wrong since scan does not capture multiple repetitions" 265 | spec = call(<<-RUBY) 266 | Gem::Specification.new "foo" do |s| 267 | s.add_runtime_dependency 'xxx', '>= 1.2.3', '< 2' 268 | end 269 | RUBY 270 | spec.should == [["xxx", ">= 1.2.3, < 2"]] 271 | end 272 | end 273 | end 274 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "repo_dependency_graph" 2 | require "yaml" 3 | require "tmpdir" 4 | require "stringio" 5 | 6 | module SpecHelpers 7 | def silence_stderr 8 | old, $stderr = $stderr, StringIO.new 9 | yield 10 | ensure 11 | $stderr = old 12 | end 13 | 14 | def config 15 | config_file = "spec/private.yml" 16 | @config ||= if File.exist?(config_file) 17 | YAML.load_file(config_file) 18 | else 19 | {"token" => "6783dd513f2b28dc814" + "f251e3d503f1f2c2cf1c1"} # tome from user: some-public-token (obfuscated so github does not see it) -> higher rate limits 20 | end 21 | end 22 | end 23 | 24 | RSpec.configure do |c| 25 | c.include SpecHelpers 26 | end 27 | --------------------------------------------------------------------------------