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,7 +1,8 @@
1
- # typed: true
1
+ # typed: strict
2
2
 
3
3
  require 'singleton'
4
4
  require 'rubygems'
5
+ require 'shellwords'
5
6
  require 'tempfile'
6
7
 
7
8
  module Braid
@@ -9,45 +10,56 @@ module Braid
9
10
 
10
11
  module Operations
11
12
  class ShellExecutionError < BraidError
13
+ sig {returns(String)}
12
14
  attr_reader :err, :out
13
15
 
14
- def initialize(err = nil, out = nil)
16
+ sig {params(err: String, out: String).void}
17
+ def initialize(err, out)
15
18
  @err = err
16
19
  @out = out
17
20
  end
18
21
 
22
+ sig {returns(String)}
19
23
  def message
20
- @err.to_s.split("\n").first
24
+ @err
21
25
  end
22
26
  end
23
27
  class VersionTooLow < BraidError
28
+ sig {params(command: String, version: String, required: String).void}
24
29
  def initialize(command, version, required)
25
30
  @command = command
26
- @version = version.to_s.split("\n").first
31
+ # TODO (typing): Probably should not be nilable
32
+ @version = T.let(version.to_s.split("\n").first, T.nilable(String))
27
33
  @required = required
28
34
  end
29
35
 
36
+ sig {returns(String)}
30
37
  def message
31
38
  "#{@command} version too low: #{@version}. #{@required} needed."
32
39
  end
33
40
  end
34
41
  class UnknownRevision < BraidError
42
+ sig {returns(String)}
35
43
  def message
36
44
  "unknown revision: #{super}"
37
45
  end
38
46
  end
39
47
  class LocalChangesPresent < BraidError
48
+ sig {returns(String)}
40
49
  def message
41
50
  'local changes are present'
42
51
  end
43
52
  end
44
53
  class MergeError < BraidError
54
+ sig {returns(String)}
45
55
  attr_reader :conflicts_text
46
56
 
57
+ sig {params(conflicts_text: String).void}
47
58
  def initialize(conflicts_text)
48
59
  @conflicts_text = conflicts_text
49
60
  end
50
61
 
62
+ sig {returns(String)}
51
63
  def message
52
64
  'could not merge'
53
65
  end
@@ -55,18 +67,24 @@ module Braid
55
67
 
56
68
  # The command proxy is meant to encapsulate commands such as git, that work with subcommands.
57
69
  class Proxy
70
+ extend T::Sig
58
71
  include Singleton
59
72
 
60
- def self.command;
61
- T.unsafe(name).split('::').last.downcase;
73
+ # TODO (typing): We could make this method abstract if our fake Sorbet
74
+ # runtime supported abstract methods.
75
+ sig {returns(String)}
76
+ def self.command
77
+ raise InternalError, 'Proxy.command not overridden'
62
78
  end
63
79
 
64
80
  # hax!
81
+ sig {returns(String)}
65
82
  def version
66
- _, out, _ = exec!("#{self.class.command} --version")
83
+ _, out, _ = exec!([self.class.command, '--version'])
67
84
  out.sub(/^.* version/, '').strip.sub(/ .*$/, '').strip
68
85
  end
69
86
 
87
+ sig {params(required: String).returns(T::Boolean)}
70
88
  def require_version(required)
71
89
  # Gem::Version is intended for Ruby gem versions, but various web sites
72
90
  # suggest it as a convenient way of comparing version strings in
@@ -75,75 +93,117 @@ module Braid
75
93
  Gem::Version.new(version) >= Gem::Version.new(required)
76
94
  end
77
95
 
96
+ sig {params(required: String).void}
78
97
  def require_version!(required)
79
98
  require_version(required) || raise(VersionTooLow.new(self.class.command, version, required))
80
99
  end
81
100
 
82
101
  private
83
102
 
103
+ sig {params(name: String).returns(T::Array[String])}
84
104
  def command(name)
