braid 1.1.9 → 1.1.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,12 +1,26 @@
1
- # typed: true
1
+ # typed: strict
2
2
  module Braid
3
3
  module Commands
4
4
  class UpgradeConfig < Command
5
+ class Options < T::Struct
6
+ prop :dry_run, T::Boolean
7
+ prop :allow_breaking_changes, T::Boolean
8
+ end
9
+
10
+ sig {params(options: Options).void}
11
+ def initialize(options)
12
+ @options = options
13
+ end
14
+
15
+ private
16
+
17
+ sig {returns(Config::ConfigMode)}
5
18
  def config_mode
6
19
  Config::MODE_UPGRADE
7
20
  end
8
21
 
9
- def run(options)
22
+ sig {void}
23
+ def run_internal
10
24
  # Config loading in MODE_UPGRADE will bail out only if the config
11
25
  # version is too new.
12
26
 
@@ -38,11 +52,11 @@ The following breaking changes will occur:
38
52
  MSG
39
53
  end
40
54
 
41
- if options['dry_run']
55
+ if @options.dry_run
42
56
  puts <<-MSG
43
57
  Run 'braid upgrade-config#{config.breaking_change_descs.empty? ? '' : ' --allow-breaking-changes'}' to perform the upgrade.
44
58
  MSG
45
- elsif !config.breaking_change_descs.empty? && !options['allow_breaking_changes']
59
+ elsif !config.breaking_change_descs.empty? && !@options.allow_breaking_changes
46
60
  raise BraidError, 'You must pass --allow-breaking-changes to accept the breaking changes.'
47
61
  else
48
62
  config.write_db
data/lib/braid/config.rb CHANGED
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: strict
2
2
  require 'yaml'
3
3
  require 'json'
4
4
  require 'yaml/store'
@@ -84,20 +84,29 @@ module Braid
84
84
  sig {returns(T::Array[String])}
85
85
  attr_reader :breaking_change_descs
86
86
 
87
- # options: config_file, old_config_files, mode
88
- sig {params(options: T.untyped).void}
89
- def initialize(options = {})
90
- @config_file = options['config_file'] || CONFIG_FILE
91
- old_config_files = options['old_config_files'] || [OLD_CONFIG_FILE]
92
- @mode = options['mode'] || MODE_MAY_WRITE
87
+ sig {params(config_file: String, old_config_files: T::Array[String], mode: ConfigMode).void}
88
+ def initialize(config_file: CONFIG_FILE, old_config_files: [OLD_CONFIG_FILE], mode: MODE_MAY_WRITE)
89
+ @config_file = config_file
90
+ old_config_files = old_config_files
91
+ @mode = mode
93
92
 
94
93
  data = load_config(@config_file, old_config_files)
95
- @config_existed = !data.nil?
94
+ @config_existed = T.let(!data.nil?, T::Boolean)
96
95
  if !@config_existed
97
- @config_version = CURRENT_CONFIG_VERSION
98
- @db = {}
96
+ @config_version = T.let(CURRENT_CONFIG_VERSION, Integer)
97
+ @db = T.let({}, T::Hash[String, Mirror::MirrorAttributes])
99
98
  elsif data['config_version'].is_a?(Numeric)
100
99
  @config_version = data['config_version']
100
+ # WARNING (typing): In general, slapping a type annotation on the loaded
101
+ # data without validating its structure could be misleading as we work
102
+ # on the rest of the code. In the worst case, it might lead us to
103
+ # delete useful sanity checks because they trigger Sorbet unreachable
104
+ # code errors. As of this writing (2024-08-04), we have nothing to lose
105
+ # because we don't attempt to defend against malformed data anyway, and
106
+ # there's a benefit to having the type information available. If we add
107
+ # any validation, consider whether the annotations should be changed to
108
+ # verify that we did the validation correctly, e.g., by starting with a
109
+ # type like `T.anything` instead of `T.untyped`.
101
110
  @db = data['mirrors']
102
111
  else
103
112
  # Before config versioning (Braid < 1.1.0)
@@ -115,7 +124,7 @@ MSG
115
124
  end
116
125
 
117
126
  # In all modes, instantiate all mirrors to scan for breaking changes.
