braid 1.1.8 → 1.1.9

Sign up to get free protection for your applications and to get access to all the features.
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: []