85
105
  # stub
86
- name
106
+ [name]
87
107
  end
88
108
 
89
- def invoke(arg, *args)
90
- exec!("#{command(arg)} #{args.join(' ')}".strip)[1].strip # return stdout
109
+ sig {params(arg: String, args: T::Array[String]).returns(String)}
110
+ def invoke(arg, args)
111
+ exec!(command(arg) + args)[1].strip # return stdout
91
112
  end
92
113
 
93
- def method_missing(name, *args)
94
- # We have to use this rather than `T.unsafe` because `invoke` is
95
- # private. See https://sorbet.org/docs/type-assertions#tbind.
96
- T.bind(self, T.untyped)
97
- invoke(name, *args)
98
- end
114
+ # Some of the unit tests want to mock out `exec`, but they have no way to
115
+ # construct a real Process::Status and thus use an integer instead. We
116
+ # have to accommodate this in the type annotation to avoid runtime type
117
+ # check failures during the tests. In normal use of Braid, this will
118
+ # always be a real Process::Status. Fortunately, allowing Integer doesn't
119
+ # seem to cause any other problems right now.
120
+ ProcessStatusOrInteger = T.type_alias { T.any(Process::Status, Integer) }
99
121
 
122
+ sig {params(cmd: T::Array[String]).returns([ProcessStatusOrInteger, String, String])}
100
123
  def exec(cmd)
101
- cmd.strip!
102
-
103
124
  Operations::with_modified_environment({'LANG' => 'C'}) do
104
125
  log(cmd)
105
- out, err, status = Open3.capture3(cmd)
126
+ # The special `[cmd[0], cmd[0]]` syntax ensures that `cmd[0]` is
127
+ # interpreted as the path of the executable and not a shell command
128
+ # even if `cmd` has only one element. See the documentation:
129
+ # https://ruby-doc.org/core-3.1.2/Process.html#method-c-spawn.
130
+ # Granted, this shouldn't matter for Braid for two reasons: (1)
131
+ # `cmd[0]` is always "git", which doesn't contain any shell special
132
+ # characters, and (2) `cmd` always has at least one additional
133
+ # argument (the Git subcommand). However, it's still nice to make our
134
+ # intent clear.
135
+ out, err, status = T.unsafe(Open3).capture3([cmd[0], cmd[0]], *cmd[1..])
106
136
  [status, out, err]
107
137
  end
108
138
  end
109
139
 
140
+ sig {params(cmd: T::Array[String]).returns([ProcessStatusOrInteger, String, String])}
110
141
  def exec!(cmd)
111
142
  status, out, err = exec(cmd)
112
143
  raise ShellExecutionError.new(err, out) unless status == 0
113
144
  [status, out, err]
114
145
  end
115
146
 
147
+ sig {params(cmd: T::Array[String]).returns(ProcessStatusOrInteger)}
116
148
  def system(cmd)
117
- cmd.strip!
118
-
119
149
  # Without this, "braid diff" output came out in the wrong order on Windows.
120
150
  $stdout.flush
121
151
  $stderr.flush
122
152
  Operations::with_modified_environment({'LANG' => 'C'}) do
123
- Kernel.system(cmd)
153
+ # See the comment in `exec` about the `[cmd[0], cmd[0]]` syntax.
154
+ T.unsafe(Kernel).system([cmd[0], cmd[0]], *cmd[1..])
124
155
  return $?
125
156
  end
126
157
  end
127
158
 
159
+ sig {params(str: String).void}
128
160
  def msg(str)
129
161
  puts "Braid: #{str}"
130
162
  end
131
163
 
164
+ sig {params(cmd: T::Array[String]).void}
132
165
  def log(cmd)