118
- @breaking_change_descs = []
127
+ @breaking_change_descs = T.let([], T::Array[String])
119
128
  paths_to_delete = []
120
129
  @db.each do |path, attributes|
121
130
  begin
@@ -163,7 +172,7 @@ MSG
163
172
 
164
173
  end
165
174
 
166
- sig {params(url: String, options: T.untyped).returns(Mirror)}
175
+ sig {params(url: String, options: Mirror::Options).returns(Mirror)}
167
176
  def add_from_options(url, options)
168
177
  mirror = Mirror.new_from_options(url, options)
169
178
 
@@ -171,7 +180,7 @@ MSG
171
180
  mirror
172
181
  end
173
182
 
174
- sig {returns(T::Array[Mirror])}
183
+ sig {returns(T::Array[String])}
175
184
  def mirrors
176
185
  @db.keys
177
186
  end
@@ -213,11 +222,12 @@ MSG
213
222
  # Public for upgrade-config command only.
214
223
  sig {void}
215
224
  def write_db
216
- new_db = {}
225
+ new_db = T.let({}, T::Hash[String, Mirror::MirrorAttributes])
217
226
  @db.keys.sort.each do |key|
218
- new_db[key] = {}
227
+ attrs = T.must(@db[key])
228
+ new_db[key] = new_attrs = {}
219
229
  Braid::Mirror::ATTRIBUTES.each do |k|
220
- new_db[key][k] = @db[key][k] if @db[key].has_key?(k)
230
+ new_attrs[k] = attrs[k] if attrs.has_key?(k)
221
231
  end
222
232
  end
