braid 1.1.7 → 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: ad14f6dd7a5d428c5796bbd19039aae6e59f5803d93da3889fe0a92a8afd032b
4
- data.tar.gz: 9d6bcfd73c5a306f853e6b9653edda5aff6a7bbd3541b6ab644bd7b96bc510e6
3
+ metadata.gz: 0ddcc3421c5a2dffcf7634e71118d7392d9f506ab49064f25eb2c38f7f0d67b1
4
+ data.tar.gz: d3ad647f242d287ece52de41be7edb56cfdc3c469336ee84bb35c15257ca4d36
5
5
  SHA512:
6
- metadata.gz: 817cd08525921352f103a81f05b6c550e8eaff62fb647c04d0ecb746e6ed425a78ef1bea4a477786675c406b0e23982f36a52eea205278a3b25ccee6e64ecdf5
7
- data.tar.gz: 76d9e62b1d58a437a8eae77f879b5a66ec6a30cda270d267cc458e621a3f1560fa192f5cc435b5e718c9f093691464d458b049d2e236864d0faea6e801c89629
6
+ metadata.gz: f002d88fa8bb752ab878d746c8b1ead6c73b80518d37a06f261eba2b99540c6081c7991a9f404ec0ca26c39165b6e9345725e4b4f49f52ad132ca1e359465e39
7
+ data.tar.gz: 4da0947c03c1846b073b331f4118f8b07e42b4138c3450b3f2ce6ba967e3a8451edf843622e3e6b6bbe4f5c70951866d7c6abbcc7e1211d10501b75fefa8e095
@@ -1,3 +1,10 @@
1
+ # Since this file can't safely depend on the Sorbet runtime, it isn't prudent to
2
+ # try to commit to `typed: true` even if the file currently passes type checking
3
+ # without needing any references to `T`. Like `exe/braid`, this file doesn't
4
+ # have much code worth type checking.
5
+ #
6
+ # typed: false
7
+
1
8
  # Braid has several entry points that run code from Ruby gems (either Braid
2
9
  # itself or dependencies such as Sorbet) and expect to get the correct versions
3
10
  # (either the same copy of Braid or versions of dependencies consistent with the
data/lib/braid/command.rb CHANGED
@@ -17,6 +17,11 @@ module Braid
17
17
  klass.new.run(*args)
18
18
 
19
19
  rescue BraidError => error
20
+ handle_error(error)
21
+ end
22
+
23
+ sig {params(error: BraidError).returns(T.noreturn)}
24
+ def self.handle_error(error)
20
25
  case error
21
26
  when Operations::ShellExecutionError
22
27
  msg "Shell error: #{error.message}"
@@ -36,6 +41,7 @@ module Braid
36
41
  self.class.msg(str)
37
42
  end
38
43
 
44
+ sig {returns(Config)}
39
45
  def config
40
46
  @config ||= Config.new({'mode' => config_mode})
41
47
  end
@@ -1,15 +1,55 @@
1
+ # typed: true
1
2
  module Braid
2
3
  module Commands
3
4
  class Add < Command
5
+ # Returns the default branch name of the repository at the given URL, or
6
+ # nil if it couldn't be determined.
7
+ #
8
+ # We won't be able to determine a default branch in certain cases that we
9
+ # expect to be unusual in the context of Braid, such as if the HEAD is
10
+ # detached or points to a ref outside of `refs/heads`. (Presumably, the
11
+ # same thing happens if the server is too old to report symrefs to us.)
12
+ # In those cases, a plausible alternative behavior would be to just lock
13
+ # the mirror to the remote HEAD revision, but that's probably not what the
14
+ # user wants. It's much more likely that something is wrong and Braid
15
+ # should report an error.
16
+ def get_default_branch_name(url)
17
+ head_targets = []
18
+ # The `HEAD` parameter here doesn't appear to do an exact match (it
19
+ # appears to match any ref with `HEAD` as the last path component, such
20
+ # as `refs/remotes/origin/HEAD` in the unusual case where the repository
21
+ # contains its own remote-tracking branches), but it reduces the data we
22
+ # have to scan a bit.
23
+ git.ls_remote(['--symref', url, 'HEAD']).split("\n").each do |line|
24
+ m = /^ref: (.*)\tHEAD$/.match(line)
25
+ head_targets.push(m[1]) if m
26
+ end
27
+ return nil unless head_targets.size == 1
28
+ m = /^refs\/heads\/(.*)$/.match(head_targets[0])
29
+ return nil unless m
30
+ m[1]
31
+ end
32
+
4
33
  def run(url, options = {})