133
- msg "Executing `#{cmd}` in #{Dir.pwd}" if verbose?
166
+ # Note: `Shellwords.shelljoin` follows Bourne shell quoting rules, as
167
+ # its documentation states. This may not be what a Windows user
168
+ # expects, but it's not worth the trouble to try to find a library that
169
+ # produces something better on Windows, especially because it's unclear
170
+ # which of Windows's several different quoted formats we would use
171
+ # (e.g., CommandLineToArgvW, cmd.exe, or PowerShell). The most
172
+ # important thing is to use _some_ unambiguous representation.
173
+ msg "Executing `#{Shellwords.shelljoin(cmd)}` in #{Dir.pwd}" if verbose?
134
174
  end
135
175
 
176
+ sig {returns(T::Boolean)}
136
177
  def verbose?
137
178
  Braid.verbose
138
179
  end
139
180
  end
140
181
 
141
182
  class Git < Proxy
183
+
184
+ sig {returns(String)}
185
+ def self.command
186
+ 'git'
187
+ end
188
+
189
+ # A string representing a Git object ID (i.e., hash). This type alias is
190
+ # used as documentation and is not enforced, so there's a risk that we
191
+ # mistakenly mark something as an ObjectID when it can actually be a
192
+ # String that is not an ObjectID.
193
+ ObjectID = T.type_alias { String }
194
+
195
+ # A string containing an expression that can be evaluated to an object ID
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.
199
+ ObjectExpr = T.type_alias { String }
200
+
142
201
  # Get the physical path to a file in the git repository (e.g.,
143
202
  # 'MERGE_MSG'), taking into account worktree configuration. The returned
144
203
  # path may be absolute or relative to the current working directory.
204
+ sig {params(path: String).returns(String)}
145
205
  def repo_file_path(path)
146
- invoke(:rev_parse, '--git-path', path)
206
+ invoke('rev-parse', ['--git-path', path])
147
207
  end
148
208
 
149
209
  # If the current directory is not inside a git repository at all, this
@@ -151,8 +211,9 @@ module Braid
151
211
  # propagated as a ShellExecutionError. is_inside_worktree can return
152
212
  # false when inside a bare repository and in certain other rare cases such
153
213
  # as when the GIT_WORK_TREE environment variable is set.
214
+ sig {returns(T::Boolean)}
154
215
  def is_inside_worktree
155
- invoke(:rev_parse, '--is-inside-work-tree') == 'true'
216
+ invoke('rev-parse', ['--is-inside-work-tree']) == 'true'
156
217
  end
157
218
 
158
219
  # Get the prefix of the current directory relative to the worktree. Empty
@@ -160,21 +221,23 @@ module Braid
160
221
  # In some cases in which the current directory is not inside a worktree at
161
222
  # all, this will successfully return an empty string, so it may be
162
223
  # desirable to check is_inside_worktree first.
224
+ sig {returns(String)}
163
225
  def relative_working_dir
164
- invoke(:rev_parse, '--show-prefix')
226
+ invoke('rev-parse', ['--show-prefix'])
165
227
  end
166
228
 
167
- def commit(message, *args)
168
- cmd = 'git commit --no-verify'
229
+ sig {params(message: T.nilable(String), args: T::Array[String]).returns(T::Boolean)}
230
+ def commit(message, args = [])
231
+ cmd = ['git', 'commit', '--no-verify']
169
232
  message_file = nil
170
233
  if message # allow nil
171
234
  message_file = Tempfile.new('braid_commit')
172
235
  message_file.print("Braid: #{message}")
173
236
  message_file.flush
174
237
  message_file.close
175
- cmd << " -F #{message_file.path}"
238
+ cmd += ['-F', T.must(message_file.path)]
176
239
  end
177
- cmd << " #{args.join(' ')}" unless args.empty?
240
+ cmd += args
178
241
  status, out, err = exec(cmd)
179
242
  message_file.unlink if message_file
180
243
 
@@ -183,54 +246,59 @@ module Braid
183
246
  elsif out.match(/nothing.* to commit/)
