repo_dependency_graph 0.1.5 → 0.2.0
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 +4 -4
- data/bin/repo-dependency-graph +2 -3
- data/lib/repo_dependency_graph/cli.rb +62 -0
- data/lib/repo_dependency_graph/output.rb +155 -0
- data/lib/repo_dependency_graph/version.rb +1 -1
- data/lib/repo_dependency_graph.rb +14 -192
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2f7e2fef071a05082e7a36ae97805147a46337b2
|
4
|
+
data.tar.gz: cc00557bde2101f361a08113394076aaa95f36ae
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 41150cb68c3c85cbe4b07b0ff113549b533a89aed9958ef354fd347f4efd9c14f5526f1106f6d27dfc6afdaada8c0fb7a0aa05db7824318a0210326bd6a632dc
|
7
|
+
data.tar.gz: 8c2577f04f2d56c2930181ff221ddb59bd561c101fcd1068c9f0f2f953818225b204fa42814bc819de5ee40bddfe63a4945e40dc5792aea4fa53e7e6fe3f5183
|
data/bin/repo-dependency-graph
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
require "rubygems"
|
3
2
|
require "optparse"
|
4
3
|
|
5
4
|
$LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
|
6
|
-
require "repo_dependency_graph"
|
5
|
+
require "repo_dependency_graph/cli"
|
7
6
|
|
8
|
-
exit RepoDependencyGraph.run(ARGV)
|
7
|
+
exit RepoDependencyGraph::CLI.run(ARGV)
|
@@ -0,0 +1,62 @@
|
|
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("--select REGEX", "Only include repos with matching names") { |regex| options[:select] = Regexp.new(regex) }
|
43
|
+
opts.on("--reject REGEX", "Exclude repos with matching names") { |regex| options[:reject] = Regexp.new(regex) }
|
44
|
+
opts.on("-h", "--help", "Show this.") { puts opts; exit }
|
45
|
+
opts.on("-v", "--version", "Show Version"){ puts RepoDependencyGraph::VERSION; exit}
|
46
|
+
end.parse!(argv)
|
47
|
+
|
48
|
+
options[:token] ||= begin
|
49
|
+
token = `git config github.token`.strip
|
50
|
+
token if $?.success?
|
51
|
+
end
|
52
|
+
|
53
|
+
options
|
54
|
+
end
|
55
|
+
|
56
|
+
def git_config(thing)
|
57
|
+
result = `git config #{thing}`.strip
|
58
|
+
result.empty? ? nil : result
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,155 @@
|
|
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
|
+
<!doctype html>
|
23
|
+
<html>
|
24
|
+
<head>
|
25
|
+
<title>Network</title>
|
26
|
+
<style>
|
27
|
+
#mynetwork {
|
28
|
+
width: 2000px;
|
29
|
+
height: 2000px;
|
30
|
+
border: 1px solid lightgray;
|
31
|
+
background: #F3F3F3;
|
32
|
+
}
|
33
|
+
</style>
|
34
|
+
|
35
|
+
<script type="text/javascript" src="http://visjs.org/dist/vis.js"></script>
|
36
|
+
<link href="http://visjs.org/dist/vis.css" rel="stylesheet" type="text/css" />
|
37
|
+
|
38
|
+
<script type="text/javascript">
|
39
|
+
var nodes = null;
|
40
|
+
var edges = null;
|
41
|
+
var network = null;
|
42
|
+
|
43
|
+
function draw() {
|
44
|
+
nodes = #{nodes.values.to_json};
|
45
|
+
edges = #{edges.to_json};
|
46
|
+
|
47
|
+
var container = document.getElementById('mynetwork');
|
48
|
+
var data = {
|
49
|
+
nodes: nodes,
|
50
|
+
edges: edges
|
51
|
+
};
|
52
|
+
var options = {stabilize: false};
|
53
|
+
|
54
|
+
new vis.Network(container, data, options);
|
55
|
+
}
|
56
|
+
</script>
|
57
|
+
</head>
|
58
|
+
|
59
|
+
<body onload="draw()">
|
60
|
+
<div id="mynetwork"></div>
|
61
|
+
</body>
|
62
|
+
</html>
|
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| "<table>\n#{t.map { |t| "<tr>#{t.map { |t| "<td>#{t}</td>" }.join("")}</tr>" }.join("\n")}\n</table>" }
|
86
|
+
|
87
|
+
html = <<-HTML.gsub(/^ /, "")
|
88
|
+
<!doctype html>
|
89
|
+
<html>
|
90
|
+
<head>
|
91
|
+
<title>Network</title>
|
92
|
+
<style>
|
93
|
+
table { width: 600px; }
|
94
|
+
</style>
|
95
|
+
</head>
|
96
|
+
<body>
|
97
|
+
#{tables.join("<br>\n<br>\n")}
|
98
|
+
</body>
|
99
|
+
</html>
|
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
|
@@ -4,175 +4,6 @@ require "bundler" # get all dependency for lockfile_parser
|
|
4
4
|
|
5
5
|
module RepoDependencyGraph
|
6
6
|
class << self
|
7
|
-
MAX_HEX = 255
|
8
|
-
|
9
|
-
def run(argv)
|
10
|
-
options = parse_options(argv)
|
11
|
-
draw(dependencies(options), options)
|
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(/^ {10}/, "")
|
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("--chef", "Parse chef metadata.rb files") { options[:chef] = true }
|
42
|
-
opts.on("--select REGEX", "Only include repos with matching names") { |regex| options[:select] = Regexp.new(regex) }
|
43
|
-
opts.on("--reject REGEX", "Exclude repos with matching names") { |regex| options[:reject] = Regexp.new(regex) }
|
44
|
-
opts.on("-h", "--help", "Show this.") { puts opts; exit }
|
45
|
-
opts.on("-v", "--version", "Show Version"){ puts RepoDependencyGraph::VERSION; exit}
|
46
|
-
end.parse!(argv)
|
47
|
-
|
48
|
-
options[:token] ||= begin
|
49
|
-
token = `git config github.token`.strip
|
50
|
-
token if $?.success?
|
51
|
-
end
|
52
|
-
|
53
|
-
options
|
54
|
-
end
|
55
|
-
|
56
|
-
def git_config(thing)
|
57
|
-
result = `git config #{thing}`.strip
|
58
|
-
result.empty? ? nil : result
|
59
|
-
end
|
60
|
-
|
61
|
-
def draw(dependencies, options)
|
62
|
-
case options[:draw]
|
63
|
-
when "html"
|
64
|
-
nodes, edges = convert_to_graphviz(dependencies)
|
65
|
-
html = <<-HTML.gsub(/^ /, "")
|
66
|
-
<!doctype html>
|
67
|
-
<html>
|
68
|
-
<head>
|
69
|
-
<title>Network</title>
|
70
|
-
<style>
|
71
|
-
#mynetwork {
|
72
|
-
width: 2000px;
|
73
|
-
height: 2000px;
|
74
|
-
border: 1px solid lightgray;
|
75
|
-
background: #F3F3F3;
|
76
|
-
}
|
77
|
-
</style>
|
78
|
-
|
79
|
-
<script type="text/javascript" src="http://visjs.org/dist/vis.js"></script>
|
80
|
-
<link href="http://visjs.org/dist/vis.css" rel="stylesheet" type="text/css" />
|
81
|
-
|
82
|
-
<script type="text/javascript">
|
83
|
-
var nodes = null;
|
84
|
-
var edges = null;
|
85
|
-
var network = null;
|
86
|
-
|
87
|
-
function draw() {
|
88
|
-
nodes = #{nodes.values.to_json};
|
89
|
-
edges = #{edges.to_json};
|
90
|
-
|
91
|
-
var container = document.getElementById('mynetwork');
|
92
|
-
var data = {
|
93
|
-
nodes: nodes,
|
94
|
-
edges: edges
|
95
|
-
};
|
96
|
-
var options = {stabilize: false};
|
97
|
-
|
98
|
-
new vis.Network(container, data, options);
|
99
|
-
}
|
100
|
-
</script>
|
101
|
-
</head>
|
102
|
-
|
103
|
-
<body onload="draw()">
|
104
|
-
<div id="mynetwork"></div>
|
105
|
-
</body>
|
106
|
-
</html>
|
107
|
-
HTML
|
108
|
-
File.write("out.html", html)
|
109
|
-
when "table"
|
110
|
-
tables = dependencies.map do |name, uses|
|
111
|
-
used = dependencies.map do |d, uses|
|
112
|
-
used = uses.detect { |d| d.first == name }
|
113
|
-
[d, used.last] if used
|
114
|
-
end.compact
|
115
|
-
size = [used.size, uses.size, 1].max
|
116
|
-
table = []
|
117
|
-
size.times do |i|
|
118
|
-
table[i] = [
|
119
|
-
(used[i] || []).join(": "),
|
120
|
-
(name if i == 0),
|
121
|
-
(uses[i] || []).join(": ")
|
122
|
-
]
|
123
|
-
end
|
124
|
-
table.unshift ["Used", "", "Uses"]
|
125
|
-
table
|
126
|
-
end
|
127
|
-
tables.map!{ |t| "<table>\n#{t.map{|t| "<tr>#{t.map{|t| "<td>#{t}</td>" }.join("")}</tr>" }.join("\n")}\n</table>" }
|
128
|
-
|
129
|
-
html = <<-HTML.gsub(/^ /, "")
|
130
|
-
<!doctype html>
|
131
|
-
<html>
|
132
|
-
<head>
|
133
|
-
<title>Network</title>
|
134
|
-
<style>
|
135
|
-
table { width: 600px; }
|
136
|
-
</style>
|
137
|
-
</head>
|
138
|
-
<body>
|
139
|
-
#{tables.join("<br>\n<br>\n")}
|
140
|
-
</body>
|
141
|
-
</html>
|
142
|
-
HTML
|
143
|
-
File.write("out.html", html)
|
144
|
-
else
|
145
|
-
nodes, edges = convert_to_graphviz(dependencies)
|
146
|
-
require 'graphviz'
|
147
|
-
g = GraphViz.new(:G, :type => :digraph)
|
148
|
-
|
149
|
-
nodes = Hash[nodes.map do |_, data|
|
150
|
-
node = g.add_node(data[:id], :color => data[:color], :style => "filled")
|
151
|
-
[data[:id], node]
|
152
|
-
end]
|
153
|
-
|
154
|
-
edges.each do |edge|
|
155
|
-
g.add_edge(nodes[edge[:from]], nodes[edge[:to]], :label => edge[:label])
|
156
|
-
end
|
157
|
-
|
158
|
-
g.output(:png => "out.png")
|
159
|
-
end
|
160
|
-
end
|
161
|
-
|
162
|
-
def convert_to_graphviz(dependencies)
|
163
|
-
counts = dependency_counts(dependencies)
|
164
|
-
range = counts.values.min..counts.values.max
|
165
|
-
nodes = Hash[counts.each_with_index.map do |(name, count), i|
|
166
|
-
[name, {:id => name, :color => color(count, range)}]
|
167
|
-
end]
|
168
|
-
edges = dependencies.map do |name, dependencies|
|
169
|
-
dependencies.map do |dependency, version|
|
170
|
-
{:from => nodes[name][:id], :to => nodes[dependency][:id], :label => (version || '')}
|
171
|
-
end
|
172
|
-
end.flatten
|
173
|
-
[nodes, edges]
|
174
|
-
end
|
175
|
-
|
176
7
|
def dependencies(options)
|
177
8
|
if options[:map] && options[:external]
|
178
9
|
raise ArgumentError, "Map only makes sense when searching for internal repos"
|
@@ -187,7 +18,7 @@ module RepoDependencyGraph
|
|
187
18
|
possible.map! { |p| p.sub(options[:map][0], options[:map][1].to_s) } if options[:map]
|
188
19
|
|
189
20
|
dependencies = all.map do |repo|
|
190
|
-
found = dependent_repos(repo, options)
|
21
|
+
found = dependent_repos(repo, options)
|
191
22
|
found.select! { |f| possible.include?(f.first) } unless options[:external]
|
192
23
|
next if found.empty?
|
193
24
|
puts "#{repo.name}: #{found.map { |n,v| "#{n}: #{v}" }.join(", ")}"
|
@@ -196,13 +27,19 @@ module RepoDependencyGraph
|
|
196
27
|
Hash[dependencies]
|
197
28
|
end
|
198
29
|
|
30
|
+
private
|
31
|
+
|
199
32
|
def dependent_repos(repo, options)
|
200
|
-
|
33
|
+
repos = []
|
34
|
+
|
35
|
+
if !options[:only] || options[:only] == "chef"
|
201
36
|
if content = repo.content("metadata.rb")
|
202
|
-
scan_chef_metadata(content)
|
37
|
+
repos.concat scan_chef_metadata(content)
|
203
38
|
end
|
204
|
-
|
205
|
-
|
39
|
+
end
|
40
|
+
|
41
|
+
if !options[:only] || options[:only] == "gem"
|
42
|
+
gems = if repo.gem? && spec = load_spec(repo.gemspec_content)
|
206
43
|
spec.runtime_dependencies.map do |d|
|
207
44
|
r = d.requirement.to_s
|
208
45
|
r = nil if r == ">= 0"
|
@@ -213,7 +50,10 @@ module RepoDependencyGraph
|
|
213
50
|
elsif content = repo.content("Gemfile")
|
214
51
|
scan_gemfile(content)
|
215
52
|
end
|
53
|
+
repos.concat gems if gems
|
216
54
|
end
|
55
|
+
|
56
|
+
repos
|
217
57
|
end
|
218
58
|
|
219
59
|
def scan_chef_metadata(content)
|
@@ -239,23 +79,5 @@ module RepoDependencyGraph
|
|
239
79
|
$stderr.puts "Error when parsing content:\n#{content}\n\n#{$!}"
|
240
80
|
nil
|
241
81
|
end
|
242
|
-
|
243
|
-
def color(value, range)
|
244
|
-
value -= range.min # lowest -> green
|
245
|
-
max = range.max - range.min
|
246
|
-
|
247
|
-
i = (value * MAX_HEX / max);
|
248
|
-
i *= 0.6 # green-blue gradient instead of green-green
|
249
|
-
half = MAX_HEX * 0.5
|
250
|
-
values = [0,2,4].map { |v| (Math.sin(0.024 * i + v) * half + half).round.to_s(16).rjust(2, "0") }
|
251
|
-
"##{values.join}"
|
252
|
-
end
|
253
|
-
|
254
|
-
def dependency_counts(dependencies)
|
255
|
-
all = (dependencies.keys + dependencies.values.map { |v| v.map(&:first) }).flatten.uniq
|
256
|
-
Hash[all.map do |k|
|
257
|
-
[k, dependencies.values.map(&:first).count { |name, _| name == k } ]
|
258
|
-
end]
|
259
|
-
end
|
260
82
|
end
|
261
83
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: repo_dependency_graph
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael Grosser
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-09-
|
11
|
+
date: 2014-09-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: organization_audit
|
@@ -33,6 +33,8 @@ extra_rdoc_files: []
|
|
33
33
|
files:
|
34
34
|
- bin/repo-dependency-graph
|
35
35
|
- lib/repo_dependency_graph.rb
|
36
|
+
- lib/repo_dependency_graph/cli.rb
|
37
|
+
- lib/repo_dependency_graph/output.rb
|
36
38
|
- lib/repo_dependency_graph/version.rb
|
37
39
|
homepage: http://github.com/grosser/repo_dependency_graph
|
38
40
|
licenses:
|