braid 1.1.9 → 1.1.10

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.
@@ -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