berkshelf 3.0.0.beta7 → 3.0.0.beta8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +4 -1
- data/CONTRIBUTING.md +1 -1
- data/Gemfile +0 -1
- data/Guardfile +0 -8
- data/README.md +33 -13
- data/berkshelf.gemspec +3 -3
- data/features/commands/install.feature +16 -88
- data/features/commands/search.feature +15 -0
- data/features/commands/shelf/show.feature +2 -2
- data/features/commands/shelf/uninstall.feature +1 -1
- data/features/commands/show.feature +3 -3
- data/features/commands/update.feature +29 -1
- data/features/commands/upload.feature +172 -7
- data/features/commands/vendor.feature +32 -0
- data/features/json_formatter.feature +26 -24
- data/features/lifecycle.feature +285 -0
- data/features/lockfile.feature +9 -7
- data/features/step_definitions/chef_server_steps.rb +1 -0
- data/features/step_definitions/cli_steps.rb +2 -2
- data/features/step_definitions/filesystem_steps.rb +2 -4
- data/gem_graph.png +0 -0
- data/generator_files/chefignore +0 -2
- data/lib/berkshelf.rb +39 -14
- data/lib/berkshelf/berksfile.rb +161 -113
- data/lib/berkshelf/cached_cookbook.rb +2 -2
- data/lib/berkshelf/cli.rb +15 -3
- data/lib/berkshelf/commands/shelf.rb +3 -7
- data/lib/berkshelf/community_rest.rb +9 -9
- data/lib/berkshelf/config.rb +3 -3
- data/lib/berkshelf/cookbook_generator.rb +0 -8
- data/lib/berkshelf/cookbook_store.rb +1 -2
- data/lib/berkshelf/dependency.rb +25 -138
- data/lib/berkshelf/downloader.rb +41 -7
- data/lib/berkshelf/errors.rb +113 -214
- data/lib/berkshelf/formatters/base.rb +42 -0
- data/lib/berkshelf/formatters/human.rb +145 -0
- data/lib/berkshelf/formatters/json.rb +149 -133
- data/lib/berkshelf/formatters/null.rb +8 -18
- data/lib/berkshelf/init_generator.rb +1 -1
- data/lib/berkshelf/installer.rb +115 -104
- data/lib/berkshelf/location.rb +22 -121
- data/lib/berkshelf/locations/base.rb +75 -0
- data/lib/berkshelf/locations/git.rb +196 -0
- data/lib/berkshelf/locations/github.rb +8 -0
- data/lib/berkshelf/locations/path.rb +78 -0
- data/lib/berkshelf/lockfile.rb +452 -290
- data/lib/berkshelf/logger.rb +9 -3
- data/lib/berkshelf/mixin/logging.rb +4 -9
- data/lib/berkshelf/resolver.rb +12 -12
- data/lib/berkshelf/source.rb +13 -1
- data/lib/berkshelf/version.rb +1 -1
- data/spec/fixtures/cookbooks/example_cookbook-0.5.0/metadata.rb +3 -7
- data/spec/fixtures/cookbooks/example_cookbook/metadata.rb +3 -6
- data/spec/spec_helper.rb +5 -6
- data/spec/support/matchers/file_system_matchers.rb +4 -0
- data/spec/support/shared_examples/formatter.rb +11 -0
- data/spec/unit/berkshelf/berksfile_spec.rb +25 -28
- data/spec/unit/berkshelf/cli_spec.rb +19 -11
- data/spec/unit/berkshelf/dependency_spec.rb +4 -164
- data/spec/unit/berkshelf/formatters/base_spec.rb +35 -0
- data/spec/unit/berkshelf/formatters/human_spec.rb +7 -0
- data/spec/unit/berkshelf/formatters/json_spec.rb +7 -0
- data/spec/unit/berkshelf/formatters/null_spec.rb +7 -11
- data/spec/unit/berkshelf/location_spec.rb +16 -144
- data/spec/unit/berkshelf/locations/base_spec.rb +80 -0
- data/spec/unit/berkshelf/locations/git_spec.rb +249 -0
- data/spec/unit/berkshelf/locations/path_spec.rb +107 -0
- data/spec/unit/berkshelf/lockfile_parser_spec.rb +3 -3
- data/spec/unit/berkshelf/lockfile_spec.rb +55 -11
- data/spec/unit/berkshelf/logger_spec.rb +2 -2
- data/spec/unit/berkshelf/mixin/logging_spec.rb +5 -9
- data/spec/unit/berkshelf/source_spec.rb +32 -13
- data/spec/unit/berkshelf_spec.rb +6 -9
- metadata +33 -33
- data/.ruby-version +0 -1
- data/berkshelf-complete.sh +0 -75
- data/lib/berkshelf/formatters.rb +0 -110
- data/lib/berkshelf/formatters/human_readable.rb +0 -142
- data/lib/berkshelf/git.rb +0 -204
- data/lib/berkshelf/locations/git_location.rb +0 -135
- data/lib/berkshelf/locations/github_location.rb +0 -55
- data/lib/berkshelf/locations/mercurial_location.rb +0 -114
- data/lib/berkshelf/locations/path_location.rb +0 -88
- data/lib/berkshelf/mercurial.rb +0 -146
- data/lib/berkshelf/mixin.rb +0 -7
- data/spec/support/mercurial.rb +0 -123
- data/spec/unit/berkshelf/formatters_spec.rb +0 -114
- data/spec/unit/berkshelf/git_spec.rb +0 -312
- data/spec/unit/berkshelf/locations/git_location_spec.rb +0 -126
- data/spec/unit/berkshelf/locations/mercurial_location_spec.rb +0 -131
- data/spec/unit/berkshelf/locations/path_location_spec.rb +0 -25
- data/spec/unit/berkshelf/mercurial_spec.rb +0 -172
data/lib/berkshelf/errors.rb
CHANGED
@@ -14,11 +14,7 @@ module Berkshelf
|
|
14
14
|
class DeprecatedError < BerkshelfError; status_code(10); end
|
15
15
|
class InternalError < BerkshelfError; status_code(99); end
|
16
16
|
class ArgumentError < InternalError; end
|
17
|
-
class AbstractFunction < InternalError
|
18
|
-
def to_s
|
19
|
-
'Function must be implemented on includer'
|
20
|
-
end
|
21
|
-
end
|
17
|
+
class AbstractFunction < InternalError; end
|
22
18
|
|
23
19
|
class BerksfileNotFound < BerkshelfError
|
24
20
|
status_code(100)
|
@@ -34,75 +30,45 @@ module Berkshelf
|
|
34
30
|
end
|
35
31
|
end
|
36
32
|
|
37
|
-
class
|
38
|
-
|
39
|
-
class CookbookNotFound < BerkshelfError; status_code(103); end
|
40
|
-
|
41
|
-
class GitError < BerkshelfError
|
42
|
-
status_code(104)
|
33
|
+
class CookbookNotFound < BerkshelfError
|
34
|
+
status_code(103)
|
43
35
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
@
|
48
|
-
end
|
49
|
-
|
50
|
-
# A common header for all git errors. The #to_s method should
|
51
|
-
# use this before outputting any specific errors.
|
52
|
-
#
|
53
|
-
# @return [String]
|
54
|
-
def header
|
55
|
-
'An error occurred during Git execution:'
|
36
|
+
def initialize(name, version, location)
|
37
|
+
@name = name
|
38
|
+
@version = version
|
39
|
+
@location = location
|
56
40
|
end
|
57
41
|
|
58
42
|
def to_s
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
"
|
63
|
-
|
64
|
-
].join("\n")
|
43
|
+
if @version
|
44
|
+
"Cookbook '#{@name}' (#{@version}) not found #{@location}!"
|
45
|
+
else
|
46
|
+
"Cookbook '#{@name}' not found #{@location}!"
|
47
|
+
end
|
65
48
|
end
|
66
49
|
end
|
67
50
|
|
68
|
-
class
|
69
|
-
|
70
|
-
@ref = ref
|
71
|
-
end
|
72
|
-
|
73
|
-
def to_s
|
74
|
-
[
|
75
|
-
header,
|
76
|
-
"",
|
77
|
-
" Ambiguous Git ref: '#{@ref}'",
|
78
|
-
"",
|
79
|
-
].join("\n")
|
80
|
-
end
|
81
|
-
end
|
51
|
+
class DuplicateDependencyDefined < BerkshelfError
|
52
|
+
status_code(105)
|
82
53
|
|
83
|
-
|
84
|
-
|
85
|
-
@ref = ref
|
54
|
+
def initialize(name)
|
55
|
+
@name = name
|
86
56
|
end
|
87
57
|
|
88
58
|
def to_s
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
"",
|
94
|
-
].join("\n")
|
59
|
+
out = "Your Berksfile contains multiple entries named "
|
60
|
+
out << "'#{@name}'. Please remove duplicate dependencies, or put them in "
|
61
|
+
out << "different groups."
|
62
|
+
out
|
95
63
|
end
|
96
64
|
end
|
97
65
|
|
98
|
-
class DuplicateDependencyDefined < BerkshelfError; status_code(105); end
|
99
|
-
|
100
66
|
class NoSolutionError < BerkshelfError
|
101
67
|
status_code(106)
|
102
68
|
|
103
69
|
attr_reader :demands
|
104
70
|
|
105
|
-
# @param [Array<
|
71
|
+
# @param [Array<Dependency>] demands
|
106
72
|
def initialize(demands)
|
107
73
|
@demands = demands
|
108
74
|
end
|
@@ -113,79 +79,6 @@ module Berkshelf
|
|
113
79
|
end
|
114
80
|
|
115
81
|
class CookbookSyntaxError < BerkshelfError; status_code(107); end
|
116
|
-
|
117
|
-
class MercurialError < BerkshelfError
|
118
|
-
status_code(108);
|
119
|
-
end
|
120
|
-
|
121
|
-
class InvalidHgURI < BerkshelfError
|
122
|
-
status_code(110)
|
123
|
-
|
124
|
-
# @param [String] uri
|
125
|
-
def initialize(uri)
|
126
|
-
@uri = uri
|
127
|
-
end
|
128
|
-
|
129
|
-
def to_s
|
130
|
-
"'#{@uri}' is not a valid Mercurial URI"
|
131
|
-
end
|
132
|
-
end
|
133
|
-
|
134
|
-
class InvalidGitURI < BerkshelfError
|
135
|
-
status_code(110)
|
136
|
-
|
137
|
-
# @param [String] uri
|
138
|
-
def initialize(uri)
|
139
|
-
@uri = uri
|
140
|
-
end
|
141
|
-
|
142
|
-
def to_s
|
143
|
-
"'#{@uri}' is not a valid Git URI"
|
144
|
-
end
|
145
|
-
end
|
146
|
-
|
147
|
-
class InvalidGitHubIdentifier < BerkshelfError
|
148
|
-
status_code(110)
|
149
|
-
|
150
|
-
# @param [String] repo_identifier
|
151
|
-
def initialize(repo_identifier)
|
152
|
-
@repo_identifier = repo_identifier
|
153
|
-
end
|
154
|
-
|
155
|
-
def to_s
|
156
|
-
"'#{@repo_identifier}' is not a valid GitHub identifier - should not end in '.git'"
|
157
|
-
end
|
158
|
-
end
|
159
|
-
|
160
|
-
class UnknownGitHubProtocol < BerkshelfError
|
161
|
-
status_code(110)
|
162
|
-
|
163
|
-
# @param [String] protocol
|
164
|
-
def initialize(protocol)
|
165
|
-
@protocol = protocol
|
166
|
-
end
|
167
|
-
|
168
|
-
def to_s
|
169
|
-
"'#{@protocol}' is not supported for the 'github' location key - please use 'git' instead"
|
170
|
-
end
|
171
|
-
end
|
172
|
-
|
173
|
-
class MercurialNotFound < BerkshelfError
|
174
|
-
status_code(111)
|
175
|
-
|
176
|
-
def to_s
|
177
|
-
'Could not find a Mercurial executable in your path - please add it and try again'
|
178
|
-
end
|
179
|
-
end
|
180
|
-
|
181
|
-
class GitNotFound < BerkshelfError
|
182
|
-
status_code(110)
|
183
|
-
|
184
|
-
def to_s
|
185
|
-
'Could not find a Git executable in your path - please add it and try again'
|
186
|
-
end
|
187
|
-
end
|
188
|
-
|
189
82
|
class ConstraintNotSatisfied < BerkshelfError; status_code(111); end
|
190
83
|
class BerksfileReadError < BerkshelfError
|
191
84
|
status_code(113)
|
@@ -197,7 +90,6 @@ module Berkshelf
|
|
197
90
|
@error_backtrace = original_error.backtrace
|
198
91
|
end
|
199
92
|
|
200
|
-
|
201
93
|
def status_code
|
202
94
|
@original_error.respond_to?(:status_code) ? @original_error.status_code : 113
|
203
95
|
end
|
@@ -219,9 +111,9 @@ module Berkshelf
|
|
219
111
|
class MismatchedCookbookName < BerkshelfError
|
220
112
|
status_code(114)
|
221
113
|
|
222
|
-
# @param [
|
114
|
+
# @param [Dependency] dependency
|
223
115
|
# the dependency with the expected name
|
224
|
-
# @param [
|
116
|
+
# @param [CachedCookbook] cached_cookbook
|
225
117
|
# the cached_cookbook with the mismatched name
|
226
118
|
def initialize(dependency, cached_cookbook)
|
227
119
|
@dependency = dependency
|
@@ -229,17 +121,18 @@ module Berkshelf
|
|
229
121
|
end
|
230
122
|
|
231
123
|
def to_s
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
124
|
+
out = "In your Berksfile, you have:\n"
|
125
|
+
out << "\n"
|
126
|
+
out << " cookbook '#{@dependency.name}'\n"
|
127
|
+
out << "\n"
|
128
|
+
out << "But that cookbook is actually named '#{@cached_cookbook.cookbook_name}'\n"
|
129
|
+
out << "\n"
|
130
|
+
out << "This can cause potentially unwanted side-effects in the future.\n"
|
131
|
+
out << "\n"
|
132
|
+
out << "NOTE: If you do not explicitly set the 'name' attribute in the "
|
133
|
+
out << "metadata, the name of the directory will be used instead. This "
|
134
|
+
out << "is often a cause of confusion for dependency solving."
|
135
|
+
out
|
243
136
|
end
|
244
137
|
end
|
245
138
|
|
@@ -251,51 +144,79 @@ module Berkshelf
|
|
251
144
|
end
|
252
145
|
|
253
146
|
def to_s
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
147
|
+
out = "Invalid configuration:\n"
|
148
|
+
@errors.each do |key, errors|
|
149
|
+
errors.each do |error|
|
150
|
+
out << " #{key} #{error}\n"
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
out.strip
|
258
155
|
end
|
259
156
|
end
|
260
157
|
|
261
|
-
class
|
262
|
-
|
263
|
-
|
158
|
+
class InsufficientPrivledges < BerkshelfError
|
159
|
+
status_code(119)
|
160
|
+
|
161
|
+
def initialize(path)
|
162
|
+
@path = path
|
163
|
+
end
|
164
|
+
|
165
|
+
def to_s
|
166
|
+
"You do not have permission to write to '#{@path}'! Please chown the " \
|
167
|
+
"path to the current user, chmod the permissions to include the " \
|
168
|
+
"user, or choose a different path."
|
169
|
+
end
|
170
|
+
end
|
264
171
|
|
265
172
|
class DependencyNotFound < BerkshelfError
|
266
173
|
status_code(120)
|
267
174
|
|
268
|
-
# @param [String, Array<String>]
|
269
|
-
# the list of
|
270
|
-
def initialize(
|
271
|
-
@
|
175
|
+
# @param [String, Array<String>] names
|
176
|
+
# the list of cookbook names that were not defined
|
177
|
+
def initialize(names)
|
178
|
+
@names = Array(names)
|
272
179
|
end
|
273
180
|
|
274
181
|
def to_s
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
"
|
279
|
-
"Berksfile, then run `berks install` to download and install the " \
|
280
|
-
"missing dependencies."
|
182
|
+
if @names.size == 1
|
183
|
+
"Dependency '#{@names.first}' was not found. Please make sure it is " \
|
184
|
+
"in your Berksfile, and then run `berks install` to download and " \
|
185
|
+
"install the missing dependencies."
|
281
186
|
else
|
282
|
-
|
283
|
-
|
284
|
-
|
187
|
+
out = "The following dependencies were not found:\n"
|
188
|
+
@names.each do |name|
|
189
|
+
out << " * #{name}\n"
|
190
|
+
end
|
191
|
+
out << "\n"
|
192
|
+
out << "Please make sure they are in your Berksfile, and then run "
|
193
|
+
out << "`berks install` to download and install the missing "
|
194
|
+
out << "dependencies."
|
195
|
+
out
|
285
196
|
end
|
286
197
|
end
|
287
198
|
end
|
288
199
|
|
289
|
-
class
|
290
|
-
|
291
|
-
|
200
|
+
class CommunitySiteError < BerkshelfError
|
201
|
+
status_code(123)
|
202
|
+
|
203
|
+
def initialize(uri, message)
|
204
|
+
@uri = uri
|
205
|
+
@message = message
|
206
|
+
end
|
207
|
+
|
208
|
+
def to_s
|
209
|
+
"An unexpected error occurred retrieving #{@message} from the cookbook " \
|
210
|
+
"site at '#{@api_uri}'."
|
211
|
+
end
|
212
|
+
end
|
292
213
|
|
293
214
|
class CookbookValidationFailure < BerkshelfError
|
294
215
|
status_code(124)
|
295
216
|
|
296
|
-
# @param [
|
217
|
+
# @param [Location] location
|
297
218
|
# the location (or any subclass) raising this validation error
|
298
|
-
# @param [
|
219
|
+
# @param [CachedCookbook] cached_cookbook
|
299
220
|
# the cached_cookbook that does not satisfy the constraint
|
300
221
|
def initialize(dependency, cached_cookbook)
|
301
222
|
@dependency = dependency
|
@@ -307,10 +228,7 @@ module Berkshelf
|
|
307
228
|
end
|
308
229
|
end
|
309
230
|
|
310
|
-
class ClientKeyFileNotFound < BerkshelfError; status_code(125); end
|
311
|
-
|
312
231
|
class UploadFailure < BerkshelfError; end
|
313
|
-
|
314
232
|
class FrozenCookbook < UploadFailure
|
315
233
|
status_code(126)
|
316
234
|
|
@@ -320,18 +238,18 @@ module Berkshelf
|
|
320
238
|
end
|
321
239
|
|
322
240
|
def to_s
|
323
|
-
"The cookbook #{@cookbook.cookbook_name} (#{@cookbook.version})"
|
324
|
-
|
325
|
-
|
241
|
+
"The cookbook #{@cookbook.cookbook_name} (#{@cookbook.version}) " \
|
242
|
+
"already exists and is frozen on the Chef Server. Use the --force " \
|
243
|
+
"option to override."
|
326
244
|
end
|
327
245
|
end
|
328
246
|
|
329
247
|
class OutdatedDependency < BerkshelfError
|
330
248
|
status_code(128)
|
331
249
|
|
332
|
-
# @param [
|
250
|
+
# @param [Dependency] locked_dependency
|
333
251
|
# the locked dependency
|
334
|
-
# @param [
|
252
|
+
# @param [Dependency] dependency
|
335
253
|
# the dependency that is outdated
|
336
254
|
def initialize(locked, dependency)
|
337
255
|
@locked = locked
|
@@ -384,7 +302,7 @@ module Berkshelf
|
|
384
302
|
# Raised when a cookbook or its recipes contain a space or invalid
|
385
303
|
# character in the path.
|
386
304
|
#
|
387
|
-
# @param [
|
305
|
+
# @param [CachedCookbook] cookbook
|
388
306
|
# the cookbook that failed validation
|
389
307
|
# @param [Array<#to_s>] files
|
390
308
|
# the list of files that were not valid
|
@@ -407,25 +325,6 @@ module Berkshelf
|
|
407
325
|
end
|
408
326
|
end
|
409
327
|
|
410
|
-
# Raised when a CachedCookbook has a license file that isn't allowed
|
411
|
-
# by the Berksfile.
|
412
|
-
#
|
413
|
-
# @param [Berkshelf::CachedCookbook] cookbook
|
414
|
-
# the cookbook that failed license validation
|
415
|
-
class LicenseNotAllowed < BerkshelfError
|
416
|
-
status_code(133)
|
417
|
-
|
418
|
-
def initialize(cookbook)
|
419
|
-
@cookbook = cookbook
|
420
|
-
end
|
421
|
-
|
422
|
-
def to_s
|
423
|
-
msg = "'#{@cookbook.cookbook_name}' has a license of '#{@cookbook.metadata.license}', but"
|
424
|
-
msg << " '#{@cookbook.metadata.license}' is not in your list of allowed licenses"
|
425
|
-
msg
|
426
|
-
end
|
427
|
-
end
|
428
|
-
|
429
328
|
class LicenseNotFound < BerkshelfError
|
430
329
|
status_code(134)
|
431
330
|
|
@@ -437,7 +336,7 @@ module Berkshelf
|
|
437
336
|
|
438
337
|
def to_s
|
439
338
|
"Unknown license: '#{license}'\n" +
|
440
|
-
"Available licenses: #{
|
339
|
+
"Available licenses: #{CookbookGenerator::LICENSES.join(', ')}"
|
441
340
|
end
|
442
341
|
end
|
443
342
|
|
@@ -480,16 +379,15 @@ module Berkshelf
|
|
480
379
|
class InvalidSourceURI < BerkshelfError
|
481
380
|
status_code(137)
|
482
381
|
|
483
|
-
attr_reader :reason
|
484
|
-
|
485
382
|
def initialize(url, reason = nil)
|
486
383
|
@url = url
|
487
384
|
@reason = reason
|
488
385
|
end
|
489
386
|
|
490
387
|
def to_s
|
491
|
-
msg =
|
492
|
-
msg
|
388
|
+
msg = "'#{@url}' is not a valid Berkshelf source URI."
|
389
|
+
msg << " #{@reason}." unless @reason.nil?
|
390
|
+
msg
|
493
391
|
end
|
494
392
|
end
|
495
393
|
|
@@ -513,11 +411,11 @@ module Berkshelf
|
|
513
411
|
end
|
514
412
|
|
515
413
|
def to_s
|
516
|
-
"#{@path} does not appear to be a valid cookbook.
|
414
|
+
"The resource at '#{@path}' does not appear to be a valid cookbook. " \
|
415
|
+
"Does it have a metadata.rb?"
|
517
416
|
end
|
518
417
|
end
|
519
418
|
|
520
|
-
class InvalidLockFile < BerkshelfError; status_code(142); end
|
521
419
|
class PackageError < BerkshelfError; status_code(143); end
|
522
420
|
|
523
421
|
class LockfileOutOfSync < BerkshelfError
|
@@ -532,25 +430,26 @@ module Berkshelf
|
|
532
430
|
status_code(145)
|
533
431
|
|
534
432
|
def initialize(dependency)
|
535
|
-
name = dependency.name
|
536
|
-
version = dependency.locked_version
|
433
|
+
@name = dependency.name
|
434
|
+
@version = dependency.locked_version
|
435
|
+
end
|
537
436
|
|
538
|
-
|
539
|
-
|
540
|
-
|
437
|
+
def to_s
|
438
|
+
"The cookbook '#{@name} (#{@version})' is not installed. Please run " \
|
439
|
+
"`berks install` to download and install the missing dependency."
|
541
440
|
end
|
542
441
|
end
|
543
442
|
|
544
443
|
class NoAPISourcesDefined < BerkshelfError
|
545
444
|
status_code(146)
|
546
445
|
|
547
|
-
def
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
446
|
+
def to_s
|
447
|
+
"Your Berksfile does not define any API sources! You must define " \
|
448
|
+
"at least one source in order to download cookbooks. To add the " \
|
449
|
+
"default Berkshelf API server, add the following code to the top of " \
|
450
|
+
"your Berksfile:" \
|
451
|
+
"\n\n" \
|
452
|
+
" source 'https://api.berkshelf.com'"
|
554
453
|
end
|
555
454
|
end
|
556
455
|
end
|