braid 1.1.8 → 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)
@@ -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