braid 1.0.22 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +0 -2
  3. data/README.md +48 -0
  4. data/_config.yml +1 -0
  5. data/bin/braid +44 -7
  6. data/braid.gemspec +4 -0
  7. data/config_versions.md +58 -0
  8. data/lib/braid.rb +7 -0
  9. data/lib/braid/command.rb +7 -7
  10. data/lib/braid/commands/add.rb +3 -3
  11. data/lib/braid/commands/diff.rb +7 -14
  12. data/lib/braid/commands/push.rb +10 -4
  13. data/lib/braid/commands/setup.rb +5 -1
  14. data/lib/braid/commands/status.rb +5 -1
  15. data/lib/braid/commands/update.rb +6 -15
  16. data/lib/braid/commands/upgrade_config.rb +56 -0
  17. data/lib/braid/config.rb +166 -27
  18. data/lib/braid/mirror.rb +111 -11
  19. data/lib/braid/operations.rb +51 -35
  20. data/lib/braid/version.rb +1 -1
  21. data/spec/config_spec.rb +2 -2
  22. data/spec/fixtures/shiny-conf-1.0.9-lock/.braids.json +10 -0
  23. data/spec/fixtures/shiny-conf-1.0.9-lock/expected.braids.json +9 -0
  24. data/spec/fixtures/shiny-conf-1.0.9-lock/skit1/layouts/layout.liquid +219 -0
  25. data/spec/fixtures/shiny-conf-1.0.9-lock/skit1/preview.png +0 -0
  26. data/spec/fixtures/shiny-conf-breaking-changes/.braids +14 -0
  27. data/spec/fixtures/shiny-conf-breaking-changes/Spoon-Knife/README.md +9 -0
  28. data/spec/fixtures/shiny-conf-breaking-changes/Spoon-Knife/index.html +20 -0
  29. data/spec/fixtures/shiny-conf-breaking-changes/Spoon-Knife/styles.css +17 -0
  30. data/spec/fixtures/shiny-conf-breaking-changes/expected.braids.json +10 -0
  31. data/spec/fixtures/shiny-conf-breaking-changes/skit1/layouts/layout.liquid +219 -0
  32. data/spec/fixtures/shiny-conf-breaking-changes/skit1/preview.png +0 -0
  33. data/spec/fixtures/shiny-conf-future/.braids.json +10 -0
  34. data/spec/fixtures/shiny-conf-future/skit1/layouts/layout.liquid +219 -0
  35. data/spec/fixtures/shiny-conf-future/skit1/preview.png +0 -0
  36. data/spec/fixtures/shiny-conf-json-old-name/.braids +9 -0
  37. data/spec/fixtures/shiny-conf-json-old-name/expected.braids.json +10 -0
  38. data/spec/fixtures/shiny-conf-json-old-name/skit1/layouts/layout.liquid +219 -0
  39. data/spec/fixtures/shiny-conf-json-old-name/skit1/preview.png +0 -0
  40. data/spec/fixtures/shiny-conf-yaml/.braids +8 -0
  41. data/spec/fixtures/shiny-conf-yaml/expected.braids.json +10 -0
  42. data/spec/fixtures/shiny-conf-yaml/skit1/layouts/layout.liquid +219 -0
  43. data/spec/fixtures/shiny-conf-yaml/skit1/preview.png +0 -0
  44. data/spec/fixtures/shiny/skit-layout.liquid.test +2 -0
  45. data/spec/fixtures/shiny/skit1.test +2 -0
  46. data/spec/fixtures/skit1.1x/layouts/layout.liquid +219 -0
  47. data/spec/integration/adding_spec.rb +82 -34
  48. data/spec/integration/config_versioning_spec.rb +222 -0
  49. data/spec/integration/diff_spec.rb +173 -9
  50. data/spec/integration/integration_helper.rb +23 -5
  51. data/spec/integration/push_spec.rb +57 -4
  52. data/spec/integration/remove_spec.rb +27 -5
  53. data/spec/integration/updating_spec.rb +104 -19
  54. metadata +73 -2
@@ -56,8 +56,6 @@ module Braid
56
56
  rescue InvalidRevision
57
57
  # Ignored as it means the revision matches expected
58
58
  end
59
- target_revision = determine_target_revision(mirror, new_revision)
60
- current_revision = determine_target_revision(mirror, mirror.base_revision)
61
59
 
62
60
  from_desc =
63
61
  original_tag ? "tag '#{original_tag}'" :