5
34
  with_reset_on_error do
35
+ if options['branch'].nil? && options['tag'].nil? && options['revision'].nil?
36
+ default_branch = get_default_branch_name(url)
37
+ if default_branch.nil?
38
+ raise BraidError, <<-MSG
39
+ Failed to detect the default branch of the remote repository. Please specify
40
+ the branch you want to use via the --branch option.
41
+ MSG
42
+ end
43
+ options['branch'] = default_branch
44
+ end
45
+
6
46
  mirror = config.add_from_options(url, options)
7
47
  add_config_file
8
48
 
9
49
  mirror.branch = nil if options['revision']
10
50
  raise BraidError, 'Can not add mirror specifying both a revision and a tag' if options['revision'] && mirror.tag
11
51
 
12
- branch_message = (mirror.branch.nil? || mirror.branch == 'master') ? '' : " branch '#{mirror.branch}'"
52
+ branch_message = mirror.branch.nil? ? '' : " branch '#{mirror.branch}'"
13
53
  tag_message = mirror.tag.nil? ? '' : " tag '#{mirror.tag}'"
14
54
  revision_message = options['revision'] ? " at #{display_revision(mirror, options['revision'])}" : ''
15
55
  msg "Adding mirror of '#{mirror.url}'#{branch_message}#{tag_message}#{revision_message}."
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  module Braid
2
3
  module Commands
3
4
  class Diff < Command
@@ -35,7 +36,7 @@ module Braid
35
36
 
36
37
  # XXX: Warn if the user specifies file paths that are outside the
37
38
  # mirror? Currently, they just won't match anything.
38
- git.diff_to_stdout(*mirror.diff_args(options['git_diff_args']))
39
+ git.diff_to_stdout(mirror.diff_args(options['git_diff_args']))
39
40
 
40
41
  clear_remote(mirror, options)
41
42
  end
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  require 'fileutils'
2
3
  require 'tmpdir'
3
4
 
@@ -43,7 +44,8 @@ module Braid
43
44
  end
44
45
  clone_dir = Dir.tmpdir + "/braid_push.#{$$}"
45
46
  Dir.mkdir(clone_dir)
46
- 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))
47
49
  if remote_url == mirror.cached_url
48
50
  remote_url = mirror.url
49
51
  elsif File.directory?(remote_url)
@@ -67,7 +69,7 @@ module Braid
67
69
  File.open('.git/objects/info/alternates', 'wb') { |f|
68
70
  f.puts(odb_paths)
69
71
  }
70
- git.fetch(remote_url, mirror.remote_ref)
72
+ git.fetch(remote_url, [mirror.remote_ref])
71
73
  new_tree = git.make_tree_with_item(base_revision,
72
74
  mirror.remote_path || '', local_mirror_item)
73
75
  if git.require_version('2.27')
@@ -105,11 +107,11 @@ module Braid
105
107
  # Update HEAD the same way git.checkout(base_revision) would, but
106
108
  # don't populate the index or working tree (to save us the trouble of
107
109
  # emptying them again before the git.read_tree).
108
- git.update_ref('--no-deref', 'HEAD', base_revision)
109
- git.read_tree('-mu', new_tree)
110
+ git.update_ref(['--no-deref', 'HEAD', base_revision])
111
+ git.read_tree_um(new_tree)
110
112
  system('git commit -v')