223
233
  new_data = {
@@ -237,7 +247,7 @@ MSG
237
247
  (old_config_files + [config_file]).each do |file|
238
248
  next unless File.exist?(file)
239
249
  begin
240
- store = T.let(YAML::Store, T.untyped).new(file)
250
+ store = T.unsafe(YAML::Store).new(file)
241
251
  data = {}
242
252
  store.transaction(true) do
243
253
  store.roots.each do |path|
@@ -258,7 +268,7 @@ MSG
258
268
  @db[mirror.path] = clean_attributes(mirror.attributes)
259
269
  end
260
270
 
261
- sig {params(hash: T::Hash[String, T.untyped]).returns(T::Hash[String, T.untyped])}
271
+ sig {params(hash: Mirror::MirrorAttributes).returns(Mirror::MirrorAttributes)}
262
272
  def clean_attributes(hash)
263
273
  hash.reject { |_, v| v.nil? }
264
274
  end
data/lib/braid/main.rb CHANGED
@@ -1,3 +1,11 @@
1
+ # `typed: strict` doesn't seem worth the trouble for this file at this time:
2
+ # there's not much for us to actually annotate, and it would take a crazy hack
3
+ # to avoid a Sorbet error on the `@argv` references because Sorbet doesn't seem
4
+ # to honor `T.bind` for instance variables (TODO: file a Sorbet bug about
5
+ # that?). Finding an approach to meaningfully check our code that uses the
6
+ # `main` DSL is a bigger project that we may or may not undertake later.
7
+ # ~ Matt 2024-08-04
8
+ #
1
9
  # typed: true
2
10
 
3
11
  require 'braid'
@@ -31,8 +39,8 @@ T.unsafe(Main).run {
31
39
  # The "main" library doesn't provide a way to do this??
32
40
  def check_no_extra_args!
33
41
  if @argv.length > 0
34
- Braid::Command.handle_error(
35
- Braid::BraidError.new('Extra argument(s) passed to command.'))
42
+ Command.handle_error(
43
+ BraidError.new('Extra argument(s) passed to command.'))
36
44
  end
37
45
  end
38
46
 
@@ -61,7 +69,7 @@ T.unsafe(Main).run {
61
69
  run {
62
70
  check_no_extra_args!
63
71
  Braid.verbose = verbose
64
- Braid::Command.run(:Add, url, {'path' => local_path, 'branch' => branch, 'tag' => tag, 'revision' => revision, 'remote_path' => path})
72
+ Commands::Add.new(url, Mirror::Options.new(path: local_path, branch: branch, tag: tag, revision: revision, remote_path: path)).run
65
73
  }
66
74
  }
67
75
 
@@ -86,15 +94,15 @@ T.unsafe(Main).run {
86
94
 
87
95
  run {
88
96
  check_no_extra_args!
89
- options = {
90
- 'branch' => branch,
91
- 'tag' => tag,
92
- 'revision' => revision,
93
- 'head' => head,
94
- 'keep' => keep
95
- }
97
+ options = Commands::Update::Options.new(
98
+ branch: branch,
99
+ tag: tag,
100
+ revision: revision,
101
+ head: head,
102
+ keep: keep
103
+ )
96
104
  Braid.verbose = verbose
97
- Braid::Command.run(:Update, local_path, options)
105
+ Commands::Update.new(local_path, options).run
98
106
  }
99
107
  }
100
108
 
@@ -115,11 +123,11 @@ T.unsafe(Main).run {
115
123
 
116
124
  run {
117
125
  check_no_extra_args!
118
- options = {
119
- :keep => keep
120
- }
126
+ options = Commands::Remove::Options.new(
127
+ keep: keep
128
+ )
121
129
  Braid.verbose = verbose
122
- Braid::Command.run(:Remove, local_path, options)
130
+ Commands::Remove.new(local_path, options).run
123
131
  }
124
132
  }
125
133
 
@@ -141,12 +149,12 @@ T.unsafe(Main).run {
141
149
  if @argv.length > 0 && @argv[0] == '--'
142
150
  @argv.shift
143
151
  end
144
- options = {
145
- 'keep' => keep,
146
- 'git_diff_args' => @argv
147
- }
152
+ options = Commands::Diff::Options.new(
153
+ keep: keep,
154
+ git_diff_args: @argv
155
+ )
148
156
  Braid.verbose = verbose
149
- Braid::Command.run(:Diff, local_path, options)
157
+ Commands::Diff.new(local_path, options).run
150
158
  }
151
159
  }
152
160
 
@@ -159,12 +167,12 @@ T.unsafe(Main).run {
159
167
 
160
168
  run {
161
169
  check_no_extra_args!
162
- options = {
163
- 'keep' => keep,
164
- 'branch' => branch
165
- }
170
+ options = Commands::Push::Options.new(
171
+ keep: keep,
172
+ branch: branch
173
+ )
166
174
  Braid.verbose = verbose
167
- Braid::Command.run(:Push, local_path, options)
175
+ Commands::Push.new(local_path, options).run
168
176
  }
169
177
  }
170
178
 
@@ -179,7 +187,7 @@ T.unsafe(Main).run {
179
187
  check_no_extra_args!
180
188
  Braid.verbose = verbose
181
189
  Braid.force = force
182
- Braid::Command.run(:Setup, local_path)
190
+ Commands::Setup.new(local_path).run
183
191
  }
184
192
  }
185
193
 
@@ -188,7 +196,7 @@ T.unsafe(Main).run {
188
196
 
189
197
  run {
190
198
  check_no_extra_args!
191
- puts "braid #{Braid::VERSION}"
199
+ puts "braid #{VERSION}"
192
200
  }
193
201
  }
194
202
 
@@ -200,7 +208,7 @@ T.unsafe(Main).run {
200
208
  run {
201
209
  check_no_extra_args!
202
210
  Braid.verbose = verbose
203
- Braid::Command.run(:Status, local_path)
211
+ Commands::Status.new(local_path).run
204
212
  }
205
213
  }
206
214
 
@@ -220,6 +228,7 @@ T.unsafe(Main).run {
220
228
  optional
221
229
  desc 'Explain the consequences of the upgrade without performing it.'
222
230
  attr :dry_run
231
+ default false
223
232
  }
224
233
 