@@ -77,7 +75,7 @@ module Braid
77
75
 
78
76
  if !switching &&
79
77
  (
80
- (options['revision'] && was_locked && target_revision == current_revision) ||
78
+ (options['revision'] && was_locked && new_revision == mirror.base_revision) ||
81
79
  (options['revision'].nil? && !was_locked && mirror.merged?(git.rev_parse(new_revision)))
82
80
  )
83
81
  msg "Mirror '#{mirror.path}' is already up to date."
@@ -93,11 +91,13 @@ module Braid
93
91
  in_error = false
94
92
  begin
95
93
  local_hash = git.rev_parse('HEAD')
96
- base_hash = git.make_tree_with_subtree('HEAD', mirror.path, mirror.versioned_path(base_revision))
97
- remote_hash = git.make_tree_with_subtree('HEAD', mirror.path, target_revision)
94
+ base_hash = git.make_tree_with_item('HEAD', mirror.path,
95
+ mirror.upstream_item_for_revision(base_revision))
96
+ remote_hash = git.make_tree_with_item('HEAD', mirror.path,
97
+ mirror.upstream_item_for_revision(new_revision))
98
98
  Operations::with_modified_environment({
99
99
  "GITHEAD_#{local_hash}" => 'HEAD',
100
- "GITHEAD_#{remote_hash}" => target_revision
100
+ "GITHEAD_#{remote_hash}" => new_revision
101
101
  }) do
102
102
  git.merge_trees(base_hash, local_hash, remote_hash)
103
103
  end
@@ -121,15 +121,6 @@ module Braid
121
121
  msg "Updated mirror to #{display_revision(mirror)}."
122
122
  clear_remote(mirror, options)
123
123
  end
124
-
125
- def generate_tree_hash(mirror, revision)
126
- git.with_temporary_index do
127
- git.read_tree_im('HEAD')
128
- git.rm_r_cached(mirror.path)
129
- git.read_tree_prefix_i(revision, mirror.path)
130
- git.write_tree
131
- end
132
- end
133
124
  end
134
125
  end
135
126
  end