111
113
  msg "Pushing changes to remote branch #{branch}."
112
- git.push(remote_url, "HEAD:refs/heads/#{branch}")
114
+ git.push([remote_url, "HEAD:refs/heads/#{branch}"])
113
115
  end
114
116
  FileUtils.rm_r(clone_dir)
115
117
 
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  module Braid
2
3
  module Commands
3
4
  class Remove < Command
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  module Braid
2
3
  module Commands
3
4
  class Setup < Command
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  module Braid
2
3
  module Commands
3
4
  class Status < Command
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  module Braid
2
3
  module Commands
3
4
  class Update < Command
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  module Braid
2
3
  module Commands
3
4
  class UpgradeConfig < Command
data/lib/braid/main.rb CHANGED
@@ -1,13 +1,24 @@
1
+ # typed: true
2
+
1
3
  require 'braid'
2
4
 
3
5
  require 'rubygems'
4
6
  require 'main'
5
7
 
8
+ # This is needed for `T` below to resolve to `Braid::T` when using the fake
9
+ # Sorbet runtime. TODO: Indent the contents and accept the large diff?
10
+ module Braid
11
+
6
12
  Home = File.expand_path(ENV['HOME'] || '~')
7
13
 
8
14
  # mostly blantantly stolen from ara's punch script
9
15
  # main kicks ass!