225
234
  option('allow-breaking-changes') {
@@ -228,16 +237,17 @@ T.unsafe(Main).run {
228
237
  Perform the upgrade even if it involves breaking changes.
229
238
  DESC
230
239
  attr :allow_breaking_changes
240
+ default false
231
241
  }
232
242
 
233
243
  run {
234
244
  check_no_extra_args!
235
- options = {
236
- 'dry_run' => dry_run,
237
- 'allow_breaking_changes' => allow_breaking_changes
238
- }
245
+ options = Commands::UpgradeConfig::Options.new(
246
+ dry_run: dry_run,
247
+ allow_breaking_changes: allow_breaking_changes
248
+ )
239
249
  Braid.verbose = verbose
240
- Braid::Command.run(:UpgradeConfig, options)
250
+ Commands::UpgradeConfig.new(options).run
241
251
  }
242
252
  }
243
253
 
@@ -301,6 +311,7 @@ T.unsafe(Main).run {
301
311
  optional
302
312
  desc 'unused option'
303
313
  attr
314
+ default false
304
315
  }
305
316
  }
306
317
 
@@ -309,6 +320,7 @@ T.unsafe(Main).run {
309
320
  optional
310
321
  desc 'log shell commands'
311
322
  attr
323
+ default false
312
324
  }
313
325
  }
314
326
 
@@ -317,6 +329,7 @@ T.unsafe(Main).run {
317
329
  optional
318
330
  desc 'force'
319
331
  attr
332
+ default false
320
333
  }
321
334
  }
322
335
 
@@ -325,6 +338,7 @@ T.unsafe(Main).run {
325
338
  optional
326
339
  desc 'do not remove the remote'
327
340
  attr
341
+ default false
328
342
  }
329
343
  }
330
344
 
data/lib/braid/mirror.rb CHANGED
@@ -14,12 +14,6 @@ module Braid
14
14
  "unknown type: #{super}"
15
15
  end
16
16
  end
17
- class PathRequired < BraidError
18
- sig {returns(String)}
19
- def message
20
- 'path is required'
21
- end
22
- end
23
17
  class NoTagAndBranch < BraidError
24
18
  sig {returns(String)}
25
19
  def message
@@ -32,16 +26,21 @@ module Braid
32
26
  sig {returns(String)}
33
27
  attr_reader :path
34
28
 
35
- # It's going to take significant refactoring to be able to give this a type.
36
- sig {returns(T::Hash[String, T.untyped])}
29
+ # It's going to take significant refactoring to be able to give
30
+ # `MirrorAttributes` a type. Encapsulating the `T.untyped` in a type alias
31
+ # makes it easier to search for all the distinct root causes of untypedness
32
+ # in the code.
33
+ MirrorAttributes = T.type_alias { T::Hash[String, T.untyped] }
34
+
35
+ sig {returns(MirrorAttributes)}
37
36
  attr_reader :attributes
38
37
 
39
38
  BreakingChangeCallback = T.type_alias { T.proc.params(arg0: String).void }
40
39
 
41
- sig {params(path: String, attributes: T::Hash[String, T.untyped], breaking_change_cb: BreakingChangeCallback).void}
40
+ sig {params(path: String, attributes: MirrorAttributes, breaking_change_cb: BreakingChangeCallback).void}
42
41
  def initialize(path, attributes = {}, breaking_change_cb = DUMMY_BREAKING_CHANGE_CB)
43
42
  @path = T.let(path.sub(/\/$/, ''), String)
44
- @attributes = T.let(attributes.dup, T::Hash[String, T.untyped])
43
+ @attributes = T.let(attributes.dup, MirrorAttributes)
45
44
 
46
45
  # Not that it's terribly important to check for such an old feature. This
47
46
  # is mainly to demonstrate the RemoveMirrorDueToBreakingChange mechanism
@@ -79,19 +78,39 @@ DESC
79
78
  @attributes.delete('squashed')
80
79
  end
81
80
 
82
- sig {params(url: String, options: T.untyped).returns(Mirror)}
83
- def self.new_from_options(url, options = {})
81
+ # `Mirror::Options` doubles as the options struct for the `add` command, so
82
+ # some properties are meaningful only in that context. TODO: Maybe the code
83
+ # could be organized in a better way.
84
+ class Options < T::Struct
85
+ prop :tag, T.nilable(String)
86
+ prop :branch, T.nilable(String)
87
+ # NOTE: Used only by the `add` command; ignored by
88
+ # `Mirror::new_from_options`.
89
+ prop :revision, T.nilable(Operations::Git::ObjectExpr)
90
+ prop :path, T.nilable(String)
91
+ prop :remote_path, T.nilable(String)
92
+ end
93
+
94
+ sig {params(url: String, options: Options).returns(Mirror)}
95
+ def self.new_from_options(url, options = Options.new)
84
96
  url = url.sub(/\/$/, '')
