braid 1.1.8 → 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)
@@ -107,7 +126,7 @@ DESC
107
126
  branch.nil? && tag.nil?
108
127
  end
109
128
 
110
- sig {params(commit: String).returns(T::Boolean)}
129
+ sig {params(commit: Operations::Git::ObjectExpr).returns(T::Boolean)}
111
130
  def merged?(commit)
112
131
  # tip from spearce in #git:
113
132
  # `test z$(git merge-base A B) = z$(git rev-parse --verify A)`
@@ -115,9 +134,7 @@ DESC
115
134
  !!base_revision && git.merge_base(commit, base_revision) == commit
116
135
  end
117
136
 
118
- # We'll probably call the return type something like
119
- # Braid::Operations::Git::TreeItem.
120
- sig {params(revision: String).returns(T.untyped)}
137
+ sig {params(revision: String).returns(Operations::Git::TreeItem)}
121
138
  def upstream_item_for_revision(revision)
122
139
  git.get_tree_item(revision, self.remote_path)
123
140
  end
@@ -211,16 +228,28 @@ DESC
211
228
  git.remote_url(remote) == cached_url
212
229
  end
213
230
 
214
- sig {returns(String)}
231
+ sig {returns(Operations::Git::ObjectID)}
215
232
  def base_revision
216
- # Avoid a Sorbet "unreachable code" error.
217
- # TODO (typing): Is the revision expected to be non-nil nowadays? Can we
218
- # just remove the `inferred_revision` code path now?
219
- nilable_revision = T.let(revision, T.nilable(String))
220
- if nilable_revision
221
- git.rev_parse(revision)
233
+ # TODO (typing): We think `revision` should always be non-nil here these
234
+ # days and we can completely drop the `inferred_revision` code, but we're
235
+ # waiting for a better time to actually make this runtime behavior change
236
+ # and accept any risk of breakage
237
+ # (https://github.com/cristibalan/braid/pull/105/files#r857150464).
238
+ #
239
+ # Temporary variable
240
+ # (https://sorbet.org/docs/flow-sensitive#limitations-of-flow-sensitivity)
241
+ revision1 = revision
242
+ if revision1
243
+ git.rev_parse(revision1)
222
244
  else
223
- inferred_revision
245
+ # NOTE: Given that `inferred_revision` does appear to return nil on one
246
+ # code path, using this `T.must` and giving `base_revision` a
247
+ # non-nilable return type presents a theoretical risk of leading us to
248
+ # make changes to callers that break things at runtime. But we judge
249
+ # this a lesser evil than making the return type nilable and changing
250
+ # all callers to type-check successfully with that when we hope to
251
+ # revert the change soon anyway.
252
+ T.must(inferred_revision)
224
253
  end
225
254
  end
226
255
 
@@ -310,7 +339,12 @@ DESC
310
339
 
311
340
  sig {returns(String)}
312
341
  def remote
313
- "#{branch || tag || 'revision'}/braid/#{path}".gsub(/\/\./, '/_')
342
+ # Ensure that we replace any characters in the mirror path that might be
343
+ # problematic in a Git ref name. Theoretically, this may introduce
344
+ # collisions between mirrors, but we don't expect that to be much of a
345
+ # problem because Braid doesn't keep remotes by default after a command
346
+ # exits.
347
+ "#{branch || tag || 'revision'}_braid_#{path}".gsub(/[^-A-Za-z0-9]/, '_')
314
348
  end
315
349
 
316
350
  private
@@ -322,8 +356,8 @@ DESC
322
356
 
323
357
  sig {returns(T.nilable(String))}
324
358
  def inferred_revision
325
- local_commits = git.rev_list('HEAD', "-- #{path}").split("\n")
326
- remote_hashes = git.rev_list("--pretty=format:\"%T\"", remote).split('commit ').map do |chunk|
359
+ local_commits = git.rev_list(['HEAD', "-- #{path}"]).split("\n")
360
+ remote_hashes = git.rev_list(["--pretty=format:%T", remote]).split('commit ').map do |chunk|
327
361
  chunk.split("\n", 2).map { |value| value.strip }
328
362
  end
329
363
  hash = T.let(nil, T.nilable(String))
@@ -338,24 +372,16 @@ DESC
338
372
  hash
339
373
  end
340
374
 
341
- # TODO (typing): Return should not be nilable
342
- 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)}
343
376
  def self.extract_path_from_url(url, remote_path)
344
377
  if remote_path
345
378
  return File.basename(remote_path)
346
379
  end
347
380
 
348
- # Avoid a Sorbet "unreachable code" error.
349
- # TODO (typing): Fix this properly. Probably just remove this line?
350
- return nil unless T.let(url, T.nilable(String))
351
381
  name = File.basename(url)
352
382
 
353
- if File.extname(name) == '.git'
354
- # strip .git
355
- name[0..-5]
356
- else
357
- name
358
- end
383
+ # strip .git if present
384
+ name.delete_suffix('.git')
359
385
  end
360
386
  end
361
387
  end