10
- Main {
16
+ T.unsafe(Main).run {
17
+ # `Main` is somewhat mind-bending and I'm unsure what the type of `self`
18
+ # actually is here, but whatever it is, we don't have a type declaration for
19
+ # it.
20
+ T.bind(self, T.untyped)
21
+
11
22
  description <<-TXT
12
23
  braid is a simple tool to help track git repositories inside a git repository.
13
24
 
@@ -20,7 +31,8 @@ Main {
20
31
  # The "main" library doesn't provide a way to do this??
21
32
  def check_no_extra_args!
22
33
  if @argv.length > 0
23
- die 'Extra argument(s) passed to command.'
34
+ Braid::Command.handle_error(
35
+ Braid::BraidError.new('Extra argument(s) passed to command.'))
24
36
  end
25
37
  end
26
38
 
@@ -123,7 +135,7 @@ Main {
123
135
 
124
136
  mixin :optional_local_path, :option_verbose, :option_keep_remote
125
137
 
126
- synopsis(Main::Usage.default_synopsis(self) + ' [-- git_diff_arg*]')
138
+ synopsis(T.unsafe(Main::Usage).default_synopsis(self) + ' [-- git_diff_arg*]')
127
139
 
128
140
  run {
129
141
  if @argv.length > 0 && @argv[0] == '--'
@@ -318,3 +330,5 @@ Main {
318
330
 
319
331
  run { help! }
320
332
  }
333
+
334
+ end
data/lib/braid/mirror.rb CHANGED
@@ -86,7 +86,7 @@ DESC
86
86
  raise NoTagAndBranch if options['tag'] && options['branch']
87
87
 
88
88
  tag = options['tag']
89
- branch = options['branch'] || (tag.nil? ? 'master' : nil)
89
+ branch = options['branch']
90
90
 
91
91
  path = (options['path'] || extract_path_from_url(url, options['remote_path'])).sub(/\/$/, '')
92
92
  raise PathRequired unless path
@@ -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,5 +1,8 @@
1
+ # typed: strict
2
+
1
3
  require 'singleton'
2
4
  require 'rubygems'
5
+ require 'shellwords'
3
6
  require 'tempfile'
4
7
 
5
8
  module Braid
@@ -7,45 +10,64 @@ module Braid
7
10
 
8
11
  module Operations
9
12
  class ShellExecutionError < BraidError
13
+ # TODO (typing): Should this be nilable?
14
+ sig {returns(T.nilable(String))}
10
15
  attr_reader :err, :out
11
16
 
17
+ sig {params(err: T.nilable(String), out: T.nilable(String)).void}
12
18
  def initialize(err = nil, out = nil)
13
19
  @err = err
14
20
  @out = out
15
21
  end
16
22
 
23
+ sig {returns(String)}
17
24
  def message
18
- @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
19
33
  end
20
34
  end
21
35
  class VersionTooLow < BraidError
36
+ sig {params(command: String, version: String, required: String).void}
22
37
  def initialize(command, version, required)
23
38
  @command = command
24
- @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))
25
41
  @required = required
26
42
  end
27
43
 
44
+ sig {returns(String)}
28
45
  def message
29
46
  "#{@command} version too low: #{@version}. #{@required} needed."
30
47
  end
31
48
  end
32
49
  class UnknownRevision < BraidError
50
+ sig {returns(String)}
33
51
  def message
34
52
  "unknown revision: #{super}"
35
53
  end
36
54
  end
37
55
  class LocalChangesPresent < BraidError
56
+ sig {returns(String)}
38
57
  def message
39
58
  'local changes are present'
40
59
  end
41
60
  end
42
61
  class MergeError < BraidError
62
+ sig {returns(String)}
43
63
  attr_reader :conflicts_text
44
64
 
65
+ sig {params(conflicts_text: String).void}
45
66
  def initialize(conflicts_text)
46
67
  @conflicts_text = conflicts_text
47
68
  end
48
69
 
70
+ sig {returns(String)}
49
71
  def message
50
72
  'could not merge'
51
73
  end
@@ -53,18 +75,24 @@ module Braid
53
75
 
54
76
  # The command proxy is meant to encapsulate commands such as git, that work with subcommands.
55
77
  class Proxy
78
+ extend T::Sig
56
79
  include Singleton
57
80
 
58
- def self.command;
59
- 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'
60
86
  end
61
87
 
62
88
  # hax!
89
+ sig {returns(String)}
63
90
  def version
64
- _, out, _ = exec!("#{self.class.command} --version")
91
+ _, out, _ = exec!([self.class.command, '--version'])
65
92
  out.sub(/^.* version/, '').strip.sub(/ .*$/, '').strip
66
93
  end
67
94
 
95
+ sig {params(required: String).returns(T::Boolean)}
68
96
  def require_version(required)
69
97
  # Gem::Version is intended for Ruby gem versions, but various web sites
70
98
  # suggest it as a convenient way of comparing version strings in
@@ -73,77 +101,115 @@ module Braid
73
101
  Gem::Version.new(version) >= Gem::Version.new(required)
74
102
  end
75
103
 
104
+ sig {params(required: String).void}
76
105
  def require_version!(required)
77
106
  require_version(required) || raise(VersionTooLow.new(self.class.command, version, required))
78
107
  end
79
108
 
80
109
  private
81
110
 
111
+ sig {params(name: String).returns(T::Array[String])}
82
112
  def command(name)
83
113
  # stub
84
- name
114
+ [name]
85
115
  end
86
116
 
87
- def invoke(arg, *args)
88
- 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
89
120
  end
90
121
 
91
- def method_missing(name, *args)
92
- invoke(name, *args)
93
- 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) }
94
129
 
130
+ sig {params(cmd: T::Array[String]).returns([ProcessStatusOrInteger, String, String])}
95
131
  def exec(cmd)
96
- cmd.strip!
97
-
98
132
  Operations::with_modified_environment({'LANG' => 'C'}) do
99
133
  log(cmd)
100
- 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..])
101
144
  [status, out, err]
102
145
  end
103
146
  end
104
147
 
148
+ sig {params(cmd: T::Array[String]).returns([ProcessStatusOrInteger, String, String])}
105
149
  def exec!(cmd)
106
150
  status, out, err = exec(cmd)
107
151
  raise ShellExecutionError.new(err, out) unless status == 0
108
152
  [status, out, err]
109
153
  end
110
154
 
155
+ sig {params(cmd: T::Array[String]).returns(ProcessStatusOrInteger)}
111
156
  def system(cmd)
