braid 1.1.6 → 1.1.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/exe/braid +11 -0
  3. data/lib/braid/check_gem.rb +58 -0
  4. data/lib/braid/command.rb +13 -0
  5. data/lib/braid/commands/diff.rb +1 -1
  6. data/lib/braid/config.rb +28 -4
  7. data/{bin/braid → lib/braid/main.rb} +1 -16
  8. data/lib/braid/mirror.rb +110 -23
  9. data/lib/braid/operations.rb +5 -4
  10. data/lib/braid/operations_lite.rb +19 -1
  11. data/lib/braid/sorbet/fake_runtime.rb +68 -0
  12. data/lib/braid/sorbet/setup.rb +18 -0
  13. data/lib/braid/version.rb +1 -1
  14. data/lib/braid.rb +21 -0
  15. metadata +12 -75
  16. data/.gitignore +0 -16
  17. data/.travis.yml +0 -15
  18. data/CONTRIBUTING.md +0 -24
  19. data/Gemfile +0 -3
  20. data/README.md +0 -234
  21. data/Rakefile +0 -12
  22. data/_config.yml +0 -1
  23. data/braid.gemspec +0 -35
  24. data/braids-json.schema.json +0 -91
  25. data/config_versions.md +0 -58
  26. data/spec/config_spec.rb +0 -59
  27. data/spec/fixtures/shiny/README +0 -3
  28. data/spec/fixtures/shiny/other-skit/layout.liquid +0 -219
  29. data/spec/fixtures/shiny/skit-layout.liquid.test +0 -2
  30. data/spec/fixtures/shiny/skit1.test +0 -2
  31. data/spec/fixtures/shiny-conf-1.0.9-lock/.braids.json +0 -10
  32. data/spec/fixtures/shiny-conf-1.0.9-lock/expected.braids.json +0 -9
  33. data/spec/fixtures/shiny-conf-1.0.9-lock/skit1/layouts/layout.liquid +0 -219
  34. data/spec/fixtures/shiny-conf-1.0.9-lock/skit1/preview.png +0 -0
  35. data/spec/fixtures/shiny-conf-breaking-changes/.braids +0 -14
  36. data/spec/fixtures/shiny-conf-breaking-changes/Spoon-Knife/README.md +0 -9
  37. data/spec/fixtures/shiny-conf-breaking-changes/Spoon-Knife/index.html +0 -20
  38. data/spec/fixtures/shiny-conf-breaking-changes/Spoon-Knife/styles.css +0 -17
  39. data/spec/fixtures/shiny-conf-breaking-changes/expected.braids.json +0 -10
  40. data/spec/fixtures/shiny-conf-breaking-changes/skit1/layouts/layout.liquid +0 -219
  41. data/spec/fixtures/shiny-conf-breaking-changes/skit1/preview.png +0 -0
  42. data/spec/fixtures/shiny-conf-future/.braids.json +0 -10
  43. data/spec/fixtures/shiny-conf-future/skit1/layouts/layout.liquid +0 -219
  44. data/spec/fixtures/shiny-conf-future/skit1/preview.png +0 -0
  45. data/spec/fixtures/shiny-conf-json-old-name/.braids +0 -9
  46. data/spec/fixtures/shiny-conf-json-old-name/expected.braids.json +0 -10
  47. data/spec/fixtures/shiny-conf-json-old-name/skit1/layouts/layout.liquid +0 -219
  48. data/spec/fixtures/shiny-conf-json-old-name/skit1/preview.png +0 -0
  49. data/spec/fixtures/shiny-conf-yaml/.braids +0 -8
  50. data/spec/fixtures/shiny-conf-yaml/expected.braids.json +0 -10
  51. data/spec/fixtures/shiny-conf-yaml/skit1/layouts/layout.liquid +0 -219
  52. data/spec/fixtures/shiny-conf-yaml/skit1/preview.png +0 -0
  53. data/spec/fixtures/shiny_skit1.2_merged/layouts/layout.liquid +0 -223
  54. data/spec/fixtures/shiny_skit1.2_merged/preview.png +0 -0
  55. data/spec/fixtures/shiny_skit1_conflicting/layouts/layout.liquid +0 -221
  56. data/spec/fixtures/shiny_skit1_conflicting/preview.png +0 -0
  57. data/spec/fixtures/shiny_skit1_mergeable/layouts/layout.liquid +0 -221
  58. data/spec/fixtures/shiny_skit1_mergeable/preview.png +0 -0
  59. data/spec/fixtures/skit1/layouts/layout.liquid +0 -219
  60. data/spec/fixtures/skit1/preview.png +0 -0
  61. data/spec/fixtures/skit1.1/layouts/layout.liquid +0 -219
  62. data/spec/fixtures/skit1.1_with_filter/.gitattributes +0 -1
  63. data/spec/fixtures/skit1.1_with_filter/layouts/layout.liquid +0 -219
  64. data/spec/fixtures/skit1.1_with_filter/preview.png +0 -0
  65. data/spec/fixtures/skit1.1x/layouts/layout.liquid +0 -219
  66. data/spec/fixtures/skit1.2/layouts/layout.liquid +0 -221
  67. data/spec/fixtures/skit1.3/layouts/README.md +0 -1
  68. data/spec/fixtures/skit1.3/layouts/layout.liquid +0 -221
  69. data/spec/fixtures/skit1_with_filter/.gitattributes +0 -1
  70. data/spec/fixtures/skit1_with_filter/layouts/layout.liquid +0 -219
  71. data/spec/fixtures/skit1_with_filter/preview.png +0 -0
  72. data/spec/integration/adding_spec.rb +0 -230
  73. data/spec/integration/config_versioning_spec.rb +0 -222
  74. data/spec/integration/diff_spec.rb +0 -597
  75. data/spec/integration/integration_helper.rb +0 -129
  76. data/spec/integration/push_spec.rb +0 -399
  77. data/spec/integration/remove_spec.rb +0 -81
  78. data/spec/integration/status_spec.rb +0 -165
  79. data/spec/integration/updating_spec.rb +0 -487
  80. data/spec/mirror_spec.rb +0 -119
  81. data/spec/operations_spec.rb +0 -66
  82. data/spec/test_helper.rb +0 -19
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4ecb4234a451a2abc3ce39492543d198319a5aefe034caf7cbf377a3994aa463
4
- data.tar.gz: 716777da7ce4fb5b72213838390d009b5046b0331ffdfbb8dea32fe3267e312d
3
+ metadata.gz: ad14f6dd7a5d428c5796bbd19039aae6e59f5803d93da3889fe0a92a8afd032b
4
+ data.tar.gz: 9d6bcfd73c5a306f853e6b9653edda5aff6a7bbd3541b6ab644bd7b96bc510e6
5
5
  SHA512:
