berkshelf 3.0.0.beta6 → 3.0.0.beta7

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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/features/berksfile.feature +2 -0
  3. data/features/commands/apply.feature +1 -1
  4. data/features/commands/contingent.feature +5 -3
  5. data/features/commands/install.feature +40 -40
  6. data/features/commands/list.feature +42 -20
  7. data/features/commands/outdated.feature +60 -16
  8. data/features/commands/show.feature +51 -8
  9. data/features/commands/update.feature +43 -15
  10. data/features/commands/upload.feature +4 -1
  11. data/features/commands/vendor.feature +27 -0
  12. data/features/json_formatter.feature +20 -8
  13. data/features/lockfile.feature +192 -71
  14. data/generator_files/CHANGELOG.md.erb +5 -0
  15. data/lib/berkshelf/berksfile.rb +166 -139
  16. data/lib/berkshelf/cli.rb +33 -30
  17. data/lib/berkshelf/cookbook_generator.rb +1 -0
  18. data/lib/berkshelf/dependency.rb +64 -14
  19. data/lib/berkshelf/downloader.rb +7 -10
  20. data/lib/berkshelf/errors.rb +59 -11
  21. data/lib/berkshelf/formatters/human_readable.rb +23 -36
  22. data/lib/berkshelf/formatters/json.rb +25 -29
  23. data/lib/berkshelf/installer.rb +111 -122
  24. data/lib/berkshelf/locations/git_location.rb +22 -9
  25. data/lib/berkshelf/locations/mercurial_location.rb +20 -5
  26. data/lib/berkshelf/locations/path_location.rb +22 -7
  27. data/lib/berkshelf/lockfile.rb +435 -203
  28. data/lib/berkshelf/resolver.rb +4 -2
  29. data/lib/berkshelf/source.rb +10 -1
  30. data/lib/berkshelf/version.rb +1 -1
  31. data/spec/fixtures/cookbooks/example_cookbook/Berksfile.lock +3 -4
  32. data/spec/fixtures/lockfiles/2.0.lock +17 -0
  33. data/spec/fixtures/lockfiles/blank.lock +0 -0
  34. data/spec/fixtures/lockfiles/default.lock +18 -10
  35. data/spec/fixtures/lockfiles/empty.lock +3 -0
  36. data/spec/unit/berkshelf/berksfile_spec.rb +31 -74
  37. data/spec/unit/berkshelf/cookbook_generator_spec.rb +4 -0
  38. data/spec/unit/berkshelf/installer_spec.rb +4 -7
  39. data/spec/unit/berkshelf/lockfile_parser_spec.rb +124 -0
  40. data/spec/unit/berkshelf/lockfile_spec.rb +140 -163
  41. metadata +11 -6
  42. data/features/licenses.feature +0 -79
  43. data/features/step_definitions/lockfile_steps.rb +0 -57
@@ -1,9 +1,6 @@
1
1
  require_relative 'dependency'
2
2
 
3
3
  module Berkshelf
4
- # The object representation of the Berkshelf lockfile. The lockfile is useful
5
- # when working in teams where the same cookbook versions are desired across
6
- # multiple workstations.
7
4
  class Lockfile
8
5
  class << self
9
6
  # Initialize a Lockfile from the given filepath
@@ -24,7 +21,10 @@ module Berkshelf
24
21
  end
25
22
  end
26
23
 
27
- DEFAULT_FILENAME = "Berksfile.lock"
24
+ DEFAULT_FILENAME = 'Berksfile.lock'
25
+
26
+ DEPENDENCIES = 'DEPENDENCIES'
27
+ GRAPH = 'GRAPH'
28
28
 
29
29
  include Berkshelf::Mixin::Logging
30
30
 
@@ -36,6 +36,10 @@ module Berkshelf
36
36
  # the Berksfile for this Lockfile
37
37
  attr_reader :berksfile
38
38
 
39
+ # @return [Hash]
40
+ # the dependency graph
41
+ attr_reader :graph
42
+
39
43
  # Create a new lockfile instance associated with the given Berksfile. If a