97
+ # TODO: Ensure `url` is absolute? The user is probably more likely to
98
+ # move the downstream repository by itself than to move it along with the
99
+ # vendor repository. And we definitely don't want to use relative URLs in
100
+ # the cache.
85
101
 
86
- raise NoTagAndBranch if options['tag'] && options['branch']
102
+ raise NoTagAndBranch if options.tag && options.branch
87
103
 
88
- tag = options['tag']
89
- branch = options['branch']
104
+ tag = options.tag
105
+ branch = options.branch
90
106
 
91
- path = (options['path'] || extract_path_from_url(url, options['remote_path'])).sub(/\/$/, '')
92
- raise PathRequired unless path
107
+ path = (options.path || extract_path_from_url(url, options.remote_path)).sub(/\/$/, '')
108
+ # TODO: Check that `path` is a valid relative path and not something like
109
+ # '.' or ''. Some of these pathological cases will cause Braid to bail
110
+ # out later when something else fails, but it would be better to check up
111
+ # front.
93
112
 
94
- remote_path = options['remote_path']
113
+ remote_path = options.remote_path
95
114
 
96
115
  attributes = {'url' => url, 'branch' => branch, 'path' => remote_path, 'tag' => tag}
97
116
  self.new(path, attributes)
@@ -353,24 +372,16 @@ DESC
353
372
  hash
354
373
  end
355
374
 
356
- # TODO (typing): Return should not be nilable
357
- sig {params(url: String, remote_path: T.nilable(String)).returns(T.nilable(String))}
375
+ sig {params(url: String, remote_path: T.nilable(String)).returns(String)}
358
376
  def self.extract_path_from_url(url, remote_path)
359
377
  if remote_path
360
378
  return File.basename(remote_path)
361
379
  end
362
380
 
363
- # Avoid a Sorbet "unreachable code" error.
364
- # TODO (typing): Fix this properly. Probably just remove this line?
365
- return nil unless T.let(url, T.nilable(String))
366
381
  name = File.basename(url)
367
382
 
368
- if File.extname(name) == '.git'
369
- # strip .git
370
- name[0..-5]
371
- else
372
- name
373
- end
383
+ # strip .git if present
384
+ name.delete_suffix('.git')
374
385
  end
375
386
  end
376
387
  end
@@ -10,26 +10,18 @@ module Braid
10
10
 
11
11
  module Operations
12
12
  class ShellExecutionError < BraidError
13
- # TODO (typing): Should this be nilable?
14
- sig {returns(T.nilable(String))}
13
+ sig {returns(String)}
15
14
  attr_reader :err, :out
16
15
 
17
- sig {params(err: T.nilable(String), out: T.nilable(String)).void}
18
- def initialize(err = nil, out = nil)
16
+ sig {params(err: String, out: String).void}
17
+ def initialize(err, out)
19
18
  @err = err
20
19
  @out = out
21
20
  end
22
21
 
23
22
  sig {returns(String)}
24
23
  def message
25
- first_line = @err.to_s.split("\n").first
26
- # Currently, first_line can be nil if @err was empty, but Sorbet thinks
27
- # that the `message` method of an Exception should always return non-nil
28
- # (although override checking isn't enforced as of this writing), so
29
- # handle nil here. This seems ad-hoc but better than putting in a
30
- # `T.must` that we know has a risk of being wrong. Hopefully this will
31
- # be fixed better in https://github.com/cristibalan/braid/issues/90.
32
- first_line.nil? ? '' : first_line
24
+ @err
33
25
  end
34
26
  end
35
27
  class VersionTooLow < BraidError
@@ -201,7 +193,9 @@ module Braid
201
193
  ObjectID = T.type_alias { String }
202
194
 
203
195
  # A string containing an expression that can be evaluated to an object ID
