lib/berkshelf/lockfile.rb in berkshelf-1.4.6 vs lib/berkshelf/lockfile.rb in berkshelf-2.0.0.beta

- old
+ new

@@ -1,54 +1,251 @@ module Berkshelf + # The object representation of the Berkshelf lockfile. The lockfile is useful + # when working in teams where the same cookbook versions are desired across + # multiple workstations. + # + # @author Seth Vargo <sethvargo@gmail.com> class Lockfile - class << self - def remove! - FileUtils.rm_f DEFAULT_FILENAME - end + # @return [Pathname] + # the path to this Lockfile + attr_reader :filepath - # @param [Array<CookbookSource>] sources - def update!(sources) - contents = File.readlines(DEFAULT_FILENAME) - contents.delete_if do |line| - line =~ /cookbook '(#{sources.map(&:name).join('|')})'/ - end + # @return [Berkshelf::Berksfile] + # the Berksfile for this Lockfile + attr_reader :berksfile - contents += sources.map { |source| definition(source) } - File.open(DEFAULT_FILENAME, 'wb') { |f| f.write(contents.join("\n").squeeze("\n")) } - end + # @return [String] + # the last known SHA of the Berksfile + attr_accessor :sha - # @param [CookbookSource] source - # - # @return [String] - def definition(source) - definition = "cookbook '#{source.name}'" + # Create a new lockfile instance associated with the given Berksfile. If a + # Lockfile exists, it is automatically loaded. Otherwise, an empty instance is + # created and ready for use. + # + # @param berksfile [Berkshelf::Berksfile] + # the Berksfile associated with this Lockfile + def initialize(berksfile) + @berksfile = berksfile + @filepath = File.expand_path("#{berksfile.filepath}.lock") + @sources = {} - if source.location.is_a?(GitLocation) - definition += ", :git => '#{source.location.uri}', :ref => '#{source.location.branch || 'HEAD'}'" - elsif source.location.is_a?(PathLocation) - definition += ", :path => '#{source.location.path}'" + load! if File.exists?(@filepath) + end + + # Load the lockfile from file system. + def load! + contents = File.read(filepath) + + begin + hash = JSON.parse(contents, symbolize_names: true) + rescue JSON::ParserError + if contents =~ /^cookbook ["'](.+)["']/ + Berkshelf.ui.warn "You are using the old lockfile format. Attempting to convert..." + hash = LockfileLegacy.parse(contents) else - definition += ", :locked_version => '#{source.locked_version}'" + raise end + end - definition + @sha = hash[:sha] + + hash[:sources].each do |name, options| + add(CookbookSource.new(berksfile, name.to_s, options)) end end - DEFAULT_FILENAME = "#{Berkshelf::DEFAULT_FILENAME}.lock".freeze + # Set the sha value to nil to mark that the lockfile is not out of + # sync with the Berksfile. + def reset_sha! + @sha = nil + end - attr_reader :sources + # The list of sources constrained in this lockfile. + # + # @return [Array<Berkshelf::CookbookSource>] + # the list of sources in this lockfile + def sources + @sources.values + end - def initialize(sources) - @sources = Array(sources) + # Find the given source in this lockfile. This method accepts a source + # attribute which may either be the name of a cookbook (String) or an + # actual cookbook source. + # + # @param [String, Berkshelf::CookbookSource] source + # the cookbook source/name to find + # @return [CookbookSource, nil] + # the cookbook source from this lockfile or nil if one was not found + def find(source) + @sources[cookbook_name(source).to_s] end - def write(filename = DEFAULT_FILENAME) - content = sources.map { |source| self.class.definition(source) }.join("\n") - File.open(filename, "wb") { |f| f.write content } + # Determine if this lockfile contains the given source. + # + # @param [String, Berkshelf::CookbookSource] source + # the cookbook source/name to determine existence of + # @return [Boolean] + # true if the source exists, false otherwise + def has_source?(source) + !find(source).nil? end - def remove! - self.class.remove! + # Replace the current list of sources with `sources`. This method does + # not write out the lockfile - it only changes the state of the object. + # + # @param [Array<Berkshelf::CookbookSource>] sources + # the list of sources to update + # @option options [String] :sha + # the sha of the Berksfile updating the sources + def update(sources, options = {}) + reset_sources! + @sha = options[:sha] + + sources.each { |source| append(source) } + save end + + # Add the given source to the `sources` list, if it doesn't already exist. + # + # @param [Berkshelf::CookbookSource] source + # the source to append to the sources list + def add(source) + @sources[cookbook_name(source)] = source + end + alias_method :append, :add + + # Remove the given source from this lockfile. This method accepts a source + # attribute which may either be the name of a cookbook (String) or an + # actual cookbook source. + # + # @param [String, Berkshelf::CookbookSource] source + # the cookbook source/name to remove + # + # @raise [Berkshelf::CookbookNotFound] + # if the provided source does not exist + def remove(source) + unless has_source?(source) + raise Berkshelf::CookbookNotFound, "'#{cookbook_name(source)}' does not exist in this lockfile!" + end + + @sources.delete(cookbook_name(source)) + end + alias_method :unlock, :remove + + # @return [String] + # the string representation of the lockfile + def to_s + "#<Berkshelf::Lockfile #{Pathname.new(filepath).basename}>" + end + + # @return [String] + # the detailed string representation of the lockfile + def inspect + "#<Berkshelf::Lockfile #{Pathname.new(filepath).basename}, sources: #{sources.inspect}>" + end + + # Write the current lockfile to a hash + # + # @return [Hash] + # the hash representation of this lockfile + # * :sha [String] the last-known sha for the berksfile + # * :sources [Array<Berkshelf::CookbookSource>] the list of sources + def to_hash + { + sha: sha, + sources: @sources + } + end + + # The JSON representation of this lockfile + # + # Relies on {#to_hash} to generate the json + # + # @return [String] + # the JSON representation of this lockfile + def to_json(options = {}) + JSON.pretty_generate(to_hash, options) + end + + private + + # Save the contents of the lockfile to disk. + def save + File.open(filepath, 'w') do |file| + file.write to_json + "\n" + end + end + + # Clear the sources array + def reset_sources! + @sources = {} + end + + # Return the name of this cookbook (because it's the key in our + # table). + # + # @param [Berkshelf::CookbookSource, #to_s] source + # the source to find the name from + # + # @return [String] + # the name of the cookbook + def cookbook_name(source) + source.is_a?(CookbookSource) ? source.name : source.to_s + end + + # Legacy support for old lockfiles + # + # @author Seth Vargo <sethvargo@gmail.com> + # @todo Remove this class in the next major release. + class LockfileLegacy + class << self + # Read the old lockfile content and instance eval in context. + # + # @param [String] content + # the string content read from a legacy lockfile + def parse(content) + sources = {}.tap do |hash| + content.split("\n").each do |line| + next if line.empty? + + source = self.new(line) + hash[source.name] = source.options + end + end + + { + sha: nil, + sources: sources + } + end + end + + # @return [Hash] + # the hash of options + attr_reader :options + + # @return [String] + # the name of this cookbook + attr_reader :name + + # Create a new legacy lockfile for processing + # + # @param [String] content + # the content to parse out and convert to a hash + def initialize(content) + instance_eval(content).to_hash + end + + # Method defined in legacy lockfiles (since we are using + # instance_eval). + # + # @param [String] name + # the name of this cookbook + # @option options [String] :locked_version + # the locked version of this cookbook + def cookbook(name, options = {}) + @name = name + @options = options + end + end end end