40
44
  # Lockfile exists, it is automatically loaded. Otherwise, an empty instance is
41
45
  # created and ready for use.
@@ -48,69 +52,84 @@ module Berkshelf
48
52
  @filepath = options[:filepath].to_s
49
53
  @berksfile = options[:berksfile]
50
54
  @dependencies = {}
55
+ @graph = Graph.new(self)
56
+
57
+ parse if File.exists?(@filepath)
58
+ end
59
+
60
+ # Parse the lockfile.
61
+ #
62
+ # @return true
63
+ def parse
64
+ LockfileParser.new(self).run
65
+ true
66
+ rescue => e
67
+ raise LockfileParserError.new(e)
68
+ end
69
+
70
+ # Determine if this lockfile actually exists on disk.
71
+ #
72
+ # @return [Boolean]
73
+ # true if this lockfile exists on the disk, false otherwise
74
+ def present?
75
+ File.exists?(filepath) && !File.read(filepath).strip.empty?
76
+ end
51
77
 
52
- load! if File.exists?(@filepath)
78
+ # Determine if we can "trust" this lockfile. A lockfile is trustworthy if:
79
+ #
80
+ # 1. All dependencies defined in the Berksfile are present in this
81
+ # lockfile
82
+ # 2. Each dependency's constraint in the Berksfile is still satisifed by
83
+ # the currently locked version
84
+ #
85
+ # This method does _not_ account for leaky dependencies (i.e. dependencies
86
+ # defined in the lockfile that are no longer present in the Berksfile); this
87
+ # edge case is handed by the installer.
88
+ #
89
+ # @return [Boolean]
90
+ # true if this lockfile is trusted, false otherwise
91
+ def trusted?
92
+ berksfile.dependencies.all? do |dependency|
93
+ locked = find(dependency)
94
+ graphed = graph.find(dependency)
95
+ constraint = dependency.version_constraint
96
+
97
+ locked && graphed &&
98
+ dependency.location == locked.location &&
99
+ constraint.satisfies?(graphed.version)
100
+ end
53
101
  end
54
102
 
55
- # Resolve this Berksfile and apply the locks found in the generated Berksfile.lock to the
56
- # target Chef environment
103
+ # Resolve this Berksfile and apply the locks found in the generated
104
+ # +Berksfile.lock+ to the target Chef environment
57
105
  #
58
- # @param [String] environment_name
106
+ # @param [String] name
107
+ # the name of the environment to apply the locks to
59
108
  #
60
109
  # @option options [Hash] :ssl_verify (true)
61
110
  # Disable/Enable SSL verification during uploads
62
111
  #
63
112
  # @raise [EnvironmentNotFound]
64
- # if the target environment was not found
113
+ # if the target environment was not found on the remote Chef Server
65
114
  # @raise [ChefConnectionError]
66
- # if you are locking cookbooks with an invalid or not-specified client configuration
67
- def apply(environment_name, options = {})
68
- Berkshelf.ridley_connection(options) do |conn|
69
- unless environment = conn.environment.find(environment_name)
70
- raise EnvironmentNotFound.new(environment_name)
71
- end
115
+ # if you are locking cookbooks with an invalid or not-specified client
116
+ # configuration
117
+ def apply(name, options = {})
118
+ Berkshelf.ridley_connection(options) do |connection|
119
+ environment = connection.environment.find(name)
72
120
 
73
- environment.cookbook_versions = {}.tap do |cookbook_versions|
74
- dependencies.each do |dependency|
75
- if dependency.locked_version.nil?
76
- # A locked version must be present for each entry. Older versions of the lockfile
77
- # may have contained dependencies with a special type of location that would attempt
78
- # to dynamically determine the locked version. This is incorrect and the Lockfile
79
- # should be regenerated if that is the case.
80
- raise InvalidLockFile, "Your lockfile contains a dependency without a locked version. This " +
81
- "may be because you have an old lockfile. Regenerate your lockfile and try again."
82
- end
121
+ raise EnvironmentNotFound.new(name) if environment.nil?
83
122
 
