lib/braid/config.rb in braid-1.0.22 vs lib/braid/config.rb in braid-1.1.0

- old
+ new

@@ -1,11 +1,62 @@ require 'yaml' require 'json' require 'yaml/store' +# Some info about the configuration versioning design: +# https://github.com/cristibalan/braid/issues/66#issuecomment-354211311 +# +# Current configuration format: +# ``` +# { +# "config_version": 1, +# "mirrors": { +# <mirror path: string>: { +# "url": <upstream URL: string>, +# "path": <remote path: string>, +# "branch": <upstream branch: string>, +# "tag": <upstream tag: string>, +# "revision": <current upstream revision: string> +# } +# } +# } +# ``` +# +# History of configuration formats understood by current Braid: +# +# - Braid 1.1.0, config_version 1: +# - "config_version" introduced; mirrors moved to "mirrors" +# - Single-file mirrors (f340b0c) +# - Braid 1.0.18: +# - Locked mirrors indicated by absence of "branch" and "tag" attributes, not +# presence of "lock" attribute (e6535aa) +# - Braid 1.0.17: +# - Support for full-history mirrors ("squashed": false) removed; "squashed" +# attribute no longer written (eb72030) +# - Braid 1.0.11: +# - "remote" attribute no longer written (f8fd088) +# - Braid 1.0.9: +# - .braids -> .braids.json (6806c61) +# - Braid 1.0.0: +# - YAML -> JSON (9d3fa11) +# - Support for Subversion mirrors removed ("type": "svn") removed (9d8d390) +# +# +# (Entries that predate the creation of this list have commit IDs for reference. +# Of course, when adding a new entry, you can't add the commit ID in the same +# commit, but you don't need to because people can just run `git log` on this +# file.) + module Braid class Config + + MODE_UPGRADE = 1 + MODE_READ_ONLY = 2 + MODE_MAY_WRITE = 3 + + CURRENT_CONFIG_VERSION = 1 + class PathAlreadyInUse < BraidError def message "path already in use: #{super}" end end @@ -13,29 +64,92 @@ def message "mirror does not exist: #{super}" end end - def initialize(config_file = CONFIG_FILE, old_config_files = [OLD_CONFIG_FILE]) - @config_file = config_file - (old_config_files + [config_file]).each do |file| - next unless File.exist?(file) + class RemoveMirrorDueToBreakingChange < StandardError + end + + # For upgrade-config command only. XXX: Ideally would be immutable. + attr_reader :config_version, :config_existed, :breaking_change_descs + + # options: config_file, old_config_files, mode + def initialize(options = {}) + @config_file = options['config_file'] || CONFIG_FILE + old_config_files = options['old_config_files'] || [OLD_CONFIG_FILE] + @mode = options['mode'] || MODE_MAY_WRITE + + data = load_config(@config_file, old_config_files) + @config_existed = !data.nil? + if !@config_existed + @config_version = CURRENT_CONFIG_VERSION + @db = {} + elsif data['config_version'].is_a?(Numeric) + @config_version = data['config_version'] + @db = data['mirrors'] + else + # Before config versioning (Braid < 1.1.0) + @config_version = 0 + @db = data + end + + if @config_version > CURRENT_CONFIG_VERSION + raise BraidError, <<-MSG +This version of Braid (#{VERSION}) is too old to understand your project's Braid +configuration file (version #{@config_version}). See the instructions at +https://cristibalan.github.io/braid/config_versions.html to install and use a +compatible newer version of Braid. +MSG + end + + # In all modes, instantiate all mirrors to scan for breaking changes. + @breaking_change_descs = [] + paths_to_delete = [] + @db.each do |path, attributes| begin - store = YAML::Store.new(file) - @db = {} - store.transaction(true) do - store.roots.each do |path| - @db[path] = store[path] - end - end - return - rescue - @db = JSON.parse(file) - return if @db + mirror = Mirror.new(path, attributes, + lambda {|desc| @breaking_change_descs.push(desc)}) + # In MODE_UPGRADE, update @db now. In other modes, we won't write the + # config if an upgrade is needed, so it doesn't matter that we don't + # update @db. + # + # It's OK to change the values of existing keys during iteration: + # https://groups.google.com/d/msg/comp.lang.ruby/r5OI6UaxAAg/SVpU0cktmZEJ + write_mirror(mirror) if @mode == MODE_UPGRADE + rescue RemoveMirrorDueToBreakingChange + # I don't know if deleting during iteration is well-defined in all + # Ruby versions we support, so defer the deletion. + # ~ matt@mattmccutchen.net, 2017-12-31 + paths_to_delete.push(path) if @mode == MODE_UPGRADE end end - @db = {} + paths_to_delete.each do |path| + @db.delete(path) + end + + if @mode != MODE_UPGRADE && !@breaking_change_descs.empty? + raise BraidError, <<-MSG +This version of Braid (#{VERSION}) no longer supports a feature used by your +Braid configuration file (version #{@config_version}). Run 'braid upgrade-config --dry-run' +for information about upgrading your configuration file, or see the instructions +at https://cristibalan.github.io/braid/config_versions.html to install and run a +compatible older version of Braid. +MSG + end + + if @mode == MODE_MAY_WRITE && @config_version < CURRENT_CONFIG_VERSION + raise BraidError, <<-MSG +This command may need to write to your Braid configuration file, +but this version of Braid (#{VERSION}) cannot write to your configuration file +(currently version #{config_version}) without upgrading it to configuration version #{CURRENT_CONFIG_VERSION}, +which would force other developers on your project to upgrade Braid. Run +'braid upgrade-config' to proceed with the upgrade, or see the instructions at +https://cristibalan.github.io/braid/config_versions.html to install and run a +compatible older version of Braid. +MSG + end + end def add_from_options(url, options) mirror = Mirror.new_from_options(url, options) @@ -60,41 +174,66 @@ end def add(mirror) raise PathAlreadyInUse, mirror.path if get(mirror.path) write_mirror(mirror) + write_db end def remove(mirror) @db.delete(mirror.path) write_db end def update(mirror) raise MirrorDoesNotExist, mirror.path unless get(mirror.path) - @db.delete(mirror.path) write_mirror(mirror) - end - - private - - def write_mirror(mirror) - @db[mirror.path] = clean_attributes(mirror.attributes) write_db end + # Public for upgrade-config command only. def write_db new_db = {} @db.keys.sort.each do |key| - new_db[key] = @db[key] - new_db[key].keys.each do |k| - new_db[key].delete(k) unless Braid::Mirror::ATTRIBUTES.include?(k) + new_db[key] = {} + Braid::Mirror::ATTRIBUTES.each do |k| + new_db[key][k] = @db[key][k] if @db[key].has_key?(k) end end + new_data = { + 'config_version' => CURRENT_CONFIG_VERSION, + 'mirrors' => new_db + } File.open(@config_file, 'wb') do |f| - f.write JSON.pretty_generate(new_db) + f.write JSON.pretty_generate(new_data) f.write "\n" end + end + + private + + def load_config(config_file, old_config_files) + (old_config_files + [config_file]).each do |file| + next unless File.exist?(file) + begin + store = YAML::Store.new(file) + data = {} + store.transaction(true) do + store.roots.each do |path| + data[path] = store[path] + end + end + return data + rescue + data = JSON.parse(file) + return data if data + end + end + return nil + end + + def write_mirror(mirror) + @db[mirror.path] = clean_attributes(mirror.attributes) end def clean_attributes(hash) hash.reject { |k, v| v.nil? } end