6
- metadata.gz: 52664d57908b3758ff9ae3807727435e74267dda1a66d442ea19316c5f9b2e5b33ab475f6eb73765924a1a6eb0b220b4fe11e7ba18fd5d03c5bfb88e2b5bfec2
7
- data.tar.gz: 23b66af4bc7bd0db3e94449c6342439897f5fc38fd8d0f6c5661a2cbe1c0de0b97ef43b6caf501954eb1ee5b80c306b466d7ea61f0d4a4cd268116b8aad65802
6
+ metadata.gz: 817cd08525921352f103a81f05b6c550e8eaff62fb647c04d0ecb746e6ed425a78ef1bea4a477786675c406b0e23982f36a52eea205278a3b25ccee6e64ecdf5
7
+ data.tar.gz: 76d9e62b1d58a437a8eae77f879b5a66ec6a30cda270d267cc458e621a3f1560fa192f5cc435b5e718c9f093691464d458b049d2e236864d0faea6e801c89629
data/exe/braid ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # I haven't found a clean way to get Sorbet to check this file since the name
4
+ # doesn't end in `.rb`. (If I add `--file bin/braid` to `sorbet/config`, then
5
+ # the Sorbet language server doesn't support using Watchman.) So move all the
6
+ # interesting code to `lib/braid/main.rb` and leave this as a launcher that
7
+ # doesn't have any code worth type checking. ~ Matt 2022-04-02
8
+
9
+ require_relative '../lib/braid/check_gem'
10
+
11
+ require 'braid/main'
@@ -0,0 +1,58 @@
1
+ # Braid has several entry points that run code from Ruby gems (either Braid
2
+ # itself or dependencies such as Sorbet) and expect to get the correct versions
3
+ # (either the same copy of Braid or versions of dependencies consistent with the
4
+ # gemspec or Gemfile, as applicable). This currently applies to the following
5
+ # entry points:
6
+ #
7
+ # - The Braid CLI launcher: `exe/braid`
8
+ # - The Braid test suite under `spec`
9
+ # - The Rakefile: Has a target to run the Sorbet type checker and wants the
10
+ # correct version for the current version of Braid.
11
+ #
12
+ # If these entry points are invoked without first activating the correct gems,
13
+ # then other globally installed versions might be used by default, leading to
14
+ # unintended behavior. So we have each of these entry points check that a Braid
15
+ # gem is active in the current Ruby interpreter _and_ the entry point is part of
16
+ # that same Braid gem. If so, we can assume that the correct versions of
17
+ # dependencies are active as well.
18
+ #
19
+ # To implement the check, the entry point file loads its corresponding copy of
20
+ # check_gem.rb via `require_relative`, and that copy of check_gem.rb checks that
21
+ # it is part of the active Braid gem. Note that loading check_gem.rb via
22
+ # `require 'lib/braid/check_gem'` would defeat the purpose, since it would
23
+ # activate whatever Braid gem is visible and then check that that gem's copy of
24
+ # check_gem.rb is part of the active Braid gem (which it would always be), not
25
+ # whether the _caller_ is part of the active Braid gem.
26
+ #
27
+ # Common means of activating the correct gems before invoking an entry point
28
+ # include:
29
+ #
30
+ # - Bundler: Used when developing Braid itself and possibly by some projects
31
+ # that use Braid. It modifies the environment so that every Ruby subprocess
32
+ # activates the gems from the Gemfile.lock.
33
+ # - A wrapper for `exe/braid` generated by `gem install --wrappers`.
34
+ #
35
+ # Note that we don't have every `.rb` file that is part of Braid load
36
+ # check_gem.rb: that would be too invasive and only helps in the case where a
37
+ # user manually added Braid's `lib` dir to the load path, which is a mistake
38
+ # that a reasonable user is much less likely to make than just running an entry
39
+ # point without the proper setup.
40
+ #
41
+ # TODO: Is there a more standard way to do this check? One would think it would
42
+ # be potentially applicable to any Ruby project, though maybe most don't care as
43
+ # much as we do about catching the problem up front.
44
+
45
+ braid_spec = Gem.loaded_specs['braid']
46
+ if braid_spec.nil? || __FILE__ != braid_spec.gem_dir + '/lib/braid/check_gem.rb'
47
+ STDERR.puts <<-MSG
48
+ Error: The RubyGems environment is not set up correctly for Braid.
49
+ - If you're using a copy of Braid installed globally via 'gem install' or
50
+ similar, use the 'braid' wrapper script (which is typically installed at a
51
+ location like ~/.gem/ruby/bin/braid depending on your environment) instead of
52
+ running 'exe/braid' directly.
53
+ - If you're using a copy of Braid managed by Bundler (including when developing
54
+ Braid itself), prepend 'bundle exec' to your command or use a binstub
55
+ generated by 'bundle binstubs'.
56
+ MSG
57
+ exit(1)
58
+ end
data/lib/braid/command.rb CHANGED
@@ -1,5 +1,8 @@
1
+ # typed: true
1
2
  module Braid