112
- cmd.strip!
113
-
114
157
  # Without this, "braid diff" output came out in the wrong order on Windows.
115
158
  $stdout.flush
116
159
  $stderr.flush
117
160
  Operations::with_modified_environment({'LANG' => 'C'}) do
118
- 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..])
119
163
  return $?
120
164
  end
121
165
  end
122
166
 
167
+ sig {params(str: String).void}
123
168
  def msg(str)
124
169
  puts "Braid: #{str}"
125
170
  end
126
171
 
172
+ sig {params(cmd: T::Array[String]).void}
127
173
  def log(cmd)
128
- 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?
129
182
  end
130
183
 
184
+ sig {returns(T::Boolean)}
131
185
  def verbose?
132
186
  Braid.verbose
133
187
  end
134
188
  end
135
189
 
136
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
+
137
207
  # Get the physical path to a file in the git repository (e.g.,
138
208
  # 'MERGE_MSG'), taking into account worktree configuration. The returned
139
209
  # path may be absolute or relative to the current working directory.
210
+ sig {params(path: String).returns(String)}
140
211
  def repo_file_path(path)
141
- if require_version('2.5') # support for --git-path
142
- invoke(:rev_parse, '--git-path', path)
143
- else
144
- # Git < 2.5 doesn't support linked worktrees anyway.
145
- File.join(invoke(:rev_parse, '--git-dir'), path)
146
- end
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]*)\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,37 +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
- # Our minimum git version is 1.6.0 and the new --cacheinfo syntax
297
- # wasn't added until 2.0.0.
298
- invoke(:update_index, '--add', '--cacheinfo', item.mode, item.hash, path)
389
+ invoke('update-index', ['--add', '--cacheinfo', "#{item.mode},#{item.hash},#{path}"])
299
390
  if update_worktree
300
391
  # XXX If this fails, we've already updated the index.
301
- invoke(:checkout_index, path)
392
+ invoke('checkout-index', [path])
302
393
  end
303
394
  else
304
395
  # According to
305
396
  # https://lore.kernel.org/git/e48a281a4d3db0a04c0609fcb8658e4fcc797210.1646166271.git.gitgitgadget@gmail.com/,
306
397
  # `--prefix=` is valid if the path is empty.
307
- invoke(:read_tree, "--prefix=#{path}", update_worktree ? '-u' : '-i', item)
398
+ invoke('read-tree', ["--prefix=#{path}", update_worktree ? '-u' : '-i', item])
308
399
  end
309
400
  end
310
401
 
311
402
  # Read tree into the root of the index. This may not be the preferred way
312
403
  # to do it, but it seems to work.
404
+ sig {params(treeish: ObjectExpr).void}
313
405
  def read_tree_im(treeish)
314
- invoke(:read_tree, '-im', treeish)
315
- 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])
316
412
  end
317
413
 
318
414
  # Write a tree object for the current index and return its ID.
415
+ sig {returns(ObjectID)}
319
416
  def write_tree
320
- invoke(:write_tree)
417
+ invoke('write-tree', [])
321
418
  end
322
419
 
323
420
  # Execute a block using a temporary git index file, initially empty.
324
- 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)
325
427
  Dir.mktmpdir('braid_index') do |dir|
326
428
  Operations::with_modified_environment(
327
429
  {'GIT_INDEX_FILE' => File.join(dir, 'index')}) do
@@ -330,6 +432,7 @@ module Braid
330
432
  end
331
433
  end
332
434
 
435
+ sig {params(main_content: T.nilable(ObjectExpr), item_path: String, item: TreeItem).returns(ObjectID)}
333
436
  def make_tree_with_item(main_content, item_path, item)
334
437
  with_temporary_index do
335
438
  # If item_path is '', then rm_r_cached will fail. But in that case,
@@ -344,103 +447,161 @@ module Braid
344
447
  end
345
448
  end
346
449
 
450
+ sig {params(args: T::Array[String]).returns(T.nilable(String))}
347
451
  def config(args)
