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