2
3
  class Command
4
+ extend T::Sig
5
+
3
6
  class InvalidRevision < BraidError
4
7
  end
5
8
 
@@ -23,10 +26,12 @@ module Braid
23
26
  exit(1)
24
27
  end
25
28
 
29
+ sig {params(str: String).void}
26
30
  def self.msg(str)
27
31
  puts "Braid: #{str}"
28
32
  end
29
33
 
34
+ sig {params(str: String).void}
30
35
  def msg(str)
31
36
  self.class.msg(str)
32
37
  end
@@ -35,16 +40,19 @@ module Braid
35
40
  @config ||= Config.new({'mode' => config_mode})
36
41
  end
37
42
 
43
+ sig {returns(T::Boolean)}
38
44
  def verbose?
39
45
  Braid.verbose
40
46
  end
41
47
 
48
+ sig {returns(T::Boolean)}
42
49
  def force?
43
50
  Braid.force
44
51
  end
45
52
 
46
53
  private
47
54
 
55
+ sig {returns(Config::ConfigMode)}
48
56
  def config_mode
49
57
  Config::MODE_MAY_WRITE
50
58
  end
@@ -63,14 +71,17 @@ module Braid
63
71
  git.remote_rm(mirror.remote) unless options['keep']
64
72
  end
65
73
 
74
+ sig {returns(T::Boolean)}
66
75
  def use_local_cache?
67
76
  Braid.use_local_cache
68
77
  end
69
78
 
79
+ sig {void}
70
80
  def self.verify_git_version!
71
81
  git.require_version!(REQUIRED_GIT_VERSION)
72
82
  end
73
83
 
