braid 1.0.22 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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