diff --git a/README.md b/README.md
index 540567f..8c9753c 100644
--- a/README.md
+++ b/README.md
@@ -144,6 +144,54 @@ Go back to tracking a particular branch.
braid diff vendor/rails
+## Braid version compatibility
+
+Since Braid has been regularly changing the configuration format and adding new
+features that some projects may choose to rely on, and somewhat less often
+making breaking changes in how the configuration is handled, problems can arise
+if different developers work on the same project using different versions of
+Braid. Since version 1.1.0, Braid refuses to operate if it detects potentially
+problematic version skew. If this happens, Braid will tell you what you can do.
+If you'd like an overview of what to expect, read on.
+
+Roughly speaking, the `.braids.json` configuration file contains a configuration
+version number that corresponds to a range of compatible Braid minor versions
+(`x.y`). "Patch" upgrades to Braid (i.e., `x.y.z` -> `x.y.(z+1)`) will never
+(intentionally!) have configuration compatibility implications and are always
+recommended as they may fix critical bugs.
+
+If you use a Braid version too old for your configuration file, Braid will
+direct you to the [configuration version history page](config_versions.md) with
+instructions to upgrade Braid. If you use a Braid version too new, Braid will
+tell you how you can upgrade your configuration file or find a compatible older
+Braid version to use. (As an exception, a newer version of Braid can run
+read-only commands on an older configuration file without upgrading it if there
+are no breaking changes.) If you upgrade your configuration file, then other
+developers on the project may need to upgrade Braid. Braid does not support
+downgrading a configuration file, though you can revert the commit that upgraded
+it if you haven't made any subsequent changes to the configuration.
+
+If you work on multiple projects, you may need to install multiple versions of
+Braid and manually run the correct version for each project. Fortunately, the
+RubyGems system makes this reasonably straightforward.
+
+Another approach is to standardize the Braid version for a project by listing
+Braid in a `Gemfile` (either checking in `Gemfile.lock` or using a version
+constraint in the `Gemfile`) and run the project's version of Braid via
+[Bundler](http://bundler.io/) with `bundle exec braid`. Even non-Ruby projects
+can do this if it's acceptable to have a `Gemfile` and `Gemfile.lock`. Ruby
+projects that don't want Braid to interact with their other gems can potentially
+put the `Gemfile` in a subdirectory and provide a wrapper script for `bundle`
+that sets the `BUNDLE_GEMFILE` environment variable. We do not yet have enough
+experience with this approach to make a firm recommendation for or against it.
+
+This is the best design we could find to prevent surprises and adequately
+support normal development processes while minimizing the additional maintenance
+cost of the version compatibility mechanism. We want to have a scheme in place
+that is robust enough to make it reasonable to encourage serious adoption of
+Braid, yet we don't want to spend extra work adding conveniences until there's
+evidence of sufficient demand for them.
+
## Contributing
We appreciate any patches, error reports and usage ideas you may have. Please
diff --git a/bin/braid b/bin/braid
index 47e4867..2eec40b 100755
--- a/bin/braid
+++ b/bin/braid
@@ -64,7 +64,7 @@ Main {
run {
check_no_extra_args!
Braid.verbose = verbose
- Braid::Command.run(:add, url, {'path' => local_path, 'branch' => branch, 'tag' => tag, 'revision' => revision, 'remote_path' => path})
+ Braid::Command.run(:Add, url, {'path' => local_path, 'branch' => branch, 'tag' => tag, 'revision' => revision, 'remote_path' => path})
}
}
@@ -97,7 +97,7 @@ Main {
'keep' => keep
}
Braid.verbose = verbose
- Braid::Command.run(:update, local_path, options)
+ Braid::Command.run(:Update, local_path, options)
}
}
@@ -122,7 +122,7 @@ Main {
:keep => keep
}
Braid.verbose = verbose
- Braid::Command.run(:remove, local_path, options)
+ Braid::Command.run(:Remove, local_path, options)
}
}
@@ -149,7 +149,7 @@ Main {
'git_diff_args' => @argv
}
Braid.verbose = verbose
- Braid::Command.run(:diff, local_path, options)
+ Braid::Command.run(:Diff, local_path, options)
}
}
@@ -167,7 +167,7 @@ Main {
'branch' => branch
}
Braid.verbose = verbose
- Braid::Command.run(:push, local_path, options)
+ Braid::Command.run(:Push, local_path, options)
}
}
@@ -182,7 +182,7 @@ Main {
check_no_extra_args!
Braid.verbose = verbose
Braid.force = force
- Braid::Command.run(:setup, local_path)
+ Braid::Command.run(:Setup, local_path)
}
}
@@ -203,7 +203,44 @@ Main {
run {
check_no_extra_args!
Braid.verbose = verbose
- Braid::Command.run(:status, local_path)
+ Braid::Command.run(:Status, local_path)
+ }
+ }
+
+ mode("upgrade-config") {
+ description <<-DESC
+ Upgrade your project's Braid configuration to the latest configuration version.
+ Other commands will notify you when you need to do this. This may make older
+ versions of Braid unable to read the configuration and may introduce breaking
+ changes in how the configuration is handled by Braid. An upgrade that
+ introduces breaking changes will not be performed without the
+ --allow-breaking-changes option.
+ DESC
+
+ mixin :option_verbose
+
+ option("dry-run") {
+ optional
+ desc 'Explain the consequences of the upgrade without performing it.'
+ attr :dry_run
+ }
+
+ option("allow-breaking-changes") {
+ optional
+ desc <<-DESC
+ Perform the upgrade even if it involves breaking changes.
+ DESC
+ attr :allow_breaking_changes
+ }
+
+ run {
+ check_no_extra_args!
+ options = {
+ 'dry_run' => dry_run,
+ 'allow_breaking_changes' => allow_breaking_changes
+ }
+ Braid.verbose = verbose
+ Braid::Command.run(:UpgradeConfig, options)
}
}
diff --git a/config_versions.md b/config_versions.md
new file mode 100644
index 0000000..335ff00
--- /dev/null
+++ b/config_versions.md
@@ -0,0 +1,58 @@
+# Braid configuration version history
+
+The Braid configuration file (`.braids.json`) contains a configuration version
+number that indicates the format of the configuration file and the Braid
+features required by the project. You'll be directed to this page if you use a
+version of Braid that does not support the project's configuration version; see
+[the readme](README.md#braid-version-compatibility) for more information about
+the versioning scheme.
+
+To get a compatible version of Braid:
+
+1. First check if the project has its own instructions to install and run Braid,
+ and if so, follow them instead.
+2. Look up the Braid versions corresponding to your current configuration
+ version in the table below.
+3. Run `gem query --remote --all --exact braid` to get a list of all existing
+ versions of Braid, and choose one that is compatible with your configuration
+ version (you probably want the newest such version); call it `x.y.z`.
+4. Run `gem install braid --version x.y.z` to install the chosen version of
+ Braid.
+5. Run Braid as `braid _x.y.z_` (that's the chosen version surrounded by literal
+ underscores) followed by your desired arguments.
+
+
+
+
Config. version
+
Braid versions
+
Changes since previous
+
+
+
1
+
1.1.x
+
(Various)
+
+
+
"0"
+
+(Braid versions earlier than 1.1.0 have varying configuration formats and
+features and do not have a well-defined compatibility scheme. Braid 1.1.0 and
+newer refer to all of these formats as version "0" and are capable of correctly
+upgrading most of them. We recommend upgrading to Braid 1.1.0 or newer if you
+can.)
+
+
+
+
+
diff --git a/lib/braid.rb b/lib/braid.rb
index 4dbfa0e..6a126a2 100644
--- a/lib/braid.rb
+++ b/lib/braid.rb
@@ -35,6 +35,12 @@ def message
value if value != self.class.name
end
end
+
+ class InternalError < BraidError
+ def message
+ "internal error: #{super}"
+ end
+ end
end
require 'braid/operations_lite'
@@ -49,3 +55,4 @@ def message
require 'braid/commands/setup'
require 'braid/commands/update'
require 'braid/commands/status'
+require 'braid/commands/upgrade_config'
diff --git a/lib/braid/command.rb b/lib/braid/command.rb
index 3de7a42..0ca2e76 100644
--- a/lib/braid/command.rb
+++ b/lib/braid/command.rb
@@ -10,7 +10,7 @@ def self.run(command, *args)
verify_git_version!
check_working_dir!
- klass = Commands.const_get(command.to_s.capitalize)
+ klass = Commands.const_get(command.to_s)
klass.new.run(*args)
rescue BraidError => error
@@ -32,7 +32,7 @@ def msg(str)
end
def config
- @config ||= Config.new
+ @config ||= Config.new({'mode' => config_mode})
end
def verbose?
@@ -45,11 +45,15 @@ def force?
private
+ def config_mode
+ Config::MODE_MAY_WRITE
+ end
+
def setup_remote(mirror)
existing_force = Braid.force
begin
Braid.force = true
- Command.run(:setup, mirror.path)
+ Command.run(:Setup, mirror.path)
ensure
Braid.force = existing_force
end
diff --git a/lib/braid/commands/diff.rb b/lib/braid/commands/diff.rb
index 62fec80..7794ecf 100644
--- a/lib/braid/commands/diff.rb
+++ b/lib/braid/commands/diff.rb
@@ -5,7 +5,7 @@ def run(path = nil, options = {})
path ? diff_one(path, options) : diff_all(options)
end
- protected
+ private
def diff_all(options = {})
# We don't want "git diff" to invoke the pager once for each mirror.
@@ -39,6 +39,10 @@ def show_diff(path, options = {})
clear_remote(mirror, options)
end
+
+ def config_mode
+ Config::MODE_READ_ONLY
+ end
end
end
end
diff --git a/lib/braid/commands/push.rb b/lib/braid/commands/push.rb
index 5b9cb96..b052104 100644
--- a/lib/braid/commands/push.rb
+++ b/lib/braid/commands/push.rb
@@ -80,5 +80,11 @@ def run(path, options = {})
clear_remote(mirror, options)
end
end
+
+ private
+
+ def config_mode
+ Config::MODE_READ_ONLY # Surprisingly enough.
+ end
end
end
diff --git a/lib/braid/commands/setup.rb b/lib/braid/commands/setup.rb
index b1a2c22..152f18a 100644
--- a/lib/braid/commands/setup.rb
+++ b/lib/braid/commands/setup.rb
@@ -5,7 +5,7 @@ def run(path = nil)
path ? setup_one(path) : setup_all
end
- protected
+ private
def setup_all
msg 'Setting up all mirrors.'
@@ -31,6 +31,10 @@ def setup_one(path)
url = use_local_cache? ? git_cache.path(mirror.url) : mirror.url
git.remote_add(mirror.remote, url)
end
+
+ def config_mode
+ Config::MODE_READ_ONLY
+ end
end
end
end
diff --git a/lib/braid/commands/status.rb b/lib/braid/commands/status.rb
index 478d56a..3ce94c4 100644
--- a/lib/braid/commands/status.rb
+++ b/lib/braid/commands/status.rb
@@ -5,7 +5,7 @@ def run(path = nil, options = {})
path ? status_one(path, options) : status_all(options)
end
- protected
+ private
def status_all(options = {})
print "\n"
@@ -41,6 +41,10 @@ def status_one(path, options = {})
print "\n"
clear_remote(mirror, options)
end
+
+ def config_mode
+ Config::MODE_READ_ONLY
+ end
end
end
end
diff --git a/lib/braid/commands/upgrade_config.rb b/lib/braid/commands/upgrade_config.rb
new file mode 100644
index 0000000..a95f697
--- /dev/null
+++ b/lib/braid/commands/upgrade_config.rb
@@ -0,0 +1,56 @@
+module Braid
+ module Commands
+ class UpgradeConfig < Command
+ def config_mode
+ Config::MODE_UPGRADE
+ end
+
+ def run(options)
+ # Config loading in MODE_UPGRADE will bail out only if the config
+ # version is too new.
+
+ if !config.config_existed
+ puts <<-MSG
+Your repository has no Braid configuration file. It will be created with the
+current configuration version when you add the first mirror.
+MSG
+ return
+ elsif config.config_version == Config::CURRENT_CONFIG_VERSION
+ puts <<-MSG
+Your configuration file is already at the current configuration version (#{Config::CURRENT_CONFIG_VERSION}).
+MSG
+ return
+ end
+
+ puts <<-MSG
+Your configuration file will be upgraded from configuration version #{config.config_version} to #{Config::CURRENT_CONFIG_VERSION}.
+Other developers on your project will need to use a Braid version compatible
+with configuration version #{Config::CURRENT_CONFIG_VERSION}; see
+https://cristibalan.github.io/braid/config_versions.html .
+
+MSG
+
+ unless config.breaking_change_descs.empty?
+ puts <<-MSG
+The following breaking changes will occur:
+#{config.breaking_change_descs.join('')}
+MSG
+ end
+
+ if options['dry_run']
+ puts <<-MSG
+Run 'braid upgrade-config#{config.breaking_change_descs.empty? ? '' : ' --allow-breaking-changes'}' to perform the upgrade.
+MSG
+ elsif !config.breaking_change_descs.empty? && !options['allow_breaking_changes']
+ raise BraidError, "You must pass --allow-breaking-changes to accept the breaking changes."
+ else
+ config.write_db
+ add_config_file
+ had_changes = git.commit("Upgrade configuration")
+ raise InternalError, "upgrade-config had no changes??" unless had_changes
+ msg "Configuration upgrade complete."
+ end
+ end
+ end
+ end
+end
diff --git a/lib/braid/config.rb b/lib/braid/config.rb
index 5271f2f..05387f6 100644
--- a/lib/braid/config.rb
+++ b/lib/braid/config.rb
@@ -2,8 +2,59 @@
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": {
+# : {
+# "url": ,
+# "path": ,
+# "branch": ,
+# "tag": ,
+# "revision":
+# }
+# }
+# }
+# ```
+#
+# 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}"
@@ -15,25 +66,88 @@ def message
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)
@@ -62,6 +176,7 @@ def get!(path)
def add(mirror)
raise PathAlreadyInUse, mirror.path if get(mirror.path)
write_mirror(mirror)
+ write_db
end
def remove(mirror)
@@ -71,31 +186,55 @@ def remove(mirror)
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
diff --git a/lib/braid/mirror.rb b/lib/braid/mirror.rb
index e233f8f..1616c54 100644
--- a/lib/braid/mirror.rb
+++ b/lib/braid/mirror.rb
@@ -1,6 +1,9 @@
module Braid
class Mirror
- ATTRIBUTES = %w(url branch revision tag path)
+ # Since Braid 1.1.0, the attributes are written to .braids.json in this
+ # canonical order. For now, the order is chosen to match what Braid 1.0.22
+ # produced for newly added mirrors.
+ ATTRIBUTES = %w(url branch path tag revision)
class UnknownType < BraidError
def message
@@ -22,9 +25,44 @@ def message
attr_reader :path, :attributes
- def initialize(path, attributes = {})
+ def initialize(path, attributes = {}, breaking_change_cb = DUMMY_BREAKING_CHANGE_CB)
@path = path.sub(/\/$/, '')
- @attributes = attributes
+ @attributes = attributes.dup
+
+ # Not that it's terribly important to check for such an old feature. This
+ # is mainly to demonstrate the RemoveMirrorDueToBreakingChange mechanism
+ # in case we want to use it for something else in the future.
+ if !@attributes['type'].nil? && @attributes['type'] != 'git'
+ breaking_change_cb.call <<-DESC
+- Mirror '#{path}' is of a Subversion repository, which is no
+ longer supported. The mirror will be removed from your configuration, leaving
+ the data in the tree.
+DESC
+ raise Config::RemoveMirrorDueToBreakingChange
+ end
+ @attributes.delete('type')
+
+ # Migrate revision locks from Braid < 1.0.18. We no longer store the
+ # original branch or tag (the user has to specify it again when
+ # unlocking); we simply represent a locked revision by the absence of a
+ # branch or tag.
+ if @attributes['lock']
+ @attributes.delete('lock')
+ @attributes['branch'] = nil
+ @attributes['tag'] = nil
+ end
+
+ # Removal of support for full-history mirrors from Braid < 1.0.17 is a
+ # breaking change for users who wanted to use the imported history in some
+ # way.
+ if !@attributes['squashed'].nil? && @attributes['squashed'] != true
+ breaking_change_cb.call <<-DESC
+- Mirror '#{path}' is full-history, which is no longer supported.
+ It will be changed to squashed. Upstream history already imported will remain
+ in your project's history and will have no effect on Braid.
+DESC
+ end
+ @attributes.delete('squashed')
end
def self.new_from_options(url, options = {})
@@ -183,6 +221,11 @@ def remote
private
+ DUMMY_BREAKING_CHANGE_CB = lambda { |desc|
+ raise InternalError, "Instantiated a mirror using an unsupported " +
+ "feature outside of configuration loading."
+ }
+
def method_missing(name, *args)
if ATTRIBUTES.find { |attribute| name.to_s =~ /^(#{attribute})(=)?$/ }
if $2
diff --git a/lib/braid/version.rb b/lib/braid/version.rb
index 7bfcf1c..92a56e8 100644
--- a/lib/braid/version.rb
+++ b/lib/braid/version.rb
@@ -1,3 +1,3 @@
module Braid
- VERSION = '1.0.22'.freeze
+ VERSION = '1.1.0'.freeze
end
diff --git a/spec/config_spec.rb b/spec/config_spec.rb
index aa75eb2..1f50681 100644
--- a/spec/config_spec.rb
+++ b/spec/config_spec.rb
@@ -2,7 +2,7 @@
describe 'Braid::Config, when empty' do
before(:each) do
- @config = Braid::Config.new('tmp.yml')
+ @config = Braid::Config.new({'config_file' => 'tmp.yml'})
end
after(:each) do
@@ -23,7 +23,7 @@
describe 'Braid::Config, with one mirror' do
before(:each) do
- @config = Braid::Config.new('tmp.yml')
+ @config = Braid::Config.new({'config_file' => 'tmp.yml'})
@mirror = build_mirror
@config.add(@mirror)
end
diff --git a/spec/fixtures/shiny-conf-1.0.9-lock/.braids.json b/spec/fixtures/shiny-conf-1.0.9-lock/.braids.json
new file mode 100644
index 0000000..f11fdb5
--- /dev/null
+++ b/spec/fixtures/shiny-conf-1.0.9-lock/.braids.json
@@ -0,0 +1,10 @@
+{
+ "skit1": {
+ "url": "file:///path/to/braid/spec/fixtures/skit1",
+ "remote": "master/braid/skit1",
+ "branch": "master",
+ "squashed": true,
+ "revision": "6d3aeac08f9f4f9689d367fc771f5f1c90496176",
+ "lock": "6d3aeac08f9f4f9689d367fc771f5f1c90496176"
+ }
+}
diff --git a/spec/fixtures/shiny-conf-1.0.9-lock/expected.braids.json b/spec/fixtures/shiny-conf-1.0.9-lock/expected.braids.json
new file mode 100644
index 0000000..51813ef
--- /dev/null
+++ b/spec/fixtures/shiny-conf-1.0.9-lock/expected.braids.json
@@ -0,0 +1,9 @@
+{
+ "config_version": 1,
+ "mirrors": {
+ "skit1": {
+ "url": "file:///path/to/braid/spec/fixtures/skit1",
+ "revision": "6d3aeac08f9f4f9689d367fc771f5f1c90496176"
+ }
+ }
+}
diff --git a/spec/fixtures/shiny-conf-1.0.9-lock/skit1/layouts/layout.liquid b/spec/fixtures/shiny-conf-1.0.9-lock/skit1/layouts/layout.liquid
new file mode 100644
index 0000000..9f75009
--- /dev/null
+++ b/spec/fixtures/shiny-conf-1.0.9-lock/skit1/layouts/layout.liquid
@@ -0,0 +1,219 @@
+
+
+{{ '/feed/atom.xml' | assign_to: 'global_feed' }}
+{{ '/feed/all_comments.xml' | assign_to: 'global_comments_feed' }}
+
+
+
+ {{ site.title }}
+ {% if site.current_section %}
+ - {{ site.current_section.name }}
+ {% endif %}
+ {% if article %}
+ - {{ article.title }}
+ {% endif %}
+
+
+
+{{ 'base' | stylesheet }}
+{{ 'app' | javascript }}
+
+
+
+
+
+
+
This would be nicer if we could get the number of articles for each tag.
+
tags:
+
+ {% for tag in site.tags %}
+
{{ tag | link_to_tag }}
+ {% endfor %}
+
+
+
+{% endunless %}
+
+
boxy tall
+
When using a tall box, make sure it's got plenty of content or that it's immediately followed by a short boxy. It might look a bit chopped off otherwise.
+
+
+
+
diff --git a/spec/fixtures/shiny-conf-1.0.9-lock/skit1/preview.png b/spec/fixtures/shiny-conf-1.0.9-lock/skit1/preview.png
new file mode 100644
index 0000000..f7d1a3e
Binary files /dev/null and b/spec/fixtures/shiny-conf-1.0.9-lock/skit1/preview.png differ
diff --git a/spec/fixtures/shiny-conf-breaking-changes/.braids b/spec/fixtures/shiny-conf-breaking-changes/.braids
new file mode 100644
index 0000000..935a8a1
--- /dev/null
+++ b/spec/fixtures/shiny-conf-breaking-changes/.braids
@@ -0,0 +1,14 @@
+--- !ruby/object:Hash
+Spoon-Knife: !ruby/object:Hash
+ remote: master/braid/Spoon-Knife
+ revision: 18
+ squashed: true
+ type: svn
+ url: https://github.com/octocat/Spoon-Knife.git/trunk
+skit1: !ruby/object:Hash
+ branch: master
+ remote: master/braid/skit1
+ revision: 6d3aeac08f9f4f9689d367fc771f5f1c90496176
+ squashed: false
+ type: git
+ url: file:///path/to/braid/spec/fixtures/skit1/.git
diff --git a/spec/fixtures/shiny-conf-breaking-changes/Spoon-Knife/README.md b/spec/fixtures/shiny-conf-breaking-changes/Spoon-Knife/README.md
new file mode 100644
index 0000000..f479026
--- /dev/null
+++ b/spec/fixtures/shiny-conf-breaking-changes/Spoon-Knife/README.md
@@ -0,0 +1,9 @@
+### Well hello there!
+
+This repository is meant to provide an example for *forking* a repository on GitHub.
+
+Creating a *fork* is producing a personal copy of someone else's project. Forks act as a sort of bridge between the original repository and your personal copy. You can submit *Pull Requests* to help make other people's projects better by offering your changes up to the original project. Forking is at the core of social coding at GitHub.
+
+After forking this repository, you can make some changes to the project, and submit [a Pull Request](https://github.com/octocat/Spoon-Knife/pulls) as practice.
+
+For some more information on how to fork a repository, [check out our guide, "Forking Projects""](http://guides.github.com/overviews/forking/). Thanks! :sparkling_heart:
diff --git a/spec/fixtures/shiny-conf-breaking-changes/Spoon-Knife/index.html b/spec/fixtures/shiny-conf-breaking-changes/Spoon-Knife/index.html
new file mode 100644
index 0000000..a83618b
--- /dev/null
+++ b/spec/fixtures/shiny-conf-breaking-changes/Spoon-Knife/index.html
@@ -0,0 +1,20 @@
+
+
+
+
+
+ Spoon-Knife
+
+
+
+
+
+
+
+
+
This would be nicer if we could get the number of articles for each tag.
+
tags:
+
+ {% for tag in site.tags %}
+
{{ tag | link_to_tag }}
+ {% endfor %}
+
+
+
+{% endunless %}
+
+
boxy tall
+
When using a tall box, make sure it's got plenty of content or that it's immediately followed by a short boxy. It might look a bit chopped off otherwise.
This would be nicer if we could get the number of articles for each tag.
+
tags:
+
+ {% for tag in site.tags %}
+
{{ tag | link_to_tag }}
+ {% endfor %}
+
+
+
+{% endunless %}
+
+
boxy tall
+
When using a tall box, make sure it's got plenty of content or that it's immediately followed by a short boxy. It might look a bit chopped off otherwise.
This would be nicer if we could get the number of articles for each tag.
+
tags:
+
+ {% for tag in site.tags %}
+
{{ tag | link_to_tag }}
+ {% endfor %}
+
+
+
+{% endunless %}
+
+
boxy tall
+
When using a tall box, make sure it's got plenty of content or that it's immediately followed by a short boxy. It might look a bit chopped off otherwise.
This would be nicer if we could get the number of articles for each tag.
+
tags:
+
+ {% for tag in site.tags %}
+
{{ tag | link_to_tag }}
+ {% endfor %}
+
+
+
+{% endunless %}
+
+
boxy tall
+
When using a tall box, make sure it's got plenty of content or that it's immediately followed by a short boxy. It might look a bit chopped off otherwise.
+
+
+
+
diff --git a/spec/fixtures/shiny-conf-yaml/skit1/preview.png b/spec/fixtures/shiny-conf-yaml/skit1/preview.png
new file mode 100644
index 0000000..f7d1a3e
Binary files /dev/null and b/spec/fixtures/shiny-conf-yaml/skit1/preview.png differ
diff --git a/spec/integration/adding_spec.rb b/spec/integration/adding_spec.rb
index 0b2d33c..e8ae1cc 100644
--- a/spec/integration/adding_spec.rb
+++ b/spec/integration/adding_spec.rb
@@ -29,11 +29,13 @@
it 'should create .braids.json and add the mirror to it' do
braids = YAML::load_file("#{@repository_dir}/.braids.json")
- expect(braids['skit1']['url']).to eq(@vendor_repository_dir)
- expect(braids['skit1']['revision']).not_to be_nil
- expect(braids['skit1']['branch']).to eq('master')
- expect(braids['skit1']['tag']).to be_nil
- expect(braids['skit1']['path']).to be_nil
+ expect(braids['config_version']).to be_kind_of(Numeric)
+ mirror_obj = braids['mirrors']['skit1']
+ expect(mirror_obj['url']).to eq(@vendor_repository_dir)
+ expect(mirror_obj['revision']).not_to be_nil
+ expect(mirror_obj['branch']).to eq('master')
+ expect(mirror_obj['tag']).to be_nil
+ expect(mirror_obj['path']).to be_nil
end
end
@@ -59,11 +61,13 @@
it 'should create .braids.json and add the mirror to it' do
braids = YAML::load_file("#{@repository_dir}/.braids.json")
- expect(braids['skit-layouts']['url']).to eq(@vendor_repository_dir)
- expect(braids['skit-layouts']['revision']).not_to be_nil
- expect(braids['skit-layouts']['branch']).to eq('master')
- expect(braids['skit-layouts']['tag']).to be_nil
- expect(braids['skit-layouts']['path']).to eq('layouts')
+ expect(braids['config_version']).to be_kind_of(Numeric)
+ mirror_obj = braids['mirrors']['skit-layouts']
+ expect(mirror_obj['url']).to eq(@vendor_repository_dir)
+ expect(mirror_obj['revision']).not_to be_nil
+ expect(mirror_obj['branch']).to eq('master')
+ expect(mirror_obj['tag']).to be_nil
+ expect(mirror_obj['path']).to eq('layouts')
end
end
@@ -89,11 +93,13 @@
it 'should create .braids.json and add the mirror to it' do
braids = YAML::load_file("#{@repository_dir}/.braids.json")
- expect(braids['skit-layout.liquid']['url']).to eq(@vendor_repository_dir)
- expect(braids['skit-layout.liquid']['revision']).not_to be_nil
- expect(braids['skit-layout.liquid']['branch']).to eq('master')
- expect(braids['skit-layout.liquid']['tag']).to be_nil
- expect(braids['skit-layout.liquid']['path']).to eq('layouts/layout.liquid')
+ expect(braids['config_version']).to be_kind_of(Numeric)
+ mirror_obj = braids['mirrors']['skit-layout.liquid']
+ expect(mirror_obj['url']).to eq(@vendor_repository_dir)
+ expect(mirror_obj['revision']).not_to be_nil
+ expect(mirror_obj['branch']).to eq('master')
+ expect(mirror_obj['tag']).to be_nil
+ expect(mirror_obj['path']).to eq('layouts/layout.liquid')
end
end
@@ -122,11 +128,13 @@
it 'should create .braids.json and add the mirror to it' do
braids = YAML::load_file("#{@repository_dir}/.braids.json")
- expect(braids['skit1']['url']).to eq(@vendor_repository_dir)
- expect(braids['skit1']['revision']).not_to be_nil
- expect(braids['skit1']['branch']).to be_nil
- expect(braids['skit1']['tag']).to eq('v1')
- expect(braids['skit1']['path']).to be_nil
+ expect(braids['config_version']).to be_kind_of(Numeric)
+ mirror_obj = braids['mirrors']['skit1']
+ expect(mirror_obj['url']).to eq(@vendor_repository_dir)
+ expect(mirror_obj['revision']).not_to be_nil
+ expect(mirror_obj['branch']).to be_nil
+ expect(mirror_obj['tag']).to eq('v1')
+ expect(mirror_obj['path']).to be_nil
end
end
@@ -156,11 +164,13 @@
it 'should create .braids.json and add the mirror to it' do
braids = YAML::load_file("#{@repository_dir}/.braids.json")
- expect(braids['skit1']['url']).to eq(@vendor_repository_dir)
- expect(braids['skit1']['revision']).not_to be_nil
- expect(braids['skit1']['branch']).to be_nil
- expect(braids['skit1']['tag']).to be_nil
- expect(braids['skit1']['path']).to be_nil
+ expect(braids['config_version']).to be_kind_of(Numeric)
+ mirror_obj = braids['mirrors']['skit1']
+ expect(mirror_obj['url']).to eq(@vendor_repository_dir)
+ expect(mirror_obj['revision']).not_to be_nil
+ expect(mirror_obj['branch']).to be_nil
+ expect(mirror_obj['tag']).to be_nil
+ expect(mirror_obj['path']).to be_nil
end
end
diff --git a/spec/integration/config_versioning_spec.rb b/spec/integration/config_versioning_spec.rb
new file mode 100644
index 0000000..82e03c4
--- /dev/null
+++ b/spec/integration/config_versioning_spec.rb
@@ -0,0 +1,221 @@
+require File.dirname(__FILE__) + '/integration_helper'
+
+describe 'Config versioning:' do
+
+ before do
+ FileUtils.rm_rf(TMP_PATH)
+ FileUtils.mkdir_p(TMP_PATH)
+ end
+
+ # Workaround for Braid writing .braids.json with LF line endings on Windows,
+ # while the .braids.json files in the fixtures get converted to CRLF under Git
+ # for Windows recommended settings.
+ # https://github.com/cristibalan/braid/issues/77
+ def assert_no_diff_in_braids(file1, file2)
+ assert_no_diff(file1, file2, "--ignore-trailing-space")
+ end
+
+ describe 'read-only command' do
+
+ it "from future config version should fail" do
+ @repository_dir = create_git_repo_from_fixture('shiny-conf-future')
+
+ in_dir(@repository_dir) do
+ output = run_command_expect_failure("#{BRAID_BIN} diff skit1")
+ expect(output).to match(/is too old to understand/)
+ end
+ end
+
+ it "from old config version with no breaking changes should work" do
+ @repository_dir = create_git_repo_from_fixture('shiny-conf-yaml')
+ @vendor_repository_dir = create_git_repo_from_fixture('skit1')
+
+ vendor_revision = nil
+ in_dir(@vendor_repository_dir) do
+ vendor_revision = run_command("git rev-parse HEAD")
+ end
+
+ in_dir(@repository_dir) do
+ # For a real command to work, we have to substitute the URL and revision
+ # of the real vendor repository we created on this run of the test. The
+ # below looks marginally easier than using the real YAML parser.
+ braids_content = nil
+ File.open('.braids', 'rb') do |f|
+ braids_content = f.read
+ end
+ braids_content = braids_content.sub(/revision:.*$/, "revision: #{vendor_revision}")
+ braids_content = braids_content.sub(/url:.*$/, "url: file://#{@vendor_repository_dir}")
+ File.open('.braids', 'wb') do |f|
+ f.write braids_content
+ end
+
+ output = run_command("#{BRAID_BIN} diff skit1")
+ expect(output).to eq('') # no diff
+ end
+ end
+
+ it "from old config version with breaking changes should fail" do
+ @repository_dir = create_git_repo_from_fixture('shiny-conf-breaking-changes')
+
+ in_dir(@repository_dir) do
+ output = run_command_expect_failure("#{BRAID_BIN} diff skit1")
+ expect(output).to match(/no longer supports a feature/)
+ end
+ end
+
+ end
+
+ describe 'write command' do
+
+ it "from future config version should fail" do
+ @repository_dir = create_git_repo_from_fixture('shiny-conf-future')
+
+ in_dir(@repository_dir) do
+ output = run_command_expect_failure("#{BRAID_BIN} update skit1")
+ expect(output).to match(/is too old to understand/)
+ end
+ end
+
+ it "from old config version with no breaking changes should fail" do
+ @repository_dir = create_git_repo_from_fixture('shiny-conf-yaml')
+
+ in_dir(@repository_dir) do
+ output = run_command_expect_failure("#{BRAID_BIN} update skit1")
+ expect(output).to match(/force other developers on your project to upgrade Braid/)
+ end
+ end
+
+ it "from old config version with breaking changes should fail" do
+ @repository_dir = create_git_repo_from_fixture('shiny-conf-breaking-changes')
+
+ in_dir(@repository_dir) do
+ output = run_command_expect_failure("#{BRAID_BIN} update skit1")
+ expect(output).to match(/no longer supports a feature/)
+ end
+ end
+
+ end
+
+ describe '"braid upgrade-config"' do
+
+ it "from Braid 0.7.1 (.braids YAML) should produce the expected configuration" do
+ @repository_dir = create_git_repo_from_fixture('shiny-conf-yaml')
+
+ in_dir(@repository_dir) do
+ output = run_command("#{BRAID_BIN} upgrade-config")
+ # Check this on one of the test cases.
+ expect(output).to match(/Configuration upgrade complete\./)
+ expect(File.exists?(".braids")).to eq(false)
+ assert_no_diff_in_braids(".braids.json", "expected.braids.json")
+ end
+ end
+
+ it "from Braid 1.0.0 (.braids JSON) should produce the expected configuration" do
+ @repository_dir = create_git_repo_from_fixture('shiny-conf-json-old-name')
+
+ in_dir(@repository_dir) do
+ run_command("#{BRAID_BIN} upgrade-config")
+ expect(File.exists?(".braids")).to eq(false)
+ assert_no_diff_in_braids(".braids.json", "expected.braids.json")
+ end
+ end
+
+ it "from Braid 1.0.9 (.braids.json) with old-style lock should produce the expected configuration" do
+ @repository_dir = create_git_repo_from_fixture('shiny-conf-1.0.9-lock')
+
+ in_dir(@repository_dir) do
+ run_command("#{BRAID_BIN} upgrade-config")
+ assert_no_diff_in_braids(".braids.json", "expected.braids.json")
+ end
+ end
+
+ it "from Braid 1.0.9 (.braids.json) with old-style lock with --dry-run should print info without performing the upgrade" do
+ @repository_dir = create_git_repo_from_fixture('shiny-conf-1.0.9-lock')
+
+ in_dir(@repository_dir) do
+ output = run_command("#{BRAID_BIN} upgrade-config --dry-run")
+ expect(output).to match(/Your configuration file will be upgraded from configuration version 0 to 1\./)
+ expect(output).not_to match(/The following breaking changes/)
+ # Instructions should not include --allow-breaking-changes if it isn't necessary.
+ expect(output).to match(/Run 'braid upgrade-config'/)
+ assert_no_diff_in_braids(".braids.json", "#{FIXTURE_PATH}/shiny-conf-1.0.9-lock/.braids.json")
+ end
+ end
+
+ it "with breaking changes and --dry-run should print info without performing the upgrade" do
+ @repository_dir = create_git_repo_from_fixture('shiny-conf-breaking-changes')
+
+ in_dir(@repository_dir) do
+ output = run_command("#{BRAID_BIN} upgrade-config --dry-run")
+ expect(output).to match(/The following breaking changes/)
+ expect(output).to match(/Spoon-Knife.*Subversion/)
+ expect(output).to match(/skit1.*full-history/)
+ expect(output).to match(/Run 'braid upgrade-config --allow-breaking-changes'/)
+ assert_no_diff(".braids", "#{FIXTURE_PATH}/shiny-conf-breaking-changes/.braids")
+ expect(File.exists?(".braids.json")).to eq(false)
+ end
+ end
+
+ it "with breaking changes should fail" do
+ @repository_dir = create_git_repo_from_fixture('shiny-conf-breaking-changes')
+
+ in_dir(@repository_dir) do
+ output = run_command_expect_failure("#{BRAID_BIN} upgrade-config")
+ expect(output).to match(/The following breaking changes/)
+ expect(output).to match(/Spoon-Knife.*Subversion/)
+ expect(output).to match(/skit1.*full-history/)
+ expect(output).to match(/You must pass --allow-breaking-changes/)
+ # `braid upgrade-config` should not have changed any files.
+ assert_no_diff(".braids", "#{FIXTURE_PATH}/shiny-conf-breaking-changes/.braids")
+ expect(File.exists?(".braids.json")).to eq(false)
+ end
+ end
+
+ it "with breaking changes and --allow-breaking-changes should produce the expected configuration" do
+ @repository_dir = create_git_repo_from_fixture('shiny-conf-breaking-changes')
+
+ in_dir(@repository_dir) do
+ output = run_command("#{BRAID_BIN} upgrade-config --allow-breaking-changes")
+ expect(output).to match(/The following breaking changes/)
+ expect(output).to match(/Spoon-Knife.*Subversion/)
+ expect(output).to match(/skit1.*full-history/)
+ expect(output).to match(/Configuration upgrade complete\./)
+ expect(File.exists?(".braids")).to eq(false)
+ assert_no_diff(".braids.json", "expected.braids.json")
+ end
+ end
+
+ it "from future config version should fail" do
+ @repository_dir = create_git_repo_from_fixture('shiny-conf-future')
+
+ in_dir(@repository_dir) do
+ output = run_command_expect_failure("#{BRAID_BIN} upgrade-config")
+ expect(output).to match(/is too old to understand/)
+ end
+ end
+
+ it "from current config version should do nothing and print expected message" do
+ # Generate a current-version configuration by adding a mirror.
+ @repository_dir = create_git_repo_from_fixture('shiny')
+ @vendor_repository_dir = create_git_repo_from_fixture('skit1')
+
+ in_dir(@repository_dir) do
+ run_command("#{BRAID_BIN} add #{@vendor_repository_dir}")
+ output = run_command("#{BRAID_BIN} upgrade-config")
+ expect(output).to match(/already at the current configuration version/)
+ end
+ end
+
+ it "with no Braid configuration should do nothing and print expected message" do
+ @repository_dir = create_git_repo_from_fixture('shiny')
+
+ in_dir(@repository_dir) do
+ output = run_command("#{BRAID_BIN} upgrade-config")
+ expect(output).to match(/has no Braid configuration file/)
+ expect(File.exists?(".braids.json")).to eq(false)
+ end
+ end
+
+ end
+
+end
diff --git a/spec/integration/diff_spec.rb b/spec/integration/diff_spec.rb
index dda2fe6..db3085f 100644
--- a/spec/integration/diff_spec.rb
+++ b/spec/integration/diff_spec.rb
@@ -112,8 +112,7 @@
run_command("git rev-parse --verify --quiet #{base_revision}^{commit}")
run_command("git gc --quiet --prune=all")
# Make sure it's gone now so we know we're actually testing Braid's fetch behavior.
- `git rev-parse --verify --quiet #{base_revision}^{commit}`
- raise "'git gc' did not delete the base revision from the repository." if $?.success?
+ run_command_expect_failure("git rev-parse --verify --quiet #{base_revision}^{commit}")
diff = run_command("#{BRAID_BIN} diff skit1")
diff --git a/spec/integration/integration_helper.rb b/spec/integration/integration_helper.rb
index a8dd5d8..0625332 100644
--- a/spec/integration/integration_helper.rb
+++ b/spec/integration/integration_helper.rb
@@ -37,8 +37,8 @@ def with_editor_message(message = 'Make some changes')
end
end
-def assert_no_diff(file1, file2)
- run_command("diff -U 3 #{file1} #{file2}")
+def assert_no_diff(file1, file2, extra_flags = "")
+ run_command("diff -U 3 #{extra_flags} #{file1} #{file2}")
end
def assert_commit_attribute(format_key, value, commit_index = 0)
@@ -77,6 +77,12 @@ def run_command(command)
output
end
+def run_command_expect_failure(command)
+ output = `#{command}`
+ raise "Expected command to fail but it succeeded: #{command}\nOutput: #{output}" if $?.success?
+ output
+end
+
def update_dir_from_fixture(dir, fixture = dir)
to_dir = File.join(TMP_PATH, dir)
FileUtils.mkdir_p(to_dir)
@@ -95,7 +101,7 @@ def create_git_repo_from_fixture(fixture_name, options = {})
run_command("git config --local user.email \"#{email}\"")
run_command("git config --local user.name \"#{name}\"")
run_command('git config --local commit.gpgsign false')
- run_command('git add *')
+ run_command('git add .')
run_command("git commit -m \"initial commit of #{fixture_name}\"")
end