348
- invoke(:config, args) rescue nil
452
+ invoke('config', args) rescue nil
349
453
  end
350
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)}
351
466
  def rm_r(path)
352
- invoke(:rm, '-r', path)
467
+ invoke('rm', ['-r', path])
353
468
  true
354
469
  end
355
470
 
356
471
  # Remove from index only.
472
+ sig {params(path: String).returns(TrueClass)}
357
473
  def rm_r_cached(path)
358
- invoke(:rm, '-r', '--cached', path)
474
+ invoke('rm', ['-r', '--cached', path])
359
475
  true
360
476
  end
361
477
 
478
+ sig {params(path: String, treeish: ObjectExpr).returns(ObjectID)}
362
479
  def tree_hash(path, treeish = 'HEAD')
363
- out = invoke(:ls_tree, treeish, '-d', path)
364
- out.split[2]
480
+ out = invoke('ls-tree', [treeish, '-d', path])
481
+ T.must(out.split[2])
482
+ end
483
+
484
+ sig {params(args: T::Array[String]).returns(String)}
485
+ def diff(args)
486
+ invoke('diff', args)
365
487
  end
366
488
 
367
- def diff_to_stdout(*args)
489
+ sig {params(args: T::Array[String]).returns(ProcessStatusOrInteger)}
490
+ def diff_to_stdout(args)
368
491
  # For now, ignore the exit code. It can be 141 (SIGPIPE) if the user
369
492
  # quits the pager before reading all the output.
370
- system("git diff #{args.join(' ')}")
493
+ system(['git', 'diff'] + args)
371
494
  end
372
495
 
496
+ sig {returns(T::Boolean)}
373
497
  def status_clean?
374
- _, out, _ = exec('git status')
498
+ _, out, _ = exec(['git', 'status'])
375
499
  !out.split("\n").grep(/nothing to commit/).empty?
376
500
  end
377
501
 
502
+ sig {void}
378
503
  def ensure_clean!
379
504
  status_clean? || raise(LocalChangesPresent)
380
505
  end
381
506
 
507
+ sig {returns(ObjectID)}
382
508
  def head
383
509
  rev_parse('HEAD')
384
510
  end
385
511
 
386
- def branch
387
- _, out, _ = exec!("git branch | grep '*'")
388
- 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)
389
541
  end
390
542
 
391
- def clone(*args)
392
- # overrides builtin
393
- invoke(:clone, *args)
543
+ sig {params(args: T::Array[String]).returns(String)}
544
+ def ls_remote(args)
545
+ invoke('ls-remote', args)
394
546
  end
395
547
 
396
548
  private
397
549
 
550
+ sig {params(name: String).returns(T::Array[String])}
398
551
  def command(name)
399
- "#{self.class.command} #{name.to_s.gsub('_', '-')}"
552
+ [self.class.command, name]
400
553
  end
401
554
  end
402
555
 
403
556
  class GitCache
557
+ extend T::Sig
404
558
  include Singleton
405
559
 
560
+ sig {params(url: String).void}
406
561
  def fetch(url)
407
562
  dir = path(url)
408
563
 
409
564
  # remove local cache if it was created with --no-checkout
410
- if File.exists?("#{dir}/.git")
565
+ if File.exist?("#{dir}/.git")
411
566
  FileUtils.rm_r(dir)
412
567
  end
413
568
 
414
- if File.exists?(dir)
569
+ if File.exist?(dir)
415
570
  Dir.chdir(dir) do
416
571
  git.fetch
417
572
  end
418
573
  else
419
574
  FileUtils.mkdir_p(local_cache_dir)
420
- git.clone('--mirror', url, dir)
575
+ git.clone(['--mirror', url, dir])
421
576
  end
422
577
  end
423
578
 
579
+ sig {params(url: String).returns(String)}
424
580
  def path(url)
425
581
  File.join(local_cache_dir, url.gsub(/[\/:@]/, '_'))
426
582
  end
427
583
 
428
584
  private
429
585
 