84
+ sig {void}
74
85
  def self.check_working_dir!
75
86
  # If we aren't in a git repository at all, git.is_inside_worktree will
76
87
  # propagate a "fatal: Not a git repository" ShellException.
@@ -82,6 +93,7 @@ module Braid
82
93
  end
83
94
  end
84
95
 
96
+ sig {void}
85
97
  def bail_on_local_changes!
86
98
  git.ensure_clean!
87
99
  end
@@ -100,6 +112,7 @@ module Braid
100
112
  end
101
113
  end
102
114
 
115
+ sig {void}
103
116
  def add_config_file
104
117
  git.rm(OLD_CONFIG_FILE) if File.exist?(OLD_CONFIG_FILE)
105
118
  git.add(CONFIG_FILE)
@@ -35,7 +35,7 @@ module Braid
35
35
 
36
36
  # XXX: Warn if the user specifies file paths that are outside the
37
37
  # mirror? Currently, they just won't match anything.
38
- git.diff_to_stdout(*mirror.diff_args(*options['git_diff_args']))
38
+ git.diff_to_stdout(*mirror.diff_args(options['git_diff_args']))
39
39
 
40
40
  clear_remote(mirror, options)
41
41
  end
data/lib/braid/config.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # typed: true
1
2
  require 'yaml'
2
3
  require 'json'
3
4
  require 'yaml/store'
@@ -48,6 +49,10 @@ require 'yaml/store'
48
49
 
49
50
  module Braid
50
51
  class Config
52
+ extend T::Sig
53
+
54
+ # TODO (typing): Migrate to T::Enum?
55
+ ConfigMode = T.type_alias { Integer }
51
56
 
52
57
  MODE_UPGRADE = 1
53
58
  MODE_READ_ONLY = 2
@@ -56,11 +61,13 @@ module Braid
56
61
  CURRENT_CONFIG_VERSION = 1
57
62
 
58
63
  class PathAlreadyInUse < BraidError
64
+ sig {returns(String)}
59
65
  def message
60
66
  "path already in use: #{super}"
61
67
  end
62
68
  end
63
69
  class MirrorDoesNotExist < BraidError
70
+ sig {returns(String)}
64
71
  def message
65
72
  "mirror does not exist: #{super}"
66
73
  end
@@ -70,9 +77,15 @@ module Braid
70
77
  end
71
78
 
72
79
  # For upgrade-config command only. XXX: Ideally would be immutable.
73
- attr_reader :config_version, :config_existed, :breaking_change_descs
80
+ sig {returns(Integer)}
81
+ attr_reader :config_version
82
+ sig {returns(T::Boolean)}
83
+ attr_reader :config_existed
84
+ sig {returns(T::Array[String])}
85
+ attr_reader :breaking_change_descs
74
86
 
75
87
  # options: config_file, old_config_files, mode
88
+ sig {params(options: T.untyped).void}
76
89
  def initialize(options = {})
77
90
  @config_file = options['config_file'] || CONFIG_FILE
78
91
  old_config_files = options['old_config_files'] || [OLD_CONFIG_FILE]
@@ -150,6 +163,7 @@ MSG
150
163
 
151
164
  end
152
165
 
166
+ sig {params(url: String, options: T.untyped).returns(Mirror)}
153
167
  def add_from_options(url, options)
154
168
  mirror = Mirror.new_from_options(url, options)
155
169
 
@@ -157,33 +171,39 @@ MSG
157
171
  mirror
158
172
  end
159
173
 
174
+ sig {returns(T::Array[Mirror])}
160
175
  def mirrors
161
176
  @db.keys
162
177
  end
163
178
 
179
+ sig {params(path: String).returns(T.nilable(Mirror))}
164
180
  def get(path)
165
181
  key = path.to_s.sub(/\/$/, '')
166
182
  attributes = @db[key]
167
183
  attributes ? Mirror.new(path, attributes) : nil
168
184
  end
169
185
 
186
+ sig {params(path: String).returns(Mirror)}
170
187
  def get!(path)
171
188
  mirror = get(path)
172
189
  raise MirrorDoesNotExist, path unless mirror
173
190
  mirror
174
191
  end
175
192
 
193
+ sig {params(mirror: Mirror).void}
176
194
  def add(mirror)
177
195
  raise PathAlreadyInUse, mirror.path if get(mirror.path)
178
196
  write_mirror(mirror)
179
197
  write_db
180
198
  end
181
199
 
200
+ sig {params(mirror: Mirror).void}
182
201
  def remove(mirror)
