braid 1.1.8 → 1.1.9

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