braid 1.1.5 → 1.1.8
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 +4 -4
- data/exe/braid +11 -0
- data/lib/braid/check_gem.rb +65 -0
- data/lib/braid/command.rb +18 -0
- data/lib/braid/commands/add.rb +41 -1
- data/lib/braid/commands/diff.rb +2 -1
- data/lib/braid/commands/push.rb +1 -0
- data/lib/braid/commands/remove.rb +1 -0
- data/lib/braid/commands/setup.rb +1 -0
- data/lib/braid/commands/status.rb +1 -0
- data/lib/braid/commands/update.rb +1 -0
- data/lib/braid/commands/upgrade_config.rb +1 -0
- data/lib/braid/config.rb +28 -4
- data/{bin/braid → lib/braid/main.rb} +16 -17
- data/lib/braid/mirror.rb +111 -24
- data/lib/braid/operations.rb +20 -18
- data/lib/braid/operations_lite.rb +19 -1
- data/lib/braid/sorbet/fake_runtime.rb +75 -0
- data/lib/braid/sorbet/setup.rb +18 -0
- data/lib/braid/version.rb +4 -1
- data/lib/braid.rb +23 -3
- metadata +26 -75
- data/.gitignore +0 -16
- data/.travis.yml +0 -15
- data/CONTRIBUTING.md +0 -24
- data/Gemfile +0 -3
- data/README.md +0 -234
- data/Rakefile +0 -12
- data/_config.yml +0 -1
- data/braid.gemspec +0 -35
- data/braids-json.schema.json +0 -91
- data/config_versions.md +0 -58
- data/spec/config_spec.rb +0 -59
- data/spec/fixtures/shiny/README +0 -3
- data/spec/fixtures/shiny/other-skit/layout.liquid +0 -219
- data/spec/fixtures/shiny/skit-layout.liquid.test +0 -2
- data/spec/fixtures/shiny/skit1.test +0 -2
- data/spec/fixtures/shiny-conf-1.0.9-lock/.braids.json +0 -10
- data/spec/fixtures/shiny-conf-1.0.9-lock/expected.braids.json +0 -9
- data/spec/fixtures/shiny-conf-1.0.9-lock/skit1/layouts/layout.liquid +0 -219
- data/spec/fixtures/shiny-conf-1.0.9-lock/skit1/preview.png +0 -0
- data/spec/fixtures/shiny-conf-breaking-changes/.braids +0 -14
- data/spec/fixtures/shiny-conf-breaking-changes/Spoon-Knife/README.md +0 -9
- data/spec/fixtures/shiny-conf-breaking-changes/Spoon-Knife/index.html +0 -20
- data/spec/fixtures/shiny-conf-breaking-changes/Spoon-Knife/styles.css +0 -17
- data/spec/fixtures/shiny-conf-breaking-changes/expected.braids.json +0 -10
- data/spec/fixtures/shiny-conf-breaking-changes/skit1/layouts/layout.liquid +0 -219
- data/spec/fixtures/shiny-conf-breaking-changes/skit1/preview.png +0 -0
- data/spec/fixtures/shiny-conf-future/.braids.json +0 -10
- data/spec/fixtures/shiny-conf-future/skit1/layouts/layout.liquid +0 -219
- data/spec/fixtures/shiny-conf-future/skit1/preview.png +0 -0
- data/spec/fixtures/shiny-conf-json-old-name/.braids +0 -9
- data/spec/fixtures/shiny-conf-json-old-name/expected.braids.json +0 -10
- data/spec/fixtures/shiny-conf-json-old-name/skit1/layouts/layout.liquid +0 -219
- data/spec/fixtures/shiny-conf-json-old-name/skit1/preview.png +0 -0
- data/spec/fixtures/shiny-conf-yaml/.braids +0 -8
- data/spec/fixtures/shiny-conf-yaml/expected.braids.json +0 -10
- data/spec/fixtures/shiny-conf-yaml/skit1/layouts/layout.liquid +0 -219
- data/spec/fixtures/shiny-conf-yaml/skit1/preview.png +0 -0
- data/spec/fixtures/shiny_skit1.2_merged/layouts/layout.liquid +0 -223
- data/spec/fixtures/shiny_skit1.2_merged/preview.png +0 -0
- data/spec/fixtures/shiny_skit1_conflicting/layouts/layout.liquid +0 -221
- data/spec/fixtures/shiny_skit1_conflicting/preview.png +0 -0
- data/spec/fixtures/shiny_skit1_mergeable/layouts/layout.liquid +0 -221
- data/spec/fixtures/shiny_skit1_mergeable/preview.png +0 -0
- data/spec/fixtures/skit1/layouts/layout.liquid +0 -219
- data/spec/fixtures/skit1/preview.png +0 -0
- data/spec/fixtures/skit1.1/layouts/layout.liquid +0 -219
- data/spec/fixtures/skit1.1_with_filter/.gitattributes +0 -1
- data/spec/fixtures/skit1.1_with_filter/layouts/layout.liquid +0 -219
- data/spec/fixtures/skit1.1_with_filter/preview.png +0 -0
- data/spec/fixtures/skit1.1x/layouts/layout.liquid +0 -219
- data/spec/fixtures/skit1.2/layouts/layout.liquid +0 -221
- data/spec/fixtures/skit1.3/layouts/README.md +0 -1
- data/spec/fixtures/skit1.3/layouts/layout.liquid +0 -221
- data/spec/fixtures/skit1_with_filter/.gitattributes +0 -1
- data/spec/fixtures/skit1_with_filter/layouts/layout.liquid +0 -219
- data/spec/fixtures/skit1_with_filter/preview.png +0 -0
- data/spec/integration/adding_spec.rb +0 -230
- data/spec/integration/config_versioning_spec.rb +0 -222
- data/spec/integration/diff_spec.rb +0 -597
- data/spec/integration/integration_helper.rb +0 -129
- data/spec/integration/push_spec.rb +0 -399
- data/spec/integration/remove_spec.rb +0 -81
- data/spec/integration/status_spec.rb +0 -165
- data/spec/integration/updating_spec.rb +0 -487
- data/spec/mirror_spec.rb +0 -119
- data/spec/operations_spec.rb +0 -66
- data/spec/test_helper.rb +0 -19
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
|
-
|
|
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,13 +79,14 @@ 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
|
|
|
71
86
|
raise NoTagAndBranch if options['tag'] && options['branch']
|
|
72
87
|
|
|
73
88
|
tag = options['tag']
|
|
74
|
-
branch = options['branch']
|
|
89
|
+
branch = options['branch']
|
|
75
90
|
|
|
76
91
|
path = (options['path'] || extract_path_from_url(url, options['remote_path'])).sub(/\/$/, '')
|
|
77
92
|
raise PathRequired unless path
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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'
|
data/lib/braid/operations.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
|
|
1
3
|
require 'singleton'
|
|
2
4
|
require 'rubygems'
|
|
3
5
|
require 'tempfile'
|
|
@@ -56,12 +58,12 @@ module Braid
|
|
|
56
58
|
include Singleton
|
|
57
59
|
|
|
58
60
|
def self.command;
|
|
59
|
-
name.split('::').last.downcase;
|
|
61
|
+
T.unsafe(name).split('::').last.downcase;
|
|
60
62
|
end
|
|
61
63
|
|
|
62
64
|
# hax!
|
|
63
65
|
def version
|
|
64
|
-
|
|
66
|
+
_, out, _ = exec!("#{self.class.command} --version")
|
|
65
67
|
out.sub(/^.* version/, '').strip.sub(/ .*$/, '').strip
|
|
66
68
|
end
|
|
67
69
|
|
|
@@ -89,6 +91,9 @@ module Braid
|
|
|
89
91
|
end
|
|
90
92
|
|
|
91
93
|
def method_missing(name, *args)
|
|
94
|
+
# We have to use this rather than `T.unsafe` because `invoke` is
|
|
95
|
+
# private. See https://sorbet.org/docs/type-assertions#tbind.
|
|
96
|
+
T.bind(self, T.untyped)
|
|
92
97
|
invoke(name, *args)
|
|
93
98
|
end
|
|
94
99
|
|
|
@@ -138,12 +143,7 @@ module Braid
|
|
|
138
143
|
# 'MERGE_MSG'), taking into account worktree configuration. The returned
|
|
139
144
|
# path may be absolute or relative to the current working directory.
|
|
140
145
|
def repo_file_path(path)
|
|
141
|
-
|
|
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
|
|
146
|
+
invoke(:rev_parse, '--git-path', path)
|
|
147
147
|
end
|
|
148
148
|
|
|
149
149
|
# If the current directory is not inside a git repository at all, this
|
|
@@ -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}")
|
|
@@ -272,7 +273,7 @@ module Braid
|
|
|
272
273
|
if path.nil? || path == ''
|
|
273
274
|
tree
|
|
274
275
|
else
|
|
275
|
-
m = /^([^ ]*) ([^ ]*) ([^\t]*)\t.*$/.match(invoke(:ls_tree, tree, path))
|
|
276
|
+
m = T.must(/^([^ ]*) ([^ ]*) ([^\t]*)\t.*$/.match(invoke(:ls_tree, tree, path)))
|
|
276
277
|
mode = m[1]
|
|
277
278
|
type = m[2]
|
|
278
279
|
hash = m[3]
|
|
@@ -292,16 +293,16 @@ module Braid
|
|
|
292
293
|
# file).
|
|
293
294
|
def add_item_to_index(item, path, update_worktree)
|
|
294
295
|
if item.is_a?(BlobWithMode)
|
|
295
|
-
|
|
296
|
-
# wasn't added until 2.0.0.
|
|
297
|
-
invoke(:update_index, '--add', '--cacheinfo', item.mode, item.hash, path)
|
|
296
|
+
invoke(:update_index, '--add', '--cacheinfo', "#{item.mode},#{item.hash},#{path}")
|
|
298
297
|
if update_worktree
|
|
299
298
|
# XXX If this fails, we've already updated the index.
|
|
300
299
|
invoke(:checkout_index, path)
|
|
301
300
|
end
|
|
302
301
|
else
|
|
303
|
-
#
|
|
304
|
-
|
|
302
|
+
# According to
|
|
303
|
+
# https://lore.kernel.org/git/e48a281a4d3db0a04c0609fcb8658e4fcc797210.1646166271.git.gitgitgadget@gmail.com/,
|
|
304
|
+
# `--prefix=` is valid if the path is empty.
|
|
305
|
+
invoke(:read_tree, "--prefix=#{path}", update_worktree ? '-u' : '-i', item)
|
|
305
306
|
end
|
|
306
307
|
end
|
|
307
308
|
|
|
@@ -368,7 +369,7 @@ module Braid
|
|
|
368
369
|
end
|
|
369
370
|
|
|
370
371
|
def status_clean?
|
|
371
|
-
|
|
372
|
+
_, out, _ = exec('git status')
|
|
372
373
|
!out.split("\n").grep(/nothing to commit/).empty?
|
|
373
374
|
end
|
|
374
375
|
|
|
@@ -381,12 +382,13 @@ module Braid
|
|
|
381
382
|
end
|
|
382
383
|
|
|
383
384
|
def branch
|
|
384
|
-
|
|
385
|
+
_, out, _ = exec!("git branch | grep '*'")
|
|
385
386
|
out[2..-1]
|
|
386
387
|
end
|
|
387
388
|
|
|
388
389
|
def clone(*args)
|
|
389
390
|
# overrides builtin
|
|
391
|
+
T.bind(self, T.untyped) # Ditto the comment in `method_missing`.
|
|
390
392
|
invoke(:clone, *args)
|
|
391
393
|
end
|
|
392
394
|
|
|
@@ -404,11 +406,11 @@ module Braid
|
|
|
404
406
|
dir = path(url)
|
|
405
407
|
|
|
406
408
|
# remove local cache if it was created with --no-checkout
|
|
407
|
-
if File.
|
|
409
|
+
if File.exist?("#{dir}/.git")
|
|
408
410
|
FileUtils.rm_r(dir)
|
|
409
411
|
end
|
|
410
412
|
|
|
411
|
-
if File.
|
|
413
|
+
if File.exist?(dir)
|
|
412
414
|
Dir.chdir(dir) do
|
|
413
415
|
git.fetch
|
|
414
416
|
end
|
|
@@ -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
|
-
|
|
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]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# typed: ignore
|
|
2
|
+
|
|
3
|
+
# This file contains a fake implementation of the subset of `sorbet-runtime`
|
|
4
|
+
# used by Braid that performs no runtime checks. See the "Type checking"
|
|
5
|
+
# section of development.md for background.
|
|
6
|
+
|
|
7
|
+
require 'singleton'
|
|
8
|
+
|
|
9
|
+
# Create our fake module at `Braid::T` so that if someone loads Braid into the
|
|
10
|
+
# same Ruby interpreter as other code that needs the real sorbet-runtime, we
|
|
11
|
+
# don't break the other code. (We don't officially support loading Braid as a
|
|
12
|
+
# library, but we may as well go ahead and put this infrastructure in place.)
|
|
13
|
+
# Code in the `Braid` module still uses normal references to `T`, so the Sorbet
|
|
14
|
+
# static analyzer (which doesn't read this file) doesn't see anything out of the
|
|
15
|
+
# ordinary, but those references resolve to `Braid::T` at runtime according to
|
|
16
|
+
# Ruby's constant lookup rules.
|
|
17
|
+
module Braid
|
|
18
|
+
module T
|
|
19
|
+
|
|
20
|
+
module Sig
|
|
21
|
+
def sig; end
|
|
22
|
+
end
|
|
23
|
+
def self.let(value, type)
|
|
24
|
+
value
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# NOTICE: Like everything else in the fake Sorbet runtime (e.g., `sig`),
|
|
28
|
+
# these do not actually perform runtime checks. Currently, if you want a
|
|
29
|
+
# runtime check, you have to implement it yourself. We considered defining
|
|
30
|
+
# wrapper functions with different names to make this clearer, but then we'd
|
|
31
|
+
# lose the extra static checks that Sorbet performs on direct calls to
|
|
32
|
+
# `T.cast` and `T.must`.
|
|
33
|
+
def self.cast(value, type)
|
|
34
|
+
value
|
|
35
|
+
end
|
|
36
|
+
def self.must(value)
|
|
37
|
+
value
|
|
38
|
+
end
|
|
39
|
+
def self.unsafe(value)
|
|
40
|
+
value
|
|
41
|
+
end
|
|
42
|
+
def self.bind(value, type); end
|
|
43
|
+
|
|
44
|
+
class FakeType
|
|
45
|
+
include Singleton
|
|
46
|
+
end
|
|
47
|
+
FAKE_TYPE = FakeType.instance
|
|
48
|
+
|
|
49
|
+
def self.type_alias
|
|
50
|
+
FAKE_TYPE
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def self.nilable(type)
|
|
54
|
+
FAKE_TYPE
|
|
55
|
+
end
|
|
56
|
+
def self.untyped
|
|
57
|
+
FAKE_TYPE
|
|
58
|
+
end
|
|
59
|
+
def self.noreturn
|
|
60
|
+
FAKE_TYPE
|
|
61
|
+
end
|
|
62
|
+
Boolean = FAKE_TYPE
|
|
63
|
+
module Array
|
|
64
|
+
def self.[](type)
|
|
65
|
+
FAKE_TYPE
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
module Hash
|
|
69
|
+
def self.[](key_type, value_type)
|
|
70
|
+
FAKE_TYPE
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
|
|
3
|
+
# Code to set up the Sorbet runtime and Sorbet-related utilities used throughout
|
|
4
|
+
# the Braid code. Called by `lib/braid.rb` and `lib/braid/operations_lite.rb`
|
|
5
|
+
# before any other Braid code is loaded.
|
|
6
|
+
|
|
7
|
+
env_use_sorbet_runtime = ENV['BRAID_USE_SORBET_RUNTIME']
|
|
8
|
+
if env_use_sorbet_runtime == '1'
|
|
9
|
+
require 'sorbet-runtime'
|
|
10
|
+
elsif [nil, '0'].include?(env_use_sorbet_runtime)
|
|
11
|
+
require 'braid/sorbet/fake_runtime'
|
|
12
|
+
else
|
|
13
|
+
puts <<-MSG
|
|
14
|
+
Braid: Error: BRAID_USE_SORBET_RUNTIME environment variable has invalid
|
|
15
|
+
value #{env_use_sorbet_runtime.inspect}; it must be "1", "0", or unset.
|
|
16
|
+
MSG
|
|
17
|
+
exit(1)
|
|
18
|
+
end
|
data/lib/braid/version.rb
CHANGED
data/lib/braid.rb
CHANGED
|
@@ -1,46 +1,65 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
|
|
3
|
+
require 'braid/sorbet/setup'
|
|
1
4
|
require 'braid/version'
|
|
2
5
|
|
|
3
6
|
module Braid
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
4
9
|
OLD_CONFIG_FILE = '.braids'
|
|
5
10
|
CONFIG_FILE = '.braids.json'
|
|
6
11
|
|
|
7
12
|
# See the background in the "Supported environments" section of README.md.
|
|
8
13
|
#
|
|
9
14
|
# The newest Git feature that Braid is currently known to rely on is
|
|
10
|
-
# `
|
|
11
|
-
# 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
|
|
12
16
|
# doesn't seem worth even a small amount of work to remove that dependency and
|
|
13
17
|
# support even older versions of Git. So set that as the declared requirement
|
|
14
18
|
# for now. In general, a reasonable approach might be to try to support the
|
|
15
19
|
# oldest version of Git in current "long-term support" versions of popular OS
|
|
16
20
|
# distributions.
|
|
17
|
-
REQUIRED_GIT_VERSION = '2.
|
|
21
|
+
REQUIRED_GIT_VERSION = '2.8.0'
|
|
18
22
|
|
|
23
|
+
@verbose = T.let(false, T::Boolean)
|
|
24
|
+
|
|
25
|
+
sig {returns(T::Boolean)}
|
|
19
26
|
def self.verbose
|
|
20
27
|
!!@verbose
|
|
21
28
|
end
|
|
22
29
|
|
|
30
|
+
# TODO (typing): One would think `new_value` shouldn't be nilable, but
|
|
31
|
+
# apparently `lib/braid/main.rb` passes nil sometimes. Is that easy to fix?
|
|
32
|
+
# (Ditto with `self.force=` below.)
|
|
33
|
+
sig {params(new_value: T.nilable(T::Boolean)).void}
|
|
23
34
|
def self.verbose=(new_value)
|
|
24
35
|
@verbose = !!new_value
|
|
25
36
|
end
|
|
26
37
|
|
|
38
|
+
@force = T.let(false, T::Boolean)
|
|
39
|
+
|
|
40
|
+
sig {returns(T::Boolean)}
|
|
27
41
|
def self.force
|
|
28
42
|
!!@force
|
|
29
43
|
end
|
|
30
44
|
|
|
45
|
+
sig {params(new_value: T.nilable(T::Boolean)).void}
|
|
31
46
|
def self.force=(new_value)
|
|
32
47
|
@force = !!new_value
|
|
33
48
|
end
|
|
34
49
|
|
|
50
|
+
sig {returns(T::Boolean)}
|
|
35
51
|
def self.use_local_cache
|
|
36
52
|
[nil, 'true', '1'].include?(ENV['BRAID_USE_LOCAL_CACHE'])
|
|
37
53
|
end
|
|
38
54
|
|
|
55
|
+
sig {returns(String)}
|
|
39
56
|
def self.local_cache_dir
|
|
40
57
|
File.expand_path(ENV['BRAID_LOCAL_CACHE_DIR'] || "#{ENV['HOME']}/.braid/cache")
|
|
41
58
|
end
|
|
42
59
|
|
|
43
60
|
class BraidError < StandardError
|
|
61
|
+
extend T::Sig
|
|
62
|
+
sig {returns(String)}
|
|
44
63
|
def message
|
|
45
64
|
value = super
|
|
46
65
|
value if value != self.class.name
|
|
@@ -48,6 +67,7 @@ module Braid
|
|
|
48
67
|
end
|
|
49
68
|
|
|
50
69
|
class InternalError < BraidError
|
|
70
|
+
sig {returns(String)}
|
|
51
71
|
def message
|
|
52
72
|
"internal error: #{super}"
|
|
53
73
|
end
|