183
202
  @db.delete(mirror.path)
184
203
  write_db
185
204
  end
186
205
 
206
+ sig {params(mirror: Mirror).void}
187
207
  def update(mirror)
188
208
  raise MirrorDoesNotExist, mirror.path unless get(mirror.path)
189
209
  write_mirror(mirror)
@@ -191,6 +211,7 @@ MSG
191
211
  end
192
212
 
193
213
  # Public for upgrade-config command only.
214
+ sig {void}
194
215
  def write_db
195
216
  new_db = {}
196
217
  @db.keys.sort.each do |key|
@@ -211,11 +232,12 @@ MSG
211
232
 
212
233
  private
213
234
 
235
+ sig {params(config_file: String, old_config_files: T::Array[String]).returns(T.untyped)}
214
236
  def load_config(config_file, old_config_files)
215
237
  (old_config_files + [config_file]).each do |file|
216
238
  next unless File.exist?(file)
217
239
  begin
218
- store = YAML::Store.new(file)
240
+ store = T.let(YAML::Store, T.untyped).new(file)
219
241
  data = {}
220
242
  store.transaction(true) do
221
243
  store.roots.each do |path|
@@ -228,15 +250,17 @@ MSG
228
250
  return data if data
229
251
  end
230
252
  end
231
- return nil
253
+ nil
232
254
  end
233
255
 
256
+ sig {params(mirror: Mirror).void}
234
257
  def write_mirror(mirror)
235
258
  @db[mirror.path] = clean_attributes(mirror.attributes)
236
259
  end
237
260
 
261
+ sig {params(hash: T::Hash[String, T.untyped]).returns(T::Hash[String, T.untyped])}
238
262
  def clean_attributes(hash)
239
- hash.reject { |k, v| v.nil? }
263
+ hash.reject { |_, v| v.nil? }
240
264
  end
241
265
  end
242
266
  end
@@ -1,18 +1,3 @@
1
- #!/usr/bin/env ruby
2
-
3
- # Duplicated from Braid::Command.run. :(
4
- def die(msg)
5
- puts "Braid: Error: #{msg}"
6
- exit(1)
7
- end
8
-
9
- # If we assume Ruby >= 2.0, we can use __dir__.
10
- libdir = File.expand_path(File.dirname(File.realpath(__FILE__)) + '/../lib')
11
- unless File.exists?(libdir)
12
- # Don't silently fall back to a different globally installed copy of Braid!
13
- die "Cannot find Braid's 'lib' directory."
14
- end
15
- $LOAD_PATH.unshift(libdir)
16
1
  require 'braid'
17
2
 
18
3
  require 'rubygems'