184
247
  false
185
248
  else
186
- raise ShellExecutionError, err
249
+ raise ShellExecutionError.new(err, out)
187
250
  end
188
251
  end
189
252
 
190
- def fetch(remote = nil, *args)
191
- args.unshift "-n #{remote}" if remote
192
- exec!("git fetch #{args.join(' ')}")
193
- end
194
-
195
- def checkout(treeish)
196
- invoke(:checkout, treeish)
197
- true
253
+ sig {params(remote: T.nilable(String), args: T::Array[String]).void}
254
+ def fetch(remote = nil, args = [])
255
+ args = ['-n', remote] + args if remote
256
+ exec!(['git', 'fetch'] + args)
198
257
  end
199
258
 
200
259
  # Returns the base commit or nil.
260
+ sig {params(target: ObjectExpr, source: ObjectExpr).returns(T.nilable(ObjectID))}
201
261
  def merge_base(target, source)
202
- invoke(:merge_base, target, source)
262
+ invoke('merge-base', [target, source])
203
263
  rescue ShellExecutionError
204
264
  nil
205
265
  end
206
266
 
207
- def rev_parse(opt)
208
- invoke(:rev_parse, opt)
267
+ sig {params(expr: ObjectExpr).returns(ObjectID)}
268
+ def rev_parse(expr)
269
+ invoke('rev-parse', [expr])
209
270
  rescue ShellExecutionError
210
- raise UnknownRevision, opt
271
+ raise UnknownRevision, expr
211
272
  end
212
273
 
213
274
  # Implies tracking.
275
+ #
276
+ # TODO (typing): Remove the return value if we're confident that nothing
277
+ # uses it, here and in similar cases.
278
+ sig {params(remote: String, path: String).returns(TrueClass)}
214
279
  def remote_add(remote, path)
215
- invoke(:remote, 'add', remote, path)
280
+ invoke('remote', ['add', remote, path])
216
281
  true
217
282
  end
218
283
 
284
+ sig {params(remote: String).returns(TrueClass)}
219
285
  def remote_rm(remote)
220
- invoke(:remote, 'rm', remote)
286
+ invoke('remote', ['rm', remote])
221
287
  true
222
288
  end
223
289
 
224
290
  # Checks git remotes.
291
+ sig {params(remote: String).returns(T.nilable(String))}
225
292
  def remote_url(remote)
226
293
  key = "remote.#{remote}.url"
227
- invoke(:config, key)
294
+ invoke('config', [key])
228
295
  rescue ShellExecutionError
229
296
  nil
230
297
  end
231
298
 
299
+ sig {params(target: ObjectExpr).returns(TrueClass)}
232
300
  def reset_hard(target)
233
- invoke(:reset, '--hard', target)
301
+ invoke('reset', ['--hard', target])
234
302
  true
235
303
  end
236
304
 
@@ -242,47 +310,65 @@ module Braid
242
310
  # 'recursive' part (i.e., merge of bases) does not come into play and only
243
311
  # the trees matter. But for some reason, Git's smartest tree merge
244
312
  # algorithm is only available via the 'recursive' strategy.
313
+ sig {params(base_treeish: ObjectExpr, local_treeish: ObjectExpr, remote_treeish: ObjectExpr).returns(TrueClass)}
245
314
  def merge_trees(base_treeish, local_treeish, remote_treeish)
246
- invoke(:merge_recursive, base_treeish, "-- #{local_treeish} #{remote_treeish}")
315
+ invoke('merge-recursive', [base_treeish, '--', local_treeish, remote_treeish])
247
316
  true
248
317
  rescue ShellExecutionError => error
249
318
  # 'CONFLICT' messages go to stdout.
250
319
  raise MergeError, error.out
251
320
  end
252
321
 
322
+ sig {params(prefix: String).returns(String)}
253
323
  def read_ls_files(prefix)