84
- cookbook_versions[dependency.name] = "= #{dependency.locked_version.to_s}"
85
- end
123
+ locks = graph.locks.inject({}) do |hash, (name, dependency)|
124
+ hash[name] = "= #{dependency.locked_version.to_s}"
125
+ hash
86
126
  end
87
127
 
128
+ environment.cookbook_versions = locks
88
129
  environment.save
89
130
  end
90
131
  end
91
132
 
92
- # Load the lockfile from file system.
93
- def load!
94
- contents = File.read(filepath).strip
95
- hash = parse(contents)
96
-
97
- hash[:dependencies].each do |name, options|
98
- # Dynamically calculate paths relative to the Berksfile if a path is given
99
- options[:path] &&= File.expand_path(options[:path], File.dirname(filepath))
100
-
101
- begin
102
- dependency = Berkshelf::Dependency.new(berksfile, name.to_s, options)
103
- next if dependency.location && !dependency.location.valid?
104
- add(dependency)
105
- rescue Berkshelf::CookbookNotFound
106
- # It's possible that a source is locked that contains a path location, and
107
- # that path location was renamed or no longer exists. When loading the
108
- # lockfile, Berkshelf will throw an error if it can't find a cookbook that
109
- # previously existed at a path location.
110
- end
111
- end
112
- end
113
-
114
133
  # The list of dependencies constrained in this lockfile.
115
134
  #
116
135
  # @return [Array<Berkshelf::Dependency>]
@@ -129,7 +148,7 @@ module Berkshelf
129
148
  # @return [Berkshelf::Dependency, nil]
130
149
  # the cookbook dependency from this lockfile or nil if one was not found
131
150
  def find(dependency)
132
- @dependencies[cookbook_name(dependency).to_s]
151
+ @dependencies[Dependency.name(dependency)]
133
152
  end
134
153
 
135
154
  # Determine if this lockfile contains the given dependency.
@@ -139,222 +158,435 @@ module Berkshelf
139
158
  #
140
159
  # @return [Boolean]
141
160
  # true if the dependency exists, false otherwise
142
- def has_dependency?(dependency)
161
+ def dependency?(dependency)
143
162
  !find(dependency).nil?
144
163
  end
164
+ alias_method :has_dependency?, :dependency?
145
165
 
146
- # Replace the current list of dependencies with `dependencies`. This method does
147
- # not write out the lockfile - it only changes the state of the object.
166
+ # Add a new cookbok to the lockfile. If an entry already exists by the
167
+ # given name, it will be overwritten.
148
168
  #
149
- # @param [Array<Berkshelf::Dependency>] dependencies
150
- # the list of dependencies to update
151
- def update(dependencies)
152
- reset_dependencies!
169
+ # @param [Dependency] dependency
170
+ # the dependency to add
171
+ #
172
+ # @return [Dependency]
173
+ def add(dependency)
174
+ @dependencies[Dependency.name(dependency)] = dependency
175
+ end
176
+
177
+ # Retrieve information about a given cookbook that is in this lockfile.
178
+ #
179
+ # @raise [DependencyNotFound]
180
+ # if this lockfile does not have the given dependency
181
+ # @raise [CookbookNotFound]
182
+ # if this lockfile has the dependency, but the cookbook is not downloaded
183
+ #
184
+ # @param [String, Dependency] dependency
185
+ # the dependency or name of the dependency to find
186
+ #
187
+ # @return [CachedCookbook]
188
+ # the CachedCookbook that corresponds to the given name parameter
189
+ def retrieve(dependency)
190
+ locked = graph.locks[Dependency.name(dependency)]
153
191
 
154
- dependencies.each { |dependency| append(dependency) }
155
- save
192
+ if locked.nil?
193
+ raise DependencyNotFound.new(Dependency.name(dependency))
194
+ end
195
+
196
+ unless locked.downloaded?
197
+ raise CookbookNotFound, "Could not find cookbook '#{locked.to_s}'. " \
198
+ "Run `berks install` to download and install the missing cookbook."
199
+ end
200
+
201
+ locked.cached_cookbook
156
202
  end