@@ -0,0 +1,56 @@
1
+ module Braid
2
+ module Commands
3
+ class UpgradeConfig < Command
4
+ def config_mode
5
+ Config::MODE_UPGRADE
6
+ end
7
+
8
+ def run(options)
9
+ # Config loading in MODE_UPGRADE will bail out only if the config
10
+ # version is too new.
11
+
12
+ if !config.config_existed
13
+ puts <<-MSG
14
+ Your repository has no Braid configuration file. It will be created with the
15
+ current configuration version when you add the first mirror.
16
+ MSG
17
+ return
18
+ elsif config.config_version == Config::CURRENT_CONFIG_VERSION
19
+ puts <<-MSG
20
+ Your configuration file is already at the current configuration version (#{Config::CURRENT_CONFIG_VERSION}).
21
+ MSG
22
+ return
23
+ end
24
+
25
+ puts <<-MSG
26
+ Your configuration file will be upgraded from configuration version #{config.config_version} to #{Config::CURRENT_CONFIG_VERSION}.
27
+ Other developers on your project will need to use a Braid version compatible
28
+ with configuration version #{Config::CURRENT_CONFIG_VERSION}; see
29
+ https://cristibalan.github.io/braid/config_versions.html .
30
+
31
+ MSG
32
+
33
+ unless config.breaking_change_descs.empty?
34
+ puts <<-MSG
35
+ The following breaking changes will occur:
36
+ #{config.breaking_change_descs.join('')}
37
+ MSG
38
+ end
39
+
40
+ if options['dry_run']
41
+ puts <<-MSG
42
+ Run 'braid upgrade-config#{config.breaking_change_descs.empty? ? '' : ' --allow-breaking-changes'}' to perform the upgrade.
43
+ MSG
44
+ elsif !config.breaking_change_descs.empty? && !options['allow_breaking_changes']
45
+ raise BraidError, 'You must pass --allow-breaking-changes to accept the breaking changes.'
46
+ else
47
+ config.write_db
48
+ add_config_file
49
+ had_changes = git.commit('Upgrade configuration')
50
+ raise InternalError, 'upgrade-config had no changes??' unless had_changes
51
+ msg 'Configuration upgrade complete.'
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -2,8 +2,59 @@ require 'yaml'
2
2
  require 'json'
3
3
  require 'yaml/store'
4
4
 
5
+ # Some info about the configuration versioning design:
6
+ # https://github.com/cristibalan/braid/issues/66#issuecomment-354211311
7
+ #
8
+ # Current configuration format:
9
+ # ```
10
+ # {
11
+ # "config_version": 1,
12
+ # "mirrors": {
13
+ # <mirror path: string>: {
14
+ # "url": <upstream URL: string>,
15
+ # "path": <remote path: string>,
16
+ # "branch": <upstream branch: string>,
17
+ # "tag": <upstream tag: string>,
18
+ # "revision": <current upstream revision: string>
19
+ # }
20
+ # }
21
+ # }
22
+ # ```
23
+ #
24
+ # History of configuration formats understood by current Braid:
25
+ #
26
+ # - Braid 1.1.0, config_version 1:
27
+ # - "config_version" introduced; mirrors moved to "mirrors"
28
+ # - Single-file mirrors (f340b0c)
29
+ # - Braid 1.0.18:
30
+ # - Locked mirrors indicated by absence of "branch" and "tag" attributes, not
31
+ # presence of "lock" attribute (e6535aa)
32
+ # - Braid 1.0.17:
33
+ # - Support for full-history mirrors ("squashed": false) removed; "squashed"
34
+ # attribute no longer written (eb72030)
35
+ # - Braid 1.0.11:
36
+ # - "remote" attribute no longer written (f8fd088)
37
+ # - Braid 1.0.9:
38
+ # - .braids -> .braids.json (6806c61)
39
+ # - Braid 1.0.0:
40
+ # - YAML -> JSON (9d3fa11)
41
+ # - Support for Subversion mirrors removed ("type": "svn") removed (9d8d390)
42
+ #
43
+ #
44
+ # (Entries that predate the creation of this list have commit IDs for reference.
45
+ # Of course, when adding a new entry, you can't add the commit ID in the same
46
+ # commit, but you don't need to because people can just run `git log` on this
47
+ # file.)
48
+
5
49
  module Braid
6
50
  class Config
51
+
52
+ MODE_UPGRADE = 1
53
+ MODE_READ_ONLY = 2
54
+ MODE_MAY_WRITE = 3
55
+
56
+ CURRENT_CONFIG_VERSION = 1
57
+
7
58
  class PathAlreadyInUse < BraidError
8
59
  def message
9
60
  "path already in use: #{super}"
@@ -15,25 +66,88 @@ module Braid
15
66
  end
16
67
  end
17
68
 
18
- def initialize(config_file = CONFIG_FILE, old_config_files = [OLD_CONFIG_FILE])
19
- @config_file = config_file
20
- (old_config_files + [config_file]).each do |file|
21
- next unless File.exist?(file)
69
+ class RemoveMirrorDueToBreakingChange < StandardError
70
+ end
71
+
72
+ # For upgrade-config command only. XXX: Ideally would be immutable.
73
+ attr_reader :config_version, :config_existed, :breaking_change_descs
74
+
75
+ # options: config_file, old_config_files, mode
76
+ def initialize(options = {})
77
+ @config_file = options['config_file'] || CONFIG_FILE
78
+ old_config_files = options['old_config_files'] || [OLD_CONFIG_FILE]
79
+ @mode = options['mode'] || MODE_MAY_WRITE
80
+
81
+ data = load_config(@config_file, old_config_files)
82
+ @config_existed = !data.nil?
83
+ if !@config_existed
84
+ @config_version = CURRENT_CONFIG_VERSION
85
+ @db = {}
86
+ elsif data['config_version'].is_a?(Numeric)
87
+ @config_version = data['config_version']
88
+ @db = data['mirrors']
89
+ else
90
+ # Before config versioning (Braid < 1.1.0)
91
+ @config_version = 0
92
+ @db = data
93
+ end
94
+
95
+ if @config_version > CURRENT_CONFIG_VERSION
96
+ raise BraidError, <<-MSG
97
+ This version of Braid (#{VERSION}) is too old to understand your project's Braid
98
+ configuration file (version #{@config_version}). See the instructions at
99
+ https://cristibalan.github.io/braid/config_versions.html to install and use a
100
+ compatible newer version of Braid.
101
+ MSG
102
+ end
103
+
104
+ # In all modes, instantiate all mirrors to scan for breaking changes.
105
+ @breaking_change_descs = []
106
+ paths_to_delete = []
107
+ @db.each do |path, attributes|
22
108
  begin
23
- store = YAML::Store.new(file)
24
- @db = {}
25
- store.transaction(true) do
26
- store.roots.each do |path|
27
- @db[path] = store[path]
28
- end
29
- end
30
- return
31
- rescue
32
- @db = JSON.parse(file)
33
- return if @db
109
+ mirror = Mirror.new(path, attributes,
110
+ lambda {|desc| @breaking_change_descs.push(desc)})
111
+ # In MODE_UPGRADE, update @db now. In other modes, we won't write the
112
+ # config if an upgrade is needed, so it doesn't matter that we don't
113
+ # update @db.
114
+ #
115
+ # It's OK to change the values of existing keys during iteration:
116
+ # https://groups.google.com/d/msg/comp.lang.ruby/r5OI6UaxAAg/SVpU0cktmZEJ
117
+ write_mirror(mirror) if @mode == MODE_UPGRADE
118
+ rescue RemoveMirrorDueToBreakingChange
119
+ # I don't know if deleting during iteration is well-defined in all
120
+ # Ruby versions we support, so defer the deletion.
121
+ # ~ matt@mattmccutchen.net, 2017-12-31
122
+ paths_to_delete.push(path) if @mode == MODE_UPGRADE
34
123
  end
35
124
  end
36
- @db = {}
125
+ paths_to_delete.each do |path|
126
+ @db.delete(path)
127
+ end
128
+
129
+ if @mode != MODE_UPGRADE && !@breaking_change_descs.empty?
130
+ raise BraidError, <<-MSG
131
+ This version of Braid (#{VERSION}) no longer supports a feature used by your
132
+ Braid configuration file (version #{@config_version}). Run 'braid upgrade-config --dry-run'
133
+ for information about upgrading your configuration file, or see the instructions
134
+ at https://cristibalan.github.io/braid/config_versions.html to install and run a
135
+ compatible older version of Braid.
136
+ MSG
137
+ end
138
+
139
+ if @mode == MODE_MAY_WRITE && @config_version < CURRENT_CONFIG_VERSION
140
+ raise BraidError, <<-MSG
141
+ This command may need to write to your Braid configuration file,
142
+ but this version of Braid (#{VERSION}) cannot write to your configuration file
143
+ (currently version #{config_version}) without upgrading it to configuration version #{CURRENT_CONFIG_VERSION},
144
+ which would force other developers on your project to upgrade Braid. Run
145
+ 'braid upgrade-config' to proceed with the upgrade, or see the instructions at
146
+ https://cristibalan.github.io/braid/config_versions.html to install and run a
147
+ compatible older version of Braid.
148
+ MSG
149
+ end
150
+
37
151
  end
38
152
 
39
153
  def add_from_options(url, options)
@@ -62,6 +176,7 @@ module Braid
62
176
  def add(mirror)
63
177
  raise PathAlreadyInUse, mirror.path if get(mirror.path)
64
178
  write_mirror(mirror)
179
+ write_db
65
180
  end
66
181
 
67
182
  def remove(mirror)
@@ -71,31 +186,55 @@ module Braid
71
186
 
72
187
  def update(mirror)
73
188
  raise MirrorDoesNotExist, mirror.path unless get(mirror.path)
74
- @db.delete(mirror.path)
75
189
  write_mirror(mirror)
76
- end
77
-
78
- private
79
-
80
- def write_mirror(mirror)
81
- @db[mirror.path] = clean_attributes(mirror.attributes)
82
190
  write_db
83
191
  end
84
192
 
193
+ # Public for upgrade-config command only.
85
194
  def write_db
86
195
  new_db = {}
87
196
  @db.keys.sort.each do |key|
88
- new_db[key] = @db[key]
89
- new_db[key].keys.each do |k|
90
- new_db[key].delete(k) unless Braid::Mirror::ATTRIBUTES.include?(k)
197
+ new_db[key] = {}
198
+ Braid::Mirror::ATTRIBUTES.each do |k|
199
+ new_db[key][k] = @db[key][k] if @db[key].has_key?(k)
91
200
  end
92
201
  end
202
+ new_data = {
203
+ 'config_version' => CURRENT_CONFIG_VERSION,
204
+ 'mirrors' => new_db
205
+ }
93
206
  File.open(@config_file, 'wb') do |f|
94
- f.write JSON.pretty_generate(new_db)
207
+ f.write JSON.pretty_generate(new_data)
95
208
  f.write "\n"
96
209
  end
97
210
  end
98
211
 
212
+ private
213
+
214
+ def load_config(config_file, old_config_files)
215
+ (old_config_files + [config_file]).each do |file|
216
+ next unless File.exist?(file)
217
+ begin
218
+ store = YAML::Store.new(file)
219
+ data = {}
220
+ store.transaction(true) do
221
+ store.roots.each do |path|
222
+ data[path] = store[path]
223
+ end
224
+ end
225
+ return data
226
+ rescue
227
+ data = JSON.parse(file)
228
+ return data if data
229
+ end
230
+ end
231
+ return nil
232
+ end
233
+
234
+ def write_mirror(mirror)
235
+ @db[mirror.path] = clean_attributes(mirror.attributes)
236
+ end
237
+
99
238
  def clean_attributes(hash)
100
239
  hash.reject { |k, v| v.nil? }
101
240
  end
@@ -1,6 +1,9 @@
1
1
  module Braid
2
2
  class Mirror
3
- ATTRIBUTES = %w(url branch revision tag path)
3
+ # Since Braid 1.1.0, the attributes are written to .braids.json in this
4
+ # canonical order. For now, the order is chosen to match what Braid 1.0.22
5
+ # produced for newly added mirrors.
6
+ ATTRIBUTES = %w(url branch path tag revision)
4
7
 
5
8
  class UnknownType < BraidError
6
9
  def message
@@ -22,9 +25,44 @@ module Braid
22
25
 
23
26
  attr_reader :path, :attributes
24
27
 
25
- def initialize(path, attributes = {})
28
+ def initialize(path, attributes = {}, breaking_change_cb = DUMMY_BREAKING_CHANGE_CB)
26
29
  @path = path.sub(/\/$/, '')
27
- @attributes = attributes
30
+ @attributes = attributes.dup
31
+
32
+ # Not that it's terribly important to check for such an old feature. This
33
+ # is mainly to demonstrate the RemoveMirrorDueToBreakingChange mechanism
34
+ # in case we want to use it for something else in the future.
35
+ if !@attributes['type'].nil? && @attributes['type'] != 'git'
36
+ breaking_change_cb.call <<-DESC
37
+ - Mirror '#{path}' is of a Subversion repository, which is no
38
+ longer supported. The mirror will be removed from your configuration, leaving
39
+ the data in the tree.
40
+ DESC
41
+ raise Config::RemoveMirrorDueToBreakingChange
42
+ end
43
+ @attributes.delete('type')
44
+
45
+ # Migrate revision locks from Braid < 1.0.18. We no longer store the
46
+ # original branch or tag (the user has to specify it again when
47
+ # unlocking); we simply represent a locked revision by the absence of a
48
+ # branch or tag.
49
+ if @attributes['lock']
50
+ @attributes.delete('lock')
51
+ @attributes['branch'] = nil
52
+ @attributes['tag'] = nil
53
+ end
54
+
55
+ # Removal of support for full-history mirrors from Braid < 1.0.17 is a
56
+ # breaking change for users who wanted to use the imported history in some
57
+ # way.
58
+ if !@attributes['squashed'].nil? && @attributes['squashed'] != true
59
+ breaking_change_cb.call <<-DESC
60
+ - Mirror '#{path}' is full-history, which is no longer supported.
61
+ It will be changed to squashed. Upstream history already imported will remain
62
+ in your project's history and will have no effect on Braid.
63
+ DESC
64
+ end
65
+ @attributes.delete('squashed')
28
66
  end
29
67
 
30
68
  def self.new_from_options(url, options = {})
@@ -35,7 +73,7 @@ module Braid
35
73
  tag = options['tag']
36
74
  branch = options['branch'] || (tag.nil? ? 'master' : nil)
37
75
 
38
- path = (options['path'] || extract_path_from_url(url)).sub(/\/$/, '')
76
+ path = (options['path'] || extract_path_from_url(url, options['remote_path'])).sub(/\/$/, '')
39
77
  raise PathRequired unless path
40
78
 
41
79
  remote_path = options['remote_path']
@@ -59,15 +97,68 @@ module Braid
59
97
  !!base_revision && git.merge_base(commit, base_revision) == commit
60
98
  end
61
99
 
62
- def versioned_path(revision)
63
- "#{revision}:#{self.remote_path}"
100
+ def upstream_item_for_revision(revision)
101
+ git.get_tree_item(revision, self.remote_path)
64
102
  end
65
103
 
104
+ # Return the arguments that should be passed to "git diff" to diff this
105
+ # mirror (including uncommitted changes by default), incorporating the given
106
+ # user-specified arguments. Having the caller run "git diff" is convenient
107
+ # for now but violates encapsulation a little; we may have to reorganize the
108
+ # code in order to add features.
109
+ def diff_args(user_args = [])
110
+ upstream_item = upstream_item_for_revision(base_revision)
111
+
112
+ # We do not need to spend the time to copy the content outside the
113
+ # mirror from HEAD because --relative will exclude it anyway. Rename
114
+ # detection seems to apply only to the files included in the diff, so we
115
+ # shouldn't have another bug like
116
+ # https://github.com/cristibalan/braid/issues/41.
117
+ base_tree = git.make_tree_with_item(nil, path, upstream_item)
118
+
119
+ # Note: --relative does a naive prefix comparison. If we set (for
120
+ # example) `--relative=a/b`, that will match an unrelated file or
121
+ # directory name `a/bb`. If the mirror is a directory, we can avoid this
122
+ # by adding a trailing slash to the prefix.
123
+ #
124
+ # If the mirror is a file, the only way we can avoid matching a path like
125
+ # `a/bb` is to pass a path argument to limit the diff. This means if the
126
+ # user passes additional path arguments, we won't get the behavior we
127
+ # expect, which is the intersection of the user-specified paths with the
128
+ # mirror. However, it's probably unreasonable for a user to pass path
129
+ # arguments when diffing a single-file mirror, so we ignore the issue.
130
+ #
131
+ # Note: This code doesn't handle various cases in which a directory at the
132
+ # root of a mirror turns into a file or vice versa. If that happens,
133
+ # hopefully the user takes corrective action manually.
134
+ if upstream_item.is_a?(git.BlobWithMode)
135
+ # For a single-file mirror, we use the upstream basename for the
136
+ # upstream side of the diff and the downstream basename for the
137
+ # downstream side, like what `git diff` does when given two blobs as
138
+ # arguments. Use --relative to strip away the entire downstream path
139
+ # before we add the basenames.
140
+ return [
141
+ '--relative=' + path,
142
+ '--src-prefix=a/' + File.basename(remote_path),
143
+ '--dst-prefix=b/' + File.basename(path),
144
+ base_tree,
145
+ # user_args may contain options, which must come before paths.
146
+ *user_args,
147
+ path
148
+ ]
149
+ else
150
+ return [
151
+ '--relative=' + path + '/',
152
+ base_tree,
153
+ *user_args
154
+ ]
155
+ end
156
+ end
157
+
158
+ # Precondition: the remote for this mirror is set up.
66
159
  def diff
67
160
  fetch_base_revision_if_missing
68
- remote_hash = git.rev_parse(versioned_path(base_revision))
69
- local_hash = git.tree_hash(path)
70
- remote_hash != local_hash ? git.diff_tree(remote_hash, local_hash) : ''
161
+ git.diff(diff_args)
71
162
  end
72
163
 
73
164
  # Re-fetching the remote after deleting and re-adding it may be slow even if
@@ -79,7 +170,7 @@ module Braid
79
170
  begin
80
171
  # Without ^{commit}, this will happily pass back an object hash even if
81
172
  # the object isn't present. See the git-rev-parse(1) man page.
82
- git.rev_parse(base_revision + "^{commit}")
173
+ git.rev_parse(base_revision + '^{commit}')
83
174
  rescue Operations::UnknownRevision
84
175
  fetch
85
176
  end
@@ -130,6 +221,11 @@ module Braid
130
221
 
131
222
  private
132
223
 
224
+ DUMMY_BREAKING_CHANGE_CB = lambda { |desc|
225
+ raise InternalError, 'Instantiated a mirror using an unsupported ' +
226
+ 'feature outside of configuration loading.'
227
+ }
228
+
133
229
  def method_missing(name, *args)
134
230
  if ATTRIBUTES.find { |attribute| name.to_s =~ /^(#{attribute})(=)?$/ }
135
231
  if $2
@@ -159,7 +255,11 @@ module Braid
159
255
  hash
160
256
  end
161
257
 
162
- def self.extract_path_from_url(url)
258
+ def self.extract_path_from_url(url, remote_path)
259
+ if remote_path
260
+ return File.basename(remote_path)
261
+ end
262
+
163
263
  return nil unless url
164
264
  name = File.basename(url)
165
265