254
- invoke('ls-files', prefix)
324
+ invoke('ls-files', [prefix])
255
325
  end
256
326
 
257
327
  class BlobWithMode
328
+ extend T::Sig
329
+ sig {params(hash: ObjectID, mode: String).void}
258
330
  def initialize(hash, mode)
259
331
  @hash = hash
260
332
  @mode = mode
261
333
  end
262
- attr_reader :hash, :mode
334
+ sig {returns(ObjectID)}
335
+ attr_reader :hash
336
+ sig {returns(String)}
337
+ attr_reader :mode
263
338
  end
264
339
  # Allow the class to be referenced as `git.BlobWithMode`.
340
+ sig {returns(T.class_of(BlobWithMode))}
265
341
  def BlobWithMode
266
342
  Git::BlobWithMode
267
343
  end
344
+ # An ObjectID used as a TreeItem represents a tree.
345
+ TreeItem = T.type_alias { T.any(ObjectID, BlobWithMode) }
268
346
 
269
347
  # Get the item at the given path in the given tree. If it's a tree, just
270
348
  # return its hash; if it's a blob, return a BlobWithMode object. (This is
271
349
  # how we remember the mode for single-file mirrors.)
350
+ # TODO (typing): Should `path` be nilable?
351
+ sig {params(tree: ObjectExpr, path: T.nilable(String)).returns(TreeItem)}
272
352
  def get_tree_item(tree, path)
273
353
  if path.nil? || path == ''
274
354
  tree
275
355
  else
276
- m = T.must(/^([^ ]*) ([^ ]*) ([^\t]*)\t.*$/.match(invoke(:ls_tree, tree, path)))
277
- mode = m[1]
278
- type = m[2]
279
- hash = m[3]
356
+ m = /^([^ ]*) ([^ ]*) ([^\t]*)\t.*$/.match(invoke('ls-tree', [tree, path]))
357
+ if m.nil?
358
+ # This can happen if the user runs `braid add` with a `--path` that
359
+ # doesn't exist. TODO: Make the error message more user-friendly in
360
+ # that case.
361
+ raise BraidError, 'No tree item exists at the given path'
362
+ end
363
+ mode = T.must(m[1])
364
+ type = T.must(m[2])
365
+ hash = T.must(m[3])
280
366
  if type == 'tree'
281
367
  hash
282
368
  elsif type == 'blob'
283
369
  return BlobWithMode.new(hash, mode)
284
370
  else
285
- raise ShellExecutionError, 'Tree item is not a tree or a blob'
371
+ raise BraidError, 'Tree item is not a tree or a blob'
286
372
  end
287
373
  end
288
374
  end
@@ -291,35 +377,47 @@ module Braid
291
377
  # path. If update_worktree is true, then update the worktree, otherwise
292
378
  # disregard the state of the worktree (most useful with a temporary index
293
379
  # file).
380
+ sig {params(item: TreeItem, path: String, update_worktree: T::Boolean).void}
294
381
  def add_item_to_index(item, path, update_worktree)
295
382
  if item.is_a?(BlobWithMode)
296
- invoke(:update_index, '--add', '--cacheinfo', "#{item.mode},#{item.hash},#{path}")
383
+ invoke('update-index', ['--add', '--cacheinfo', "#{item.mode},#{item.hash},#{path}"])
297
384
  if update_worktree
298
385
  # XXX If this fails, we've already updated the index.
299
- invoke(:checkout_index, path)
386
+ invoke('checkout-index', [path])
300
387
  end
301
388
  else
302
389
  # According to
303
390
  # https://lore.kernel.org/git/e48a281a4d3db0a04c0609fcb8658e4fcc797210.1646166271.git.gitgitgadget@gmail.com/,
304
391
  # `--prefix=` is valid if the path is empty.
305
- invoke(:read_tree, "--prefix=#{path}", update_worktree ? '-u' : '-i', item)
392
+ invoke('read-tree', ["--prefix=#{path}", update_worktree ? '-u' : '-i', item])
306
393
  end