157
203
 
158
- # Add the given dependency to the `dependencies` list, if it doesn't already exist.
204
+ # Replace the list of dependencies.
159
205
  #
160
- # @param [Berkshelf::Dependency] dependency
161
- # the dependency to append to the dependencies list
162
- def add(dependency)
163
- @dependencies[cookbook_name(dependency)] = dependency
206
+ # @param [Array<Berkshelf::Dependency>] dependencies
207
+ # the list of dependencies to update
208
+ def update(dependencies)
209
+ @dependencies = {}
210
+
211
+ dependencies.each do |dependency|
212
+ @dependencies[Dependency.name(dependency)] = dependency
213
+ end
164
214
  end
165
- alias_method :append, :add
166
215
 
167
- # Remove the given dependency from this lockfile. This method accepts a dependency
168
- # attribute which may either be the name of a cookbook (String) or an
169
- # actual cookbook dependency.
216
+ # Remove the given dependency from this lockfile. This method accepts a
217
+ # +dependency+ attribute which may either be the name of a cookbook, as a
218
+ # String or an actual {Dependency} object.
170
219
  #
171
- # @param [String, Berkshelf::Dependency] dependency
172
- # the cookbook dependency/name to remove
220
+ # This method first removes the dependency from the list of top-level
221
+ # dependencies. Then it uses a recursive algorithm to safely remove any
222
+ # other dependencies from the graph that are no longer needed.
173
223
  #
174
224
  # @raise [Berkshelf::CookbookNotFound]
175
225
  # if the provided dependency does not exist
176
- def remove(dependency)
177
- unless has_dependency?(dependency)
178
- raise Berkshelf::CookbookNotFound, "'#{cookbook_name(dependency)}' does not exist in this lockfile!"
226
+ #
227
+ # @param [String] dependency
228
+ # the name of the cookbook to remove
229
+ def unlock(dependency)
230
+ unless dependency?(dependency)
231
+ raise Berkshelf::CookbookNotFound, "'#{dependency}' does not exist in this lockfile!"
232
+ end
233
+
234
+ @dependencies.delete(Dependency.name(dependency))
235
+ graph.remove(dependency)
236
+ end
237
+
238
+ # Write the contents of the current statue of the lockfile to disk. This
239
+ # method uses an atomic file write. A temporary file is created, written,
240
+ # and then copied over the existing one. This ensures any partial updates
241
+ # or failures do no affect the lockfile. The temporary file is ensured
242
+ # deletion.
243
+ #
244
+ # @return [true, false]
245
+ # true if the lockfile was saved, false otherwise
246
+ def save
247
+ return false if dependencies.empty?
248
+
249
+ tempfile = Tempfile.new(['Berksfile', '.lock'])
250
+
251
+ tempfile.write(DEPENDENCIES)
252
+ tempfile.write("\n")
253
+ dependencies.sort.each do |dependency|
254
+ tempfile.write(dependency.to_lock)
179
255
  end
180
256
 
181
- @dependencies.delete(cookbook_name(dependency))
257
+ tempfile.write("\n")
258
+ tempfile.write(graph.to_lock)
259
+
260
+ tempfile.rewind
261
+ tempfile.close
262
+
263
+ # Move the lockfile into place
264
+ FileUtils.cp(tempfile.path, filepath)
265
+
266
+ true
267
+ ensure
268
+ tempfile.unlink if tempfile
182
269
  end
183
- alias_method :unlock, :remove
184
270
 
185
- # @return [String]
186
- # the string representation of the lockfile
271
+ # @private
187
272
  def to_s
188
273
  "#<Berkshelf::Lockfile #{Pathname.new(filepath).basename}>"
189
274
  end
190
275
 
191
- # @return [String]
192
- # the detailed string representation of the lockfile
276
+ # @private
193
277
  def inspect