204
- # by `git rev-parse`. Ditto the remark about lack of enforcement.
196
+ # by `git rev-parse`. This type is enforced even less than `ObjectID` (in
197
+ # some cases, we apply it directly to user input without validation), but
198
+ # it still serves to document our intent.
205
199
  ObjectExpr = T.type_alias { String }
206
200
 
207
201
  # Get the physical path to a file in the git repository (e.g.,
@@ -252,7 +246,7 @@ module Braid
252
246
  elsif out.match(/nothing.* to commit/)
253
247
  false
254
248
  else
255
- raise ShellExecutionError, err
249
+ raise ShellExecutionError.new(err, out)
256
250
  end
257
251
  end
258
252
 
@@ -364,7 +358,7 @@ module Braid
364
358
  # This can happen if the user runs `braid add` with a `--path` that
365
359
  # doesn't exist. TODO: Make the error message more user-friendly in
366
360
  # that case.
367
- raise ShellExecutionError, 'No tree item exists at the given path'
361
+ raise BraidError, 'No tree item exists at the given path'
368
362
  end
369
363
  mode = T.must(m[1])
370
364
  type = T.must(m[2])
@@ -374,7 +368,7 @@ module Braid
374
368
  elsif type == 'blob'
375
369
  return BlobWithMode.new(hash, mode)
376
370
  else
377
- raise ShellExecutionError, 'Tree item is not a tree or a blob'
371
+ raise BraidError, 'Tree item is not a tree or a blob'
378
372
  end
379
373
  end
380
374
  end
@@ -22,7 +22,7 @@ module Braid
22
22
  ).returns(T.type_parameter(:R))
23
23
  }
24
24
  def self.with_modified_environment(dict, &blk)
25
- orig_dict = {}
25
+ orig_dict = T.let({}, T::Hash[String, T.nilable(String)])
26
26
  dict.each { |name, value|
27
27
  orig_dict[name] = ENV[name]
28
28
  ENV[name] = value
@@ -74,5 +74,24 @@ module Braid
74
74
  end
75
75
  end
76
76
 
77
+ class Struct
78
+ def initialize(**kwargs)
79
+ # The fake runtime isn't obliged to validate the property names or
80
+ # types.
81
+ #
82
+ # Note: If the caller passed a hash of keyword arguments, Ruby will copy
83
+ # it, so we don't need to copy `kwargs` again here to avoid aliasing.
84
+ @attrs = kwargs
85
+ end
86
+
87
+ def self.prop(prop_name, prop_type)
88
+ define_method(prop_name) {
89
+ @attrs[prop_name]
90
+ }
91
+ define_method(:"#{prop_name}=") { |new_value|
92
+ @attrs[prop_name] = new_value
93
+ }
94
+ end
95
+ end
77
96
  end
78
97
  end
data/lib/braid/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # typed: false
3
3
 
4
4
  module Braid
5
- VERSION = '1.1.9'.freeze
5
+ VERSION = '1.1.10'.freeze
6
6
  end
data/lib/braid.rb CHANGED
@@ -27,10 +27,7 @@ module Braid
27
27
  !!@verbose
28
28
  end
29
29
 
30
- # TODO (typing): One would think `new_value` shouldn't be nilable, but
31
- # apparently `lib/braid/main.rb` passes nil sometimes. Is that easy to fix?
32
- # (Ditto with `self.force=` below.)
33
- sig {params(new_value: T.nilable(T::Boolean)).void}
30
+ sig {params(new_value: T::Boolean).void}
34
31
  def self.verbose=(new_value)
35
32
  @verbose = !!new_value
36
33
  end
@@ -42,7 +39,7 @@ module Braid
42
39
  !!@force
43
40
  end
44
41
 
45
- sig {params(new_value: T.nilable(T::Boolean)).void}
42
+ sig {params(new_value: T::Boolean).void}
46
43
  def self.force=(new_value)
47
44
  @force = !!new_value
48
45
  end
@@ -59,11 +56,6 @@ module Braid
59
56
 
60
57
  class BraidError < StandardError
61
58
  extend T::Sig
62
- sig {returns(String)}
63
- def message
64
- value = super
65
- value if value != self.class.name
66
- end
67
59
  end
68
60
 
69
61
  class InternalError < BraidError