307
394
  end
308
395
 
309
396
  # Read tree into the root of the index. This may not be the preferred way
310
397
  # to do it, but it seems to work.
398
+ sig {params(treeish: ObjectExpr).void}
311
399
  def read_tree_im(treeish)
312
- invoke(:read_tree, '-im', treeish)
313
- true
400
+ invoke('read-tree', ['-im', treeish])
401
+ end
402
+
403
+ sig {params(treeish: ObjectExpr).void}
404
+ def read_tree_um(treeish)
405
+ invoke('read-tree', ['-um', treeish])
314
406
  end
315
407
 
316
408
  # Write a tree object for the current index and return its ID.
409
+ sig {returns(ObjectID)}
317
410
  def write_tree
318
- invoke(:write_tree)
411
+ invoke('write-tree', [])
319
412
  end
320
413
 
321
414
  # Execute a block using a temporary git index file, initially empty.
322
- def with_temporary_index
415
+ sig {
416
+ type_parameters(:R).params(
417
+ blk: T.proc.returns(T.type_parameter(:R))
418
+ ).returns(T.type_parameter(:R))
419
+ }
420
+ def with_temporary_index(&blk)
323
421
  Dir.mktmpdir('braid_index') do |dir|
324
422
  Operations::with_modified_environment(
325
423
  {'GIT_INDEX_FILE' => File.join(dir, 'index')}) do
@@ -328,6 +426,7 @@ module Braid
328
426
  end
329
427
  end
330
428
 
429
+ sig {params(main_content: T.nilable(ObjectExpr), item_path: String, item: TreeItem).returns(ObjectID)}
331
430
  def make_tree_with_item(main_content, item_path, item)
332
431
  with_temporary_index do
333
432
  # If item_path is '', then rm_r_cached will fail. But in that case,
@@ -342,66 +441,117 @@ module Braid
342
441
  end
343
442
  end
344
443
 
444
+ sig {params(args: T::Array[String]).returns(T.nilable(String))}
345
445
  def config(args)
346
- invoke(:config, args) rescue nil
446
+ invoke('config', args) rescue nil
347
447
  end
348
448
 
449
+ sig {params(path: String).void}
450
+ def add(path)
451
+ invoke('add', [path])
452
+ end
453
+
454
+ sig {params(path: String).void}
455
+ def rm(path)
456
+ invoke('rm', [path])
457
+ end
458
+
459
+ sig {params(path: String).returns(TrueClass)}
349
460
  def rm_r(path)
350
- invoke(:rm, '-r', path)
461
+ invoke('rm', ['-r', path])
351
462
  true
352
463
  end
353
464
 
354
465
  # Remove from index only.
466
+ sig {params(path: String).returns(TrueClass)}
355
467
  def rm_r_cached(path)
356
- invoke(:rm, '-r', '--cached', path)
468
+ invoke('rm', ['-r', '--cached', path])
357
469
  true
358
470
  end
359
471
 
472
+ sig {params(path: String, treeish: ObjectExpr).returns(ObjectID)}
360
473
  def tree_hash(path, treeish = 'HEAD')
361
- out = invoke(:ls_tree, treeish, '-d', path)
362
- out.split[2]
474
+ out = invoke('ls-tree', [treeish, '-d', path])
475
+ T.must(out.split[2])
363
476
  end
364
477
 
365
- def diff_to_stdout(*args)
478
+ sig {params(args: T::Array[String]).returns(String)}
479
+ def diff(args)
480
+ invoke('diff', args)
481
+ end
482
+
483
+ sig {params(args: T::Array[String]).returns(ProcessStatusOrInteger)}
484
+ def diff_to_stdout(args)
366
485
  # For now, ignore the exit code. It can be 141 (SIGPIPE) if the user