@@ -138,7 +123,7 @@ Main {
138
123
 
139
124
  mixin :optional_local_path, :option_verbose, :option_keep_remote
140
125
 
141
- synopsis (Main::Usage.default_synopsis(self) + ' [-- git_diff_arg*]')
126
+ synopsis(Main::Usage.default_synopsis(self) + ' [-- git_diff_arg*]')
142
127
 
143
128
  run {
144
129
  if @argv.length > 0 && @argv[0] == '--'
data/lib/braid/mirror.rb CHANGED
@@ -1,21 +1,27 @@
1
+ # typed: strict
1
2
  module Braid
2
3
  class Mirror
4
+ extend T::Sig
5
+
3
6
  # Since Braid 1.1.0, the attributes are written to .braids.json in this
4
7
  # canonical order. For now, the order is chosen to match what Braid 1.0.22
5
8
  # produced for newly added mirrors.
6
- ATTRIBUTES = %w(url branch path tag revision)
9
+ ATTRIBUTES = T.let(%w(url branch path tag revision), T::Array[String])
7
10
 
8
11
  class UnknownType < BraidError
12
+ sig {returns(String)}
9
13
  def message
10
14
  "unknown type: #{super}"
11
15
  end
12
16
  end
13
17
  class PathRequired < BraidError
18
+ sig {returns(String)}
14
19
  def message
15
20
  'path is required'
16
21
  end
17
22
  end
18
23
  class NoTagAndBranch < BraidError
24
+ sig {returns(String)}
19
25
  def message
20
26
  'can not specify both tag and branch configuration'
21
27
  end
@@ -23,11 +29,19 @@ module Braid
23
29
 
24
30
  include Operations::VersionControl
25
31
 
26
- attr_reader :path, :attributes
32
+ sig {returns(String)}
33
+ attr_reader :path
34
+
35
+ # It's going to take significant refactoring to be able to give this a type.
36
+ sig {returns(T::Hash[String, T.untyped])}
37
+ attr_reader :attributes
38
+
39
+ BreakingChangeCallback = T.type_alias { T.proc.params(arg0: String).void }
27
40
 
41
+ sig {params(path: String, attributes: T::Hash[String, T.untyped], breaking_change_cb: BreakingChangeCallback).void}
28
42
  def initialize(path, attributes = {}, breaking_change_cb = DUMMY_BREAKING_CHANGE_CB)
29
- @path = path.sub(/\/$/, '')
30
- @attributes = attributes.dup
43
+ @path = T.let(path.sub(/\/$/, ''), String)
44
+ @attributes = T.let(attributes.dup, T::Hash[String, T.untyped])
31
45
 
32
46
  # Not that it's terribly important to check for such an old feature. This
33
47
  # is mainly to demonstrate the RemoveMirrorDueToBreakingChange mechanism
@@ -65,6 +79,7 @@ DESC
65
79
  @attributes.delete('squashed')
66
80
  end
67
81
 
82
+ sig {params(url: String, options: T.untyped).returns(Mirror)}
68
83
  def self.new_from_options(url, options = {})
69
84
  url = url.sub(/\/$/, '')
70
85
 
@@ -82,14 +97,17 @@ DESC
82
97
  self.new(path, attributes)
83
98
  end
84
99
 
100
+ sig {params(comparison: Mirror).returns(T::Boolean)}
85
101
  def ==(comparison)
86
102
  path == comparison.path && attributes == comparison.attributes
87
103
  end
88
104
 
105
+ sig {returns(T::Boolean)}
89
106
  def locked?
90
107
  branch.nil? && tag.nil?
91
108
  end
92
109
 
110
+ sig {params(commit: String).returns(T::Boolean)}
93
111
  def merged?(commit)
94
112
  # tip from spearce in #git:
95
113
  # `test z$(git merge-base A B) = z$(git rev-parse --verify A)`
@@ -97,6 +115,9 @@ DESC
97
115
  !!base_revision && git.merge_base(commit, base_revision) == commit
98
116
  end
99
117
 
118
+ # We'll probably call the return type something like
119
+ # Braid::Operations::Git::TreeItem.
120
+ sig {params(revision: String).returns(T.untyped)}
100
121
  def upstream_item_for_revision(revision)
101
122
  git.get_tree_item(revision, self.remote_path)
102
123
  end
@@ -106,6 +127,7 @@ DESC
106
127
  # user-specified arguments. Having the caller run "git diff" is convenient
107
128
  # for now but violates encapsulation a little; we may have to reorganize the
108
129
  # code in order to add features.
130
+ sig {params(user_args: T::Array[String]).returns(T::Array[String])}
109
131
  def diff_args(user_args = [])
110
132
  upstream_item = upstream_item_for_revision(base_revision)
111
133
 
@@ -139,7 +161,7 @@ DESC
139
161
  # before we add the basenames.
140
162
  return [
141
163
  '--relative=' + path,
142
- '--src-prefix=a/' + File.basename(remote_path),
164
+ '--src-prefix=a/' + File.basename(T.must(remote_path)),
143
165
  '--dst-prefix=b/' + File.basename(path),
144
166
  base_tree,
145
167
  # user_args may contain options, which must come before paths.
@@ -156,6 +178,7 @@ DESC
156
178
  end
157
179
 
158
180
  # Precondition: the remote for this mirror is set up.
181
+ sig {returns(String)}
159
182
  def diff
160
183
  fetch_base_revision_if_missing
161
184
  git.diff(diff_args)
@@ -166,6 +189,7 @@ DESC
166
189
  # (https://github.com/cristibalan/braid/issues/71). Mitigate this for
167
190
  # `braid diff` and other commands that need the diff by skipping the fetch
168
191
  # if the base revision is already present in the repository.
192
+ sig {void}
169
193
  def fetch_base_revision_if_missing
170
194
  begin
171
195
  # Without ^{commit}, this will happily pass back an object hash even if
@@ -176,74 +200,133 @@ DESC
176
200
  end
177
201
  end
178
202
 
203
+ sig {void}
179
204
  def fetch
180
205
  git_cache.fetch(url) if cached?
181
206
  git.fetch(remote)
182
207
  end
183
208
 
209
+ sig {returns(T::Boolean)}
184
210
  def cached?
185
211
  git.remote_url(remote) == cached_url
186
212
  end
187
213
 
214
+ sig {returns(String)}
188
215
  def base_revision
189
- if 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
190
221
  git.rev_parse(revision)
191
222
  else
192
223
  inferred_revision
193
224
  end
194
225
  end
195
226
 
227
+ sig {returns(String)}
196
228
  def local_ref
197
229
  return "#{self.remote}/#{self.branch}" unless self.branch.nil?
198
230
  return "tags/#{self.tag}" unless self.tag.nil?
199
- self.revision
231
+ # TODO (typing): Remove this `T.must` if we make `revision` non-nilable.
232
+ T.must(self.revision)
200
233
  end
201
234
 
235
+ # FIXME: The return value is bogus if this mirror has neither a branch nor a
236
+ # tag. It looks like this leads to a real bug affecting `braid push` when
237
+ # `--branch` is specified. File the bug or just fix it.
238
+ sig {returns(String)}
202
239
  def remote_ref
203
240
  self.branch.nil? ? "+refs/tags/#{self.tag}" : "+refs/heads/#{self.branch}"
204
241
  end
205
242
 
243
+ # Accessors for all the attributes listed in `ATTRIBUTES` and stored in
244
+ # `self.attributes`. Most of the accessors use the same names as the
245
+ # underlying attributes. The exception is the `path` attribute, whose
246
+ # accessor is named `remote_path` to avoid a conflict with the `path`
247
+ # accessor for the local path.
248
+ #
249
+ # TODO: Can we reduce this boilerplate and still type the accessors
250
+ # statically? If we move the config attributes to instance variables of the
251
+ # Mirror object, then we can use `attr_accessor`, but we'd have to somehow
252
+ # accommodate existing call sites that access `self.attributes` as a whole.
253
+
254
+ sig {returns(String)}
255
+ def url
256
+ self.attributes['url']
257
+ end
258
+
259
+ sig {params(new_value: String).void}
260
+ def url=(new_value)
261
+ self.attributes['url'] = new_value
262
+ end
263
+
264
+ sig {returns(T.nilable(String))}
265
+ def branch
266
+ self.attributes['branch']
267
+ end
268
+
269
+ sig {params(new_value: T.nilable(String)).void}
270
+ def branch=(new_value)
271
+ self.attributes['branch'] = new_value
272
+ end
273
+
274
+ sig {returns(T.nilable(String))}
206
275
  def remote_path
207
276
  self.attributes['path']
208
277
  end
209
278
 
279
+ sig {params(remote_path: T.nilable(String)).void}
210
280
  def remote_path=(remote_path)
211
281
  self.attributes['path'] = remote_path
212
282
  end
213
283
 
284
+ sig {returns(T.nilable(String))}
285
+ def tag
286
+ self.attributes['tag']
287
+ end
288
+
289
+ sig {params(new_value: T.nilable(String)).void}
290
+ def tag=(new_value)
291
+ self.attributes['tag'] = new_value
292
+ end
293
+
294
+ # The revision may be nil in the middle of `braid add`.
295
+ # TODO (typing): Look into restructuring `braid add` to avoid this?
296
+ sig {returns(T.nilable(String))}
297
+ def revision
298
+ self.attributes['revision']
299
+ end
300
+
301
+ sig {params(new_value: String).void}
302
+ def revision=(new_value)
303
+ self.attributes['revision'] = new_value
304
+ end
305
+
306
+ sig {returns(String)}
214
307
  def cached_url
215
308
  git_cache.path(url)
216
309
  end
217
310
 
311
+ sig {returns(String)}
218
312
  def remote
219
313
  "#{branch || tag || 'revision'}/braid/#{path}".gsub(/\/\./, '/_')
220
314
  end
221
315
 
222
316
  private
223
317
 
224
- DUMMY_BREAKING_CHANGE_CB = lambda { |desc|
318
+ DUMMY_BREAKING_CHANGE_CB = T.let(lambda { |desc|
225
319
  raise InternalError, 'Instantiated a mirror using an unsupported ' +
226
320
  'feature outside of configuration loading.'
227
- }
228
-
229
- def method_missing(name, *args)
230
- if ATTRIBUTES.find { |attribute| name.to_s =~ /^(#{attribute})(=)?$/ }
231
- if $2
232
- attributes[$1] = args[0]
233
- else
234
- attributes[$1]
235
- end
236
- else
237
- raise NameError, "unknown attribute `#{name}'"
238
- end
239
- end
321
+ }, BreakingChangeCallback)
240
322
 
323
+ sig {returns(T.nilable(String))}
241
324
  def inferred_revision
242
325
  local_commits = git.rev_list('HEAD', "-- #{path}").split("\n")
243
326
  remote_hashes = git.rev_list("--pretty=format:\"%T\"", remote).split('commit ').map do |chunk|
244
327
  chunk.split("\n", 2).map { |value| value.strip }
245
328
  end
246
- hash = nil
329
+ hash = T.let(nil, T.nilable(String))
247
330
  local_commits.each do |local_commit|
248
331
  local_tree = git.tree_hash(path, local_commit)
249
332
  match = remote_hashes.find { |_, remote_tree| local_tree == remote_tree }
@@ -255,12 +338,16 @@ DESC
255
338
  hash
256
339
  end
257
340
 
341
+ # TODO (typing): Return should not be nilable
342
+ sig {params(url: String, remote_path: T.nilable(String)).returns(T.nilable(String))}
258
343
  def self.extract_path_from_url(url, remote_path)
259
344
  if remote_path
260
345
  return File.basename(remote_path)
261
346
  end
262
347
 
263
- return nil unless url
348
+ # Avoid a Sorbet "unreachable code" error.
349
+ # TODO (typing): Fix this properly. Probably just remove this line?
350
+ return nil unless T.let(url, T.nilable(String))
264
351
  name = File.basename(url)
265
352
 
266
353
  if File.extname(name) == '.git'
@@ -61,7 +61,7 @@ module Braid
61
61
 
62
62
  # hax!
63
63
  def version
64
- status, out, err = exec!("#{self.class.command} --version")
64
+ _, out, _ = exec!("#{self.class.command} --version")
65
65
  out.sub(/^.* version/, '').strip.sub(/ .*$/, '').strip
66
66
  end
67
67
 
@@ -166,6 +166,7 @@ module Braid
166
166
 
167
167
  def commit(message, *args)
168
168
  cmd = 'git commit --no-verify'
169
+ message_file = nil
169
170
  if message # allow nil
170
171
  message_file = Tempfile.new('braid_commit')
171
172
  message_file.print("Braid: #{message}")
@@ -303,7 +304,7 @@ module Braid
303
304
  # According to
304
305
  # https://lore.kernel.org/git/e48a281a4d3db0a04c0609fcb8658e4fcc797210.1646166271.git.gitgitgadget@gmail.com/,
305
306
  # `--prefix=` is valid if the path is empty.
306
- res = invoke(:read_tree, "--prefix=#{path}", update_worktree ? '-u' : '-i', item)
307
+ invoke(:read_tree, "--prefix=#{path}", update_worktree ? '-u' : '-i', item)
307
308
  end
308
309
  end
309
310
 
@@ -370,7 +371,7 @@ module Braid
370
371
  end
371
372
 
372
373
  def status_clean?
373
- status, out, err = exec('git status')
374
+ _, out, _ = exec('git status')
374
375
  !out.split("\n").grep(/nothing to commit/).empty?
375
376
  end
376
377
 
@@ -383,7 +384,7 @@ module Braid
383
384
  end
384
385
 
385
386
  def branch
386
- status, out, err = exec!("git branch | grep '*'")
387
+ _, out, _ = exec!("git branch | grep '*'")
387
388
  out[2..-1]
388
389
  end
389
390
 
@@ -1,9 +1,27 @@
1
+ # typed: strict
2
+
3
+ require 'braid/sorbet/setup'
4
+
1
5
  # One helper that is shared with the integration test harness and has no
2
6
  # dependencies on the rest of Braid.
3
7
  module Braid
4
8
  module Operations
9
+ extend T::Sig
10
+
5
11
  # Want to use https://github.com/thoughtbot/climate_control ?
6
- def self.with_modified_environment(dict)
12
+ #
13
+ # We have to declare the `&blk` parameter in order to reference it in the
14
+ # type annotation, even though the body doesn't use it. This causes the
15
+ # block to be converted to a proc at runtime, which has some performance
16
+ # cost (probably not important in the context of Braid). TODO: Find a way
17
+ # to avoid the performance cost? File a Sorbet enhancement request?
18
+ sig {
19
+ type_parameters(:R).params(
20
+ dict: T::Hash[String, String],
21
+ blk: T.proc.returns(T.type_parameter(:R))
22
+ ).returns(T.type_parameter(:R))
23
+ }
24
+ def self.with_modified_environment(dict, &blk)
7
25
  orig_dict = {}
8
26
  dict.each { |name, value|
9
27
  orig_dict[name] = ENV[name]