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,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