braid 1.1.6 → 1.1.7

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.
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]