586
+ sig {returns(String)}
430
587
  def local_cache_dir
431
588
  Braid.local_cache_dir
432
589
  end
433
590
 
591
+ sig {returns(Git)}
434
592
  def git
435
593
  Git.instance
436
594
  end
437
595
  end
438
596
 
439
597
  module VersionControl
598
+ extend T::Sig
599
+ sig {returns(Git)}
440
600
  def git
441
601
  Git.instance
442
602
  end
443
603
 
604
+ sig {returns(GitCache)}
444
605
  def git_cache
445
606
  GitCache.instance
446
607
  end
@@ -36,6 +36,10 @@ module Braid
36
36
  def self.must(value)
37
37
  value
38
38
  end
39
+ def self.unsafe(value)
40
+ value
41
+ end
42
+ def self.bind(value, type); end
39
43
 
40
44
  class FakeType
41
45
  include Singleton
@@ -52,6 +56,12 @@ module Braid
52
56
  def self.untyped
53
57
  FAKE_TYPE
54
58
  end
59
+ def self.noreturn
60
+ FAKE_TYPE
61
+ end
62
+ def self.any(*types)
63
+ FAKE_TYPE
64
+ end
55
65
  Boolean = FAKE_TYPE
56
66
  module Array
57
67
  def self.[](type)
data/lib/braid/version.rb CHANGED
@@ -1,3 +1,6 @@
1
+ # Ditto the comment in `check_gem.rb` regarding the `typed` sigil.
2
+ # typed: false
3
+
1
4
  module Braid
2
- VERSION = '1.1.7'.freeze
5
+ VERSION = '1.1.9'.freeze
3
6
  end
data/lib/braid.rb CHANGED
@@ -12,14 +12,13 @@ module Braid
12
12
  # See the background in the "Supported environments" section of README.md.
13
13
  #
14
14
  # The newest Git feature that Braid is currently known to rely on is
15
- # `receive.denyCurrentBranch = updateInstead` (in
16
- # spec/integration/push_spec.rb), which was added in Git 2.3.0 (in 2015). It
15
+ # `git ls-remote --symref`, which was added in Git 2.8.0 (in 2016). It
17
16
  # doesn't seem worth even a small amount of work to remove that dependency and
18
17
  # support even older versions of Git. So set that as the declared requirement
19
18
  # for now. In general, a reasonable approach might be to try to support the
20
19
  # oldest version of Git in current "long-term support" versions of popular OS
21
20
  # distributions.
22
- REQUIRED_GIT_VERSION = '2.3.0'
21
+ REQUIRED_GIT_VERSION = '2.8.0'
23
22
 
24
23
  @verbose = T.let(false, T::Boolean)
25
24
 
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.7
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-15 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
@@ -97,6 +97,20 @@ dependencies:
97
97
  - - ">="
98
98
  - !ruby/object:Gem::Version
99
99
  version: '0'
100
+ - !ruby/object:Gem::Dependency
101
+ name: irb
102
+ requirement: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ type: :development
108
+ prerelease: false
109
+ version_requirements: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
100
114
  description: A simple tool for tracking vendor branches in git.
101
115
  email: evil@che.lu norbert.crombach@primetheory.org peter@realityforge.org matt@mattmccutchen.net
102
116
  executables:
@@ -128,7 +142,7 @@ files:
128
142
  homepage: https://github.com/cristibalan/braid
129
143
  licenses: []
130
144
  metadata: {}
131
- post_install_message:
145
+ post_install_message:
132
146
  rdoc_options:
133
147
  - "--line-numbers"
134
148
  - "--inline-source"
@@ -148,8 +162,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
148
162
  - !ruby/object:Gem::Version
149
163
  version: '0'
150
164
  requirements: []
151
- rubygems_version: 3.2.33
152
- signing_key:
165
+ rubygems_version: 3.1.4
166
+ signing_key:
153
167
  specification_version: 4
154
168
  summary: A simple tool for tracking vendor branches in git.
155
169
  test_files: []