librarian-puppet 1.0.3 → 1.0.4
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/README.md +1 -1
- data/lib/librarian/puppet/source/forge.rb +17 -2
- data/lib/librarian/puppet/source/forge/repo.rb +32 -79
- data/lib/librarian/puppet/source/forge/repo_v1.rb +102 -0
- data/lib/librarian/puppet/source/forge/repo_v3.rb +54 -0
- data/lib/librarian/puppet/source/githubtarball/repo.rb +20 -3
- data/lib/librarian/puppet/templates/Puppetfile +10 -4
- data/lib/librarian/puppet/version.rb +1 -1
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3904f177c8c656c97186e7ffebf578db9fde4569
|
4
|
+
data.tar.gz: 14b95b8c41e5848b109c0b9c2759ba8017a20986
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fdbac9e14ea288dac3d92f2dbfcc5fa019bacdbcf63f54b237359ee41cc8f291b8dbc740e748cfa8d236384a85eee5c7b4f0e277a771a01703480897756070da
|
7
|
+
data.tar.gz: d3e6ff6c0f5211516e21cb6c656120638a720822ddb2acded53d456831cead0feed152ccd7b22e903781d5345aae285071d707173181d500904fcc96a568ed22
|
data/README.md
CHANGED
@@ -38,7 +38,7 @@ for which modules your puppet infrastructure repository depends goes in here.
|
|
38
38
|
|
39
39
|
This Puppetfile will download all the dependencies listed in your Modulefile from the Puppet Forge
|
40
40
|
|
41
|
-
forge "https://
|
41
|
+
forge "https://forgeapi.puppetlabs.com"
|
42
42
|
|
43
43
|
modulefile
|
44
44
|
|
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'uri'
|
2
2
|
require 'librarian/puppet/util'
|
3
|
-
require 'librarian/puppet/source/forge/
|
3
|
+
require 'librarian/puppet/source/forge/repo_v1'
|
4
|
+
# require 'librarian/puppet/source/forge/repo_v3'
|
4
5
|
|
5
6
|
module Librarian
|
6
7
|
module Puppet
|
@@ -51,6 +52,12 @@ module Librarian
|
|
51
52
|
|
52
53
|
def initialize(environment, uri, options = {})
|
53
54
|
self.environment = environment
|
55
|
+
|
56
|
+
# if uri =~ %r{^http(s)?://forge\.puppetlabs\.com}
|
57
|
+
# uri = "https://forgeapi.puppetlabs.com"
|
58
|
+
# warn { "Replacing Puppet Forge API URL to use v3 #{uri}. You should update your Puppetfile" }
|
59
|
+
# end
|
60
|
+
|
54
61
|
@uri = URI::parse(uri)
|
55
62
|
@cache_path = nil
|
56
63
|
end
|
@@ -141,7 +148,15 @@ module Librarian
|
|
141
148
|
|
142
149
|
def repo(name)
|
143
150
|
@repo ||= {}
|
144
|
-
@repo[name]
|
151
|
+
unless @repo[name]
|
152
|
+
# if we are using the official Forge then use API v3, otherwise stick to v1 for now
|
153
|
+
# if uri.hostname =~ /\.puppetlabs\.com$/
|
154
|
+
# @repo[name] = RepoV3.new(self, name)
|
155
|
+
# else
|
156
|
+
@repo[name] = RepoV1.new(self, name)
|
157
|
+
# end
|
158
|
+
end
|
159
|
+
@repo[name]
|
145
160
|
end
|
146
161
|
end
|
147
162
|
end
|
@@ -10,19 +10,9 @@ module Librarian
|
|
10
10
|
class Repo < Librarian::Puppet::Source::Repo
|
11
11
|
include Librarian::Puppet::Util
|
12
12
|
|
13
|
-
def initialize(source, name)
|
14
|
-
super(source, name)
|
15
|
-
# API returned data for this module including all versions and dependencies, indexed by module name
|
16
|
-
# from http://forge.puppetlabs.com/api/v1/releases.json?module=#{name}
|
17
|
-
@api_data = nil
|
18
|
-
# API returned data for this module and a specific version, indexed by version
|
19
|
-
# from http://forge.puppetlabs.com/api/v1/releases.json?module=#{name}&version=#{version}
|
20
|
-
@api_version_data = {}
|
21
|
-
end
|
22
|
-
|
23
13
|
def versions
|
24
14
|
return @versions if @versions
|
25
|
-
@versions =
|
15
|
+
@versions = get_versions
|
26
16
|
if @versions.empty?
|
27
17
|
info { "No versions found for module #{name}" }
|
28
18
|
else
|
@@ -31,8 +21,21 @@ module Librarian
|
|
31
21
|
@versions
|
32
22
|
end
|
33
23
|
|
24
|
+
# fetch list of versions ordered for newer to older
|
25
|
+
def get_versions
|
26
|
+
# implement in subclasses
|
27
|
+
end
|
28
|
+
|
29
|
+
# return map with dependencies in the form {module_name => version,...}
|
30
|
+
# version: Librarian::Manifest::Version
|
34
31
|
def dependencies(version)
|
35
|
-
|
32
|
+
# implement in subclasses
|
33
|
+
end
|
34
|
+
|
35
|
+
# return the url for a specific version tarball
|
36
|
+
# version: Librarian::Manifest::Version
|
37
|
+
def url(name, version)
|
38
|
+
# implement in subclasses
|
36
39
|
end
|
37
40
|
|
38
41
|
def manifests
|
@@ -83,10 +86,25 @@ module Librarian
|
|
83
86
|
|
84
87
|
target = vendored?(name, version) ? vendored_path(name, version).to_s : name
|
85
88
|
|
86
|
-
#
|
89
|
+
# can't pass the default v3 forge url (http://forgeapi.puppetlabs.com)
|
90
|
+
# to clients that use the v1 API (https://forge.puppetlabs.com)
|
91
|
+
# nor the other way around
|
87
92
|
module_repository = source.to_s
|
93
|
+
|
88
94
|
if Forge.client_api_version() > 1 and module_repository =~ %r{^http(s)?://forge\.puppetlabs\.com}
|
89
95
|
module_repository = "https://forgeapi.puppetlabs.com"
|
96
|
+
warn { "Replacing Puppet Forge API URL to use v3 #{module_repository} as required by your client version #{Librarian::Puppet.puppet_version}" }
|
97
|
+
end
|
98
|
+
|
99
|
+
m = module_repository.match(%r{^http(s)?://forgeapi\.puppetlabs\.com})
|
100
|
+
if Forge.client_api_version() == 1 and m
|
101
|
+
ssl = m[1]
|
102
|
+
# Puppet 2.7 can't handle the 302 returned by the https url, so stick to http
|
103
|
+
if ssl and Librarian::Puppet::puppet_gem_version < Gem::Version.create('3.0.0')
|
104
|
+
warn { "Using plain http as your version of Puppet #{Librarian::Puppet::puppet_gem_version} can't download from forge.puppetlabs.com using https" }
|
105
|
+
ssl = nil
|
106
|
+
end
|
107
|
+
module_repository = "http#{ssl}://forge.puppetlabs.com"
|
90
108
|
end
|
91
109
|
|
92
110
|
command = %W{puppet module install --version #{version} --target-dir}
|
@@ -122,8 +140,7 @@ module Librarian
|
|
122
140
|
end
|
123
141
|
|
124
142
|
def vendor_cache(name, version)
|
125
|
-
|
126
|
-
url = "#{source}#{info[name].first['file']}"
|
143
|
+
url = url(name, version)
|
127
144
|
path = vendored_path(name, version).to_s
|
128
145
|
debug { "Downloading #{url} into #{path}"}
|
129
146
|
environment.vendor!
|
@@ -134,70 +151,6 @@ module Librarian
|
|
134
151
|
end
|
135
152
|
end
|
136
153
|
|
137
|
-
private
|
138
|
-
|
139
|
-
# Issue #223 dependencies may be duplicated
|
140
|
-
def clear_duplicated_dependencies(data)
|
141
|
-
return nil if data.nil?
|
142
|
-
data.each do |m,versions|
|
143
|
-
versions.each do |v|
|
144
|
-
if v["dependencies"] and !v["dependencies"].empty?
|
145
|
-
dependency_names = v["dependencies"].map {|d| d[0]}
|
146
|
-
duplicated = dependency_names.select{ |e| dependency_names.count(e) > 1 }
|
147
|
-
unless duplicated.empty?
|
148
|
-
duplicated.uniq.each do |module_duplicated|
|
149
|
-
to_remove = []
|
150
|
-
v["dependencies"].each_index{|i| to_remove << i if module_duplicated == v["dependencies"][i][0]}
|
151
|
-
warn { "Module #{m}@#{v["version"]} contains duplicated dependencies for #{module_duplicated}, ignoring all but the first of #{to_remove.map {|i| v["dependencies"][i]}}" }
|
152
|
-
to_remove.slice(1..-1).reverse.each {|i| v["dependencies"].delete_at(i) }
|
153
|
-
v["dependencies"] = v["dependencies"] - to_remove.slice(1..-1)
|
154
|
-
end
|
155
|
-
end
|
156
|
-
end
|
157
|
-
end
|
158
|
-
end
|
159
|
-
data
|
160
|
-
end
|
161
|
-
|
162
|
-
# get and cache the API data for a specific module with all its versions and dependencies
|
163
|
-
def api_data(module_name)
|
164
|
-
return @api_data[module_name] if @api_data
|
165
|
-
# call API and cache data
|
166
|
-
@api_data = clear_duplicated_dependencies(api_call(module_name))
|
167
|
-
if @api_data.nil?
|
168
|
-
raise Error, "Unable to find module '#{name}' on #{source}"
|
169
|
-
end
|
170
|
-
@api_data[module_name]
|
171
|
-
end
|
172
|
-
|
173
|
-
# get and cache the API data for a specific module and version
|
174
|
-
def api_version_data(module_name, version)
|
175
|
-
# if we already got all the versions, find in cached data
|
176
|
-
return @api_data[module_name].detect{|x| x['version'] == version.to_s} if @api_data
|
177
|
-
# otherwise call the api for this version if not cached already
|
178
|
-
@api_version_data[version] = clear_duplicated_dependencies(api_call(name, version)) if @api_version_data[version].nil?
|
179
|
-
@api_version_data[version]
|
180
|
-
end
|
181
|
-
|
182
|
-
def api_call(module_name, version=nil)
|
183
|
-
url = source.uri.clone
|
184
|
-
url.path += "#{'/' if url.path.empty? or url.path[-1] != '/'}api/v1/releases.json"
|
185
|
-
url.query = "module=#{module_name}"
|
186
|
-
url.query += "&version=#{version}" unless version.nil?
|
187
|
-
debug { "Querying Forge API for module #{name}#{" and version #{version}" unless version.nil?}: #{url}" }
|
188
|
-
|
189
|
-
begin
|
190
|
-
data = open(url) {|f| f.read}
|
191
|
-
JSON.parse(data)
|
192
|
-
rescue OpenURI::HTTPError => e
|
193
|
-
case e.io.status[0].to_i
|
194
|
-
when 404,410
|
195
|
-
nil
|
196
|
-
else
|
197
|
-
raise e, "Error requesting #{url}: #{e.to_s}"
|
198
|
-
end
|
199
|
-
end
|
200
|
-
end
|
201
154
|
end
|
202
155
|
end
|
203
156
|
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'open-uri'
|
3
|
+
require 'librarian/puppet/source/forge/repo'
|
4
|
+
|
5
|
+
module Librarian
|
6
|
+
module Puppet
|
7
|
+
module Source
|
8
|
+
class Forge
|
9
|
+
class RepoV1 < Librarian::Puppet::Source::Forge::Repo
|
10
|
+
|
11
|
+
def initialize(source, name)
|
12
|
+
super(source, name)
|
13
|
+
# API returned data for this module including all versions and dependencies, indexed by module name
|
14
|
+
# from http://forge.puppetlabs.com/api/v1/releases.json?module=#{name}
|
15
|
+
@api_data = nil
|
16
|
+
# API returned data for this module and a specific version, indexed by version
|
17
|
+
# from http://forge.puppetlabs.com/api/v1/releases.json?module=#{name}&version=#{version}
|
18
|
+
@api_version_data = {}
|
19
|
+
end
|
20
|
+
|
21
|
+
def get_versions
|
22
|
+
api_data(name).map { |r| r['version'] }.reverse
|
23
|
+
end
|
24
|
+
|
25
|
+
def dependencies(version)
|
26
|
+
api_version_data(name, version)['dependencies']
|
27
|
+
end
|
28
|
+
|
29
|
+
def url(name, version)
|
30
|
+
info = api_version_data(name, version)
|
31
|
+
"#{source}#{info[name].first['file']}"
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# Issue #223 dependencies may be duplicated
|
37
|
+
def clear_duplicated_dependencies(data)
|
38
|
+
return nil if data.nil?
|
39
|
+
data.each do |m,versions|
|
40
|
+
versions.each do |v|
|
41
|
+
if v["dependencies"] and !v["dependencies"].empty?
|
42
|
+
dependency_names = v["dependencies"].map {|d| d[0]}
|
43
|
+
duplicated = dependency_names.select{ |e| dependency_names.count(e) > 1 }
|
44
|
+
unless duplicated.empty?
|
45
|
+
duplicated.uniq.each do |module_duplicated|
|
46
|
+
to_remove = []
|
47
|
+
v["dependencies"].each_index{|i| to_remove << i if module_duplicated == v["dependencies"][i][0]}
|
48
|
+
warn { "Module #{m}@#{v["version"]} contains duplicated dependencies for #{module_duplicated}, ignoring all but the first of #{to_remove.map {|i| v["dependencies"][i]}}" }
|
49
|
+
to_remove.slice(1..-1).reverse.each {|i| v["dependencies"].delete_at(i) }
|
50
|
+
v["dependencies"] = v["dependencies"] - to_remove.slice(1..-1)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
data
|
57
|
+
end
|
58
|
+
|
59
|
+
# get and cache the API data for a specific module with all its versions and dependencies
|
60
|
+
def api_data(module_name)
|
61
|
+
return @api_data[module_name] if @api_data
|
62
|
+
# call API and cache data
|
63
|
+
@api_data = clear_duplicated_dependencies(api_call(module_name))
|
64
|
+
if @api_data.nil?
|
65
|
+
raise Error, "Unable to find module '#{name}' on #{source}"
|
66
|
+
end
|
67
|
+
@api_data[module_name]
|
68
|
+
end
|
69
|
+
|
70
|
+
# get and cache the API data for a specific module and version
|
71
|
+
def api_version_data(module_name, version)
|
72
|
+
# if we already got all the versions, find in cached data
|
73
|
+
return @api_data[module_name].detect{|x| x['version'] == version.to_s} if @api_data
|
74
|
+
# otherwise call the api for this version if not cached already
|
75
|
+
@api_version_data[version] = clear_duplicated_dependencies(api_call(name, version)) if @api_version_data[version].nil?
|
76
|
+
@api_version_data[version]
|
77
|
+
end
|
78
|
+
|
79
|
+
def api_call(module_name, version=nil)
|
80
|
+
url = source.uri.clone
|
81
|
+
url.path += "#{'/' if url.path.empty? or url.path[-1] != '/'}api/v1/releases.json"
|
82
|
+
url.query = "module=#{module_name}"
|
83
|
+
url.query += "&version=#{version}" unless version.nil?
|
84
|
+
debug { "Querying Forge API for module #{name}#{" and version #{version}" unless version.nil?}: #{url}" }
|
85
|
+
|
86
|
+
begin
|
87
|
+
data = open(url) {|f| f.read}
|
88
|
+
JSON.parse(data)
|
89
|
+
rescue OpenURI::HTTPError => e
|
90
|
+
case e.io.status[0].to_i
|
91
|
+
when 404,410
|
92
|
+
nil
|
93
|
+
else
|
94
|
+
raise e, "Error requesting #{url}: #{e.to_s}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'librarian/puppet/source/forge/repo'
|
2
|
+
require 'puppet_forge'
|
3
|
+
require 'librarian/puppet/version'
|
4
|
+
|
5
|
+
module Librarian
|
6
|
+
module Puppet
|
7
|
+
module Source
|
8
|
+
class Forge
|
9
|
+
class RepoV3 < Librarian::Puppet::Source::Forge::Repo
|
10
|
+
|
11
|
+
PuppetForge.user_agent = "librarian-puppet/#{Librarian::Puppet::VERSION}"
|
12
|
+
|
13
|
+
def get_versions
|
14
|
+
get_module.releases.map{|r| r.version}
|
15
|
+
end
|
16
|
+
|
17
|
+
def dependencies(version)
|
18
|
+
array = get_release(version).metadata[:dependencies].map{|d| [d['name'], d['version_requirement']]}
|
19
|
+
Hash[*array.flatten(1)]
|
20
|
+
end
|
21
|
+
|
22
|
+
def url(name, version)
|
23
|
+
if name == "#{get_module().owner.username}/#{get_module().name}"
|
24
|
+
release = get_release(version)
|
25
|
+
else
|
26
|
+
# should never get here as we use one repo object for each module (to be changed in the future)
|
27
|
+
debug { "Looking up url for #{name}@#{version}" }
|
28
|
+
release = PuppetForge::Release.find("#{name.sub('/','-')}-#{version}")
|
29
|
+
end
|
30
|
+
"#{source}#{release.file_uri}"
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def get_module
|
36
|
+
@module ||= PuppetForge::Module.find(name.sub('/','-'))
|
37
|
+
raise(Error, "Unable to find module '#{name}' on #{source}") unless @module
|
38
|
+
@module
|
39
|
+
end
|
40
|
+
|
41
|
+
def get_release(version)
|
42
|
+
release = get_module.releases.find{|r| r.version == version.to_s}
|
43
|
+
if release.nil?
|
44
|
+
versions = get_module.releases.map{|r| r.version}
|
45
|
+
raise Error, "Unable to find version '#{version}' for module '#{name}' on #{source} amongst #{versions}"
|
46
|
+
end
|
47
|
+
release
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -25,7 +25,7 @@ module Librarian
|
|
25
25
|
all_versions = data.map { |r| r['name'].gsub(/^v/, '') }.sort.reverse
|
26
26
|
|
27
27
|
all_versions.delete_if do |version|
|
28
|
-
version !~ /\A\d
|
28
|
+
version !~ /\A\d+\.\d+(\.\d+.*)?\z/
|
29
29
|
end
|
30
30
|
|
31
31
|
@versions = all_versions.compact
|
@@ -71,7 +71,7 @@ module Librarian
|
|
71
71
|
clean_up_old_cached_versions(name)
|
72
72
|
|
73
73
|
url = "https://api.github.com/repos/#{name}/tarball/#{version}"
|
74
|
-
url
|
74
|
+
add_api_token_to_url(url)
|
75
75
|
|
76
76
|
environment.vendor!
|
77
77
|
File.open(vendored_path(name, version).to_s, 'wb') do |f|
|
@@ -95,6 +95,23 @@ module Librarian
|
|
95
95
|
end
|
96
96
|
end
|
97
97
|
|
98
|
+
def token_key_value
|
99
|
+
ENV[TOKEN_KEY]
|
100
|
+
end
|
101
|
+
|
102
|
+
def token_key_nil?
|
103
|
+
token_key_value.nil? || token_key_value.empty?
|
104
|
+
end
|
105
|
+
|
106
|
+
def add_api_token_to_url url
|
107
|
+
if token_key_nil?
|
108
|
+
debug { "#{TOKEN_KEY} environment value is empty or missing" }
|
109
|
+
else
|
110
|
+
url << "?access_token=#{ENV[TOKEN_KEY]}"
|
111
|
+
end
|
112
|
+
url
|
113
|
+
end
|
114
|
+
|
98
115
|
private
|
99
116
|
|
100
117
|
def api_call(path)
|
@@ -102,7 +119,7 @@ module Librarian
|
|
102
119
|
url = "https://api.github.com#{path}?page=1&per_page=100"
|
103
120
|
while true do
|
104
121
|
debug { " Module #{name} getting tags at: #{url}" }
|
105
|
-
url
|
122
|
+
add_api_token_to_url(url)
|
106
123
|
response = http_get(url, :headers => {
|
107
124
|
"User-Agent" => "librarian-puppet v#{Librarian::Puppet::VERSION}"
|
108
125
|
})
|
@@ -1,16 +1,22 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
#^syntax detection
|
3
3
|
|
4
|
-
forge "
|
4
|
+
forge "https://forgeapi.puppetlabs.com"
|
5
5
|
|
6
6
|
# use dependencies defined in Modulefile
|
7
7
|
modulefile
|
8
8
|
|
9
|
+
# A module from the Puppet Forge
|
9
10
|
# mod 'puppetlabs/stdlib'
|
10
11
|
|
11
|
-
#
|
12
|
+
# A module from git
|
13
|
+
# mod 'puppetlabs/ntp',
|
12
14
|
# :git => 'git://github.com/puppetlabs/puppetlabs-ntp.git'
|
13
15
|
|
14
|
-
#
|
16
|
+
# A module from a git branch/tag
|
17
|
+
# mod 'puppetlabs/apt',
|
15
18
|
# :git => 'https://github.com/puppetlabs/puppetlabs-apt.git',
|
16
|
-
# :ref => '
|
19
|
+
# :ref => '1.4.x'
|
20
|
+
|
21
|
+
# A module from Github pre-packaged tarball
|
22
|
+
# mod 'puppetlabs/apache', '0.6.0', :github_tarball => 'puppetlabs/puppetlabs-apache'
|
metadata
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: librarian-puppet
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tim Sharpe
|
8
|
+
- Carlos Sanchez
|
8
9
|
autorequire:
|
9
10
|
bindir: bin
|
10
11
|
cert_chain: []
|
11
|
-
date: 2014-06-
|
12
|
+
date: 2014-06-25 00:00:00.000000000 Z
|
12
13
|
dependencies:
|
13
14
|
- !ruby/object:Gem::Dependency
|
14
15
|
name: librarian
|
@@ -142,6 +143,7 @@ description: |-
|
|
142
143
|
a single command.
|
143
144
|
email:
|
144
145
|
- tim@sharpe.id.au
|
146
|
+
- carlos@apache.org
|
145
147
|
executables:
|
146
148
|
- librarian-puppet
|
147
149
|
extensions: []
|
@@ -161,6 +163,8 @@ files:
|
|
161
163
|
- lib/librarian/puppet/source.rb
|
162
164
|
- lib/librarian/puppet/source/forge.rb
|
163
165
|
- lib/librarian/puppet/source/forge/repo.rb
|
166
|
+
- lib/librarian/puppet/source/forge/repo_v1.rb
|
167
|
+
- lib/librarian/puppet/source/forge/repo_v3.rb
|
164
168
|
- lib/librarian/puppet/source/git.rb
|
165
169
|
- lib/librarian/puppet/source/githubtarball.rb
|
166
170
|
- lib/librarian/puppet/source/githubtarball/repo.rb
|