367
486
  # quits the pager before reading all the output.
368
- system("git diff #{args.join(' ')}")
487
+ system(['git', 'diff'] + args)
369
488
  end
370
489
 
490
+ sig {returns(T::Boolean)}
371
491
  def status_clean?
372
- _, out, _ = exec('git status')
492
+ _, out, _ = exec(['git', 'status'])
373
493
  !out.split("\n").grep(/nothing to commit/).empty?
374
494
  end
375
495
 
496
+ sig {void}
376
497
  def ensure_clean!
377
498
  status_clean? || raise(LocalChangesPresent)
378
499
  end
379
500
 
501
+ sig {returns(ObjectID)}
380
502
  def head
381
503
  rev_parse('HEAD')
382
504
  end
383
505
 
384
- def branch
385
- _, out, _ = exec!("git branch | grep '*'")
386
- out[2..-1]
506
+ sig {void}
507
+ def init
508
+ invoke('init', [])
509
+ end
510
+
511
+ sig {params(args: T::Array[String]).void}
512
+ def clone(args)
513
+ invoke('clone', args)
514
+ end
515
+
516
+ # Wrappers for Git commands that were called via `method_missing` before
517
+ # the move to static typing but for which the existing calls don't follow
518
+ # a clear enough pattern around which we could design a narrower API than
519
+ # forwarding an arbitrary argument list. We may narrow the API in the
520
+ # future if it becomes clear what it should be.
521
+
522
+ sig {params(args: T::Array[String]).returns(String)}
523
+ def rev_list(args)
524
+ invoke('rev-list', args)
525
+ end
526
+
527
+ sig {params(args: T::Array[String]).void}
528
+ def update_ref(args)
529
+ invoke('update-ref', args)
530
+ end
531
+
532
+ sig {params(args: T::Array[String]).void}
533
+ def push(args)
534
+ invoke('push', args)
387
535
  end
388
536
 
389
- def clone(*args)
390
- # overrides builtin
391
- T.bind(self, T.untyped) # Ditto the comment in `method_missing`.
392
- invoke(:clone, *args)
537
+ sig {params(args: T::Array[String]).returns(String)}
538
+ def ls_remote(args)
539
+ invoke('ls-remote', args)
393
540
  end
394
541
 
395
542
  private
396
543
 
544
+ sig {params(name: String).returns(T::Array[String])}
397
545
  def command(name)
398
- "#{self.class.command} #{name.to_s.gsub('_', '-')}"
546
+ [self.class.command, name]
399
547
  end
400
548
  end
401
549
 
402
550
  class GitCache
551
+ extend T::Sig
403
552
  include Singleton
404
553
 
554
+ sig {params(url: String).void}
405
555
  def fetch(url)
406
556
  dir = path(url)
407
557
 
@@ -416,30 +566,36 @@ module Braid
416
566
  end
417
567
  else
418
568
  FileUtils.mkdir_p(local_cache_dir)
419
- git.clone('--mirror', url, dir)
569
+ git.clone(['--mirror', url, dir])
420
570
  end
421
571
  end
422
572
 
573
+ sig {params(url: String).returns(String)}
423
574
  def path(url)
424
575
  File.join(local_cache_dir, url.gsub(/[\/:@]/, '_'))
425
576
  end
426
577
 
427
578
  private
428
579
 
580
+ sig {returns(String)}
429
581
  def local_cache_dir
430
582
  Braid.local_cache_dir
431
583
  end
432
584
 
585
+ sig {returns(Git)}
433
586
  def git
434
587
  Git.instance
435
588
  end
436
589
  end
437
590
 
438
591
  module VersionControl
592
+ extend T::Sig
593
+ sig {returns(Git)}
439
594
  def git
440
595
  Git.instance
441
596
  end
442
597
 
598
+ sig {returns(GitCache)}
443
599
  def git_cache
444
600
  GitCache.instance
445
601
  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