Skip to content

Commit

Permalink
Implement configuration versioning to defend against Braid version skew.
Browse files Browse the repository at this point in the history
Bump version to 1.1.0 according to the new policy that each
configuration version corresponds to a different Braid minor version.

Fixes #66.

Now that we have the infrastructure:
- Report loss of support for full-history mirrors as a breaking change.
  Fixes #56.
- Correctly upgrade revision locks from Braid < 1.0.18.  Fixes #65.
  • Loading branch information
mattmccutchen committed Feb 18, 2018
1 parent 5d7d208 commit 7fc8316
Show file tree
Hide file tree
Showing 40 changed files with 1,953 additions and 76 deletions.
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 44 additions & 7 deletions bin/braid
Original file line number Diff line number Diff line change
Expand Up @@ -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})
}
}

Expand Down Expand Up @@ -97,7 +97,7 @@ Main {
'keep' => keep
}
Braid.verbose = verbose
Braid::Command.run(:update, local_path, options)
Braid::Command.run(:Update, local_path, options)
}
}

Expand All @@ -122,7 +122,7 @@ Main {
:keep => keep
}
Braid.verbose = verbose
Braid::Command.run(:remove, local_path, options)
Braid::Command.run(:Remove, local_path, options)
}
}

Expand All @@ -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)
}
}

Expand All @@ -167,7 +167,7 @@ Main {
'branch' => branch
}
Braid.verbose = verbose
Braid::Command.run(:push, local_path, options)
Braid::Command.run(:Push, local_path, options)
}
}

Expand All @@ -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)
}
}

Expand All @@ -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)
}
}

Expand Down
58 changes: 58 additions & 0 deletions config_versions.md
Original file line number Diff line number Diff line change
@@ -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.

<table border="border">
<tr>
<th>Config. version</th>
<th>Braid versions</th>
<th>Changes since previous</th>
</tr>
<tr>
<td>1</td>
<td>1.1.x</td>
<td>(Various)</td>
</tr>
<tr>
<td>"0"</td>
<td colspan="2">
(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.)
</td>
</tr>
</table>

<style>
header, section#downloads, .inner > hr {
display: none;
}
.inner {
padding-top: 35px; /* same as header when it is visible */
}
th, td {
border: 1px solid #6d6d6d;
padding: 2px;
}
</style>
7 changes: 7 additions & 0 deletions lib/braid.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -49,3 +55,4 @@ def message
require 'braid/commands/setup'
require 'braid/commands/update'
require 'braid/commands/status'
require 'braid/commands/upgrade_config'
10 changes: 7 additions & 3 deletions lib/braid/command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -32,7 +32,7 @@ def msg(str)
end

def config
@config ||= Config.new
@config ||= Config.new({'mode' => config_mode})
end

def verbose?
Expand All @@ -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
Expand Down
6 changes: 5 additions & 1 deletion lib/braid/commands/diff.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
6 changes: 6 additions & 0 deletions lib/braid/commands/push.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 5 additions & 1 deletion lib/braid/commands/setup.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.'
Expand All @@ -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
6 changes: 5 additions & 1 deletion lib/braid/commands/status.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
56 changes: 56 additions & 0 deletions lib/braid/commands/upgrade_config.rb
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 7fc8316

Please sign in to comment.