├── .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 | 
53 | 
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 | [](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 |
#{t} | " }.join("")}