194
278
  "#<Berkshelf::Lockfile #{Pathname.new(filepath).basename}, dependencies: #{dependencies.inspect}>"
195
279
  end
196
280
 
197
- # Write the current lockfile to a hash
198
- #
199
- # @return [Hash]
200
- # the hash representation of this lockfile
201
- # * :dependencies [Array<Berkshelf::Dependency>] the list of dependencies
202
- def to_hash
203
- {
204
- dependencies: @dependencies
205
- }
206
- end
281
+ private
207
282
 
208
- # The JSON representation of this lockfile
209
- #
210
- # Relies on {#to_hash} to generate the json
211
- #
212
- # @return [String]
213
- # the JSON representation of this lockfile
214
- def to_json(options = {})
215
- JSON.pretty_generate(to_hash, options)
216
- end
283
+ # The class responsible for parsing the lockfile and turning it into a
284
+ # useful data structure.
285
+ class LockfileParser
286
+ NAME_VERSION = '(?! )(.*?)(?: \(([^-]*)(?:-(.*))?\))?'
287
+ DEPENDENCY_PATTERN = /^ {2}#{NAME_VERSION}$/
288
+ DEPENDENCIES_PATTERN = /^ {4}#{NAME_VERSION}$/
289
+ OPTION_PATTERN = /^ {4}(.+)\: (.+)/
217
290
 
218
- private
291
+ # Create a new lockfile parser.
292
+ #
293
+ # @param [Lockfile]
294
+ def initialize(lockfile)
295
+ @lockfile = lockfile
296
+ @berksfile = lockfile.berksfile
297
+ end
219
298
 
220
- # Parse the given string as JSON.
299
+ # Parse the lockfile contents, adding the correct things to the lockfile.
221
300
  #
222
- # @param [String] contents
301
+ # @return [true]
302
+ def run
303
+ @parsed_dependencies = {}
304
+
305
+ contents = File.read(@lockfile.filepath)
306
+
307
+ if contents.strip.empty?
308
+ Berkshelf.formatter.warn "Your lockfile at '#{@lockfile.filepath}' " \
309
+ "is empty. I am going to parse it anyway, but there is a chance " \
310
+ "that a larger problem is at play. If you manually edited your " \
311
+ "lockfile, you may have corrupted it."
312
+ end
313
+
314
+ if contents.strip[0] == '{'
315
+ Berkshelf.formatter.warn "It looks like you are using an older " \
316
+ "version of the lockfile. Attempting to convert..."
317
+
318
+ dependencies = "#{Lockfile::DEPENDENCIES}\n"
319
+ graph = "#{Lockfile::GRAPH}\n"
320
+
321
+ begin
322
+ hash = JSON.parse(contents)
323
+ rescue JSON::ParserError
324
+ Berkshelf.formatter.warn "Could not convert lockfile! This is a " \
325
+ "problem. You see, previous versions of the lockfile were " \
326
+ "actually a lie. It lied to you about your version locks, and we " \
327
+ "are really sorry about that.\n\n" \
328
+ "Here's the good news - we fixed it!\n\n" \
329
+ "Here's the bad news - you probably should not trust your old " \
330
+ "lockfile. You should manually delete your old lockfile and " \
331
+ "re-run the installer."
332
+ end
333
+
334
+ hash['dependencies'] && hash['dependencies'].sort .each do |name, info|
335
+ dependencies << " #{name} (>= 0.0.0)\n"
336
+ info.each do |key, value|
337
+ unless key == 'locked_version'
338
+ dependencies << " #{key}: #{value}\n"
339
+ end
340
+ end
341
+
342
+ graph << " #{name} (#{info['locked_version']})\n"
343
+ end
344
+
345
+ contents = "#{dependencies}\n#{graph}"
346
+ end
347
+
348
+ contents.split(/(?:\r?\n)+/).each do |line|
349
+ if line == Lockfile::DEPENDENCIES
350
+ @state = :dependency
351
+ elsif line == Lockfile::GRAPH
352
+ @state = :graph
353
+ else
354
+ send("parse_#{@state}", line)
355
+ end
356
+ end
357
+
358
+ @parsed_dependencies.each do |name, options|
359
+ dependency = Dependency.new(@berksfile, name, options)
360
+ @lockfile.add(dependency)
361
+ end
362
+
363
+ true
364
+ end
365
+
366
+ private
367
+
368
+ # Parse a dependency line.
223
369
  #
224
- # @return [Hash]
225
- def parse(contents)
226
- # Ruby's JSON.parse cannot handle an empty string/file
227
- return { dependencies: [] } if contents.strip.empty?
228
-
229
- hash = JSON.parse(contents, symbolize_names: true)
230
-
231
- # Legacy support for 2.0 lockfiles
232
- # @todo Remove in 4.0
233
- if hash[:sources]
234
- LockfileLegacy.warn!
235
- hash[:dependencies] = hash.delete(:sources)
370
+ # @param [String] line
371
+ def parse_dependency(line)
372
+ if line =~ DEPENDENCY_PATTERN
373
+ name, version = $1, $2
374
+
375
+ @parsed_dependencies[name] ||= {}
376
+ @parsed_dependencies[name][:constraint] = version if version
377
+ @current_dependency = @parsed_dependencies[name]
378
+ elsif line =~ OPTION_PATTERN
379
+ key, value = $1, $2
380
+ @current_dependency[key.to_sym] = value
236
381
  end
382
+ end
237
383
 
238
- return hash
239
- rescue Exception => e
240
- # Legacy support for 1.0 lockfiles
241
- # @todo Remove in 4.0
242
- if e.class == JSON::ParserError && contents =~ /^cookbook ["'](.+)["']/
243
- LockfileLegacy.warn!
244
- return LockfileLegacy.parse(berksfile, contents)
245
- else
246
- raise Berkshelf::LockfileParserError.new(filepath, e)
384
+ # Parse a graph line.
385
+ #
386
+ # @param [String] line
387
+ def parse_graph(line)
388
+ if line =~ DEPENDENCY_PATTERN
389
+ name, version = $1, $2
390
+
391
+ @lockfile.graph.find(name) || @lockfile.graph.add(name, version)
392
+ @current_lock = name
393
+ elsif line =~ DEPENDENCIES_PATTERN
394
+ name, constraint = $1, $2
395
+ @lockfile.graph.find(@current_lock).add_dependency(name, constraint)
247
396
  end
248
397
  end
398
+ end
249
399
 
250
- # Save the contents of the lockfile to disk.
251
- def save
252
- File.open(filepath, 'w') do |file|
253
- file.write to_json + "\n"
400
+ # The class representing an internal graph.
401
+ class Graph
402
+ # Create a new Lockfile graph.
403
+ #
404
+ # Some clarifying terminology:
405
+ #
406
+ # yum-epel (0.2.0) <- lock
407
+ # yum (~> 3.0) <- dependency
408
+ #
409
+ # @return [Graph]
410
+ def initialize(lockfile)
411
+ @lockfile = lockfile
412
+ @berksfile = lockfile.berksfile
413
+ @graph = {}
414
+ end
415
+
416
+ # The list of locks for this graph. Dependencies are retrieved from the
417
+ # lockfile, then the Berksfile, and finally a new dependency object is
418
+ # created if none of those exist.
419
+ #
420
+ # @return [Hash<String, Dependency>]
421
+ # a key-value hash where the key is the name of the cookbook and the
422
+ # value is the locked dependency
423
+ def locks
424
+ @graph.sort.inject({}) do |hash, (name, item)|
425
+ dependency = @lockfile.find(name) ||
426
+ @berksfile && @berksfile.find(name) ||
427
+ Dependency.new(@berksfile, name)
428
+ dependency.locked_version = item.version
429
+
430
+ hash[item.name] = dependency
431
+ hash
254
432
  end
255
433
  end
256
434
 
257
- def reset_dependencies!
258
- @dependencies = {}
435
+ # Find a given dependency in the graph.
436
+ #
437
+ # @param [Dependency, String]
438
+ # the name/dependency to find
439
+ #
440
+ # @return [GraphItem, nil]
441
+ # the item for the name
442
+ def find(dependency)
443
+ @graph[Dependency.name(dependency)]
259
444
  end
260
445
 
261
- # Return the name of this cookbook (because it's the key in our
262
- # table).
446
+ # Find if the given lock exists?
263
447
  #
264
- # @param [Berkshelf::Dependency, #to_s] dependency
265
- # the dependency to find the name from
448
+ # @param [Dependency, String]
449
+ # the name/dependency to find
266
450
  #
267
- # @return [String]
451
+ # @return [true, false]
452
+ def lock?(dependency)
453
+ !find(dependency).nil?
454
+ end
455
+ alias_method :has_lock?, :lock?
456
+
457
+ # Determine if this graph contains the given dependency. This method is
458
+ # used by the lockfile when adding or removing dependencies to see if a
459
+ # dependency can be safely removed.
460
+ #
461
+ # @param [Dependency, String] dependency
462
+ # the name/dependency to find
463
+ def dependency?(dependency)
464
+ @graph.values.any? do |item|
465
+ item.dependencies.key?(Dependency.name(dependency))
466
+ end
467
+ end
468
+ alias_method :has_dependency?, :dependency?
469
+
470
+ # Add each a new {GraphItem} to the graph.
471
+ #
472
+ # @param [#to_s] name
268
473
  # the name of the cookbook
269
- def cookbook_name(dependency)
270
- dependency.is_a?(Berkshelf::Dependency) ? dependency.name : dependency.to_s
474
+ # @param [#to_s] version
475
+ # the version of the lock
476
+ #
477
+ # @return [GraphItem]
478
+ def add(name, version)
479
+ @graph[name.to_s] = GraphItem.new(name, version)
271
480
  end
272
481
 
273
- # Legacy support for old lockfiles
482
+ # Recursively remove any dependencies from the graph unless they exist as
483
+ # top-level dependencies or nested dependencies.
274
484
  #
275
- # @todo Remove this class in Berkshelf 3.0.0
276
- class LockfileLegacy
277
- class << self
278
- # Read the old lockfile content and instance eval in context.
279
- #
280
- # @param [Berkshelf::Berksfile] berksfile
281
- # the associated berksfile
282
- # @param [String] content
283
- # the string content read from a legacy lockfile
284
- def parse(berksfile, content)
285
- dependencies = {}.tap do |hash|
286
- content.split("\n").each do |line|
287
- next if line.empty?
288
- source = new(berksfile, line)
289
- hash[source.name] = source.options
290
- end
291
- end
485
+ # @param [Dependency, String] dependency
486
+ # the name/dependency to remove
487
+ def remove(dependency)
488
+ name = Dependency.name(dependency)
292
489
 
293
- {
294
- dependencies: dependencies,
295
- }
296
- end
490
+ return if @lockfile.dependency?(name) || dependency?(name)
297
491
 
298
- # Warn the user they he/she is using an old Lockfile format.
299
- #
300
- # This automatically outputs to the {Berkshelf.ui}; nothing is
301
- # returned.
302
- #
303
- # @return [nil]
304
- def warn!
305
- Berkshelf.ui.warn(warning_message)
306
- end
492
+ # Grab the nested dependencies for this particular entry so we can
493
+ # recurse and try to remove them from the graph.
494
+ locked = @graph[name]
495
+ nested_dependencies = locked && locked.dependencies.keys || []
496
+
497
+ # Now delete the entry
498
+ @graph.delete(name)
499
+
500
+ # Recursively try to delete the remaining dependencies for this item
501
+ nested_dependencies.each(&method(:remove))
502
+ end
503
+
504
+ # Update the graph with the given cookbooks. This method destroys the
505
+ # existing dependency graph with this new result!
506
+ #
507
+ # @param [Array<CachedCookbook>]
508
+ # the list of cookbooks to populate the graph with
509
+ def update(cookbooks)
510
+ @graph = {}
511
+
512
+ cookbooks.each do |cookbook|
513
+ @graph[cookbook.cookbook_name.to_s] = GraphItem.new(
514
+ cookbook.name,
515
+ cookbook.version,
516
+ cookbook.dependencies,
517
+ )
518
+ end
519
+ end
307
520
 
308
- private
309
- # @return [String]
310
- def warning_message
311
- 'You are using the old lockfile format. Attempting to convert...'
521
+ # Write the contents of the graph to the lockfile format.
522
+ #
523
+ # The resulting format looks like:
524
+ #
525
+ # GRAPH
526
+ # apache2 (1.8.14)
527
+ # yum-epel (0.2.0)
528
+ # yum (~> 3.0)
529
+ #
530
+ # @example lockfile.graph.to_lock #=> "GRAPH\n apache2 (1.18.14)\n..."
531
+ #
532
+ # @return [String]
533
+ #
534
+ def to_lock
535
+ out = "#{Lockfile::GRAPH}\n"
536
+ @graph.sort.each do |name, item|
537
+ out << " #{name} (#{item.version})\n"
538
+
539
+ unless item.dependencies.empty?
540
+ item.dependencies.sort.each do |name, constraint|
541
+ out << " #{name} (#{constraint})\n"
312
542
  end
543
+ end
313
544
  end
314
545
 
315
- # @return [Hash]
316
- # the hash of options
317
- attr_reader :options
546
+ out
547
+ end
548
+
549
+ private
318
550
 
551
+ # A single item inside the graph.
552
+ class GraphItem
553
+ # The name of the cookbook that corresponds to this graph item.
554
+ #
319
555
  # @return [String]
320
- # the name of this cookbook
556
+ # the name of the cookbook
321
557
  attr_reader :name
322
558
 
323
- # @return [Berkshelf::Berksfile]
324
- # the berksfile
325
- attr_reader :berksfile
559
+ # The locked version for this graph item.
560
+ #
561
+ # @return [String]
562
+ # the locked version of the graph item (as a string)
563
+ attr_reader :version
326
564
 
327
- # Create a new legacy lockfile for processing
565
+ # The list of dependencies and their constraints.
328
566
  #
329
- # @param [String] content
330
- # the content to parse out and convert to a hash
331
- def initialize(berksfile, content)
332
- @berksfile = berksfile
333
- instance_eval(content).to_hash
567
+ # @return [Hash<String, String>]
568
+ # the list of dependencies for this graph item, where the key
569
+ # corresponds to the name of the dependency and the value is the
570
+ # version constraint.
571
+ attr_reader :dependencies
572
+
573
+ # Create a new graph item.
574
+ def initialize(name, version, dependencies = {})
575
+ @name = name.to_s
576
+ @version = version.to_s
577
+ @dependencies = dependencies
334
578
  end
335
579
 
336
- # Method defined in legacy lockfiles (since we are using instance_eval).
580
+ # Add a new dependency to the list.
337
581
  #
338
- # @param [String] name
339
- # the name of this cookbook
340
- # @option options [String] :locked_version
341
- # the locked version of this cookbook
342
- def cookbook(name, options = {})
343
- @name = name
344
- @options = manipulate(options)
582
+ # @param [#to_s] name
583
+ # the name to use
584
+ # @param [#to_s] constraint
585
+ # the version constraint to use
586
+ def add_dependency(name, constraint)
587
+ @dependencies[name.to_s] = constraint.to_s
345
588
  end
346
-
347
- private
348
-
349
- # Perform various manipulations on the hash.
350
- #
351
- # @param [Hash] options
352
- def manipulate(options = {})
353
- if options[:path]
354
- options[:path] = berksfile.find(name).instance_variable_get(:@options)[:path] || options[:path]
355
- end
356
- options
357
- end
358
589
  end
590
+ end
359
591
  end
360
592
  end