braid 1.1.8 → 1.1.10
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/lib/braid/command.rb +39 -15
- data/lib/braid/commands/add.rb +22 -12
- data/lib/braid/commands/diff.rb +27 -10
- data/lib/braid/commands/push.rb +33 -18
- data/lib/braid/commands/remove.rb +17 -4
- data/lib/braid/commands/setup.rb +12 -3
- data/lib/braid/commands/status.rb +17 -8
- data/lib/braid/commands/update.rb +47 -23
- data/lib/braid/commands/upgrade_config.rb +18 -4
- data/lib/braid/config.rb +28 -18
- data/lib/braid/main.rb +47 -33
- data/lib/braid/mirror.rb +70 -44
- data/lib/braid/operations.rb +235 -79
- data/lib/braid/operations_lite.rb +1 -1
- data/lib/braid/sorbet/fake_runtime.rb +22 -0
- data/lib/braid/version.rb +1 -1
- data/lib/braid.rb +2 -10
- metadata +46 -4
data/lib/braid/operations.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
|
-
# typed:
|
1
|
+
# typed: strict
|
2
2
|
|
3
3
|
require 'singleton'
|
4
4
|
require 'rubygems'
|
5
|
+
require 'shellwords'
|
5
6
|
require 'tempfile'
|
6
7
|
|
7
8
|
module Braid
|
@@ -9,45 +10,56 @@ module Braid
|
|
9
10
|
|
10
11
|
module Operations
|
11
12
|
class ShellExecutionError < BraidError
|
13
|
+
sig {returns(String)}
|
12
14
|
attr_reader :err, :out
|
13
15
|
|
14
|
-
|
16
|
+
sig {params(err: String, out: String).void}
|
17
|
+
def initialize(err, out)
|
15
18
|
@err = err
|
16
19
|
@out = out
|
17
20
|
end
|
18
21
|
|
22
|
+
sig {returns(String)}
|
19
23
|
def message
|
20
|
-
@err
|
24
|
+
@err
|
21
25
|
end
|
22
26
|
end
|
23
27
|
class VersionTooLow < BraidError
|
28
|
+
sig {params(command: String, version: String, required: String).void}
|
24
29
|
def initialize(command, version, required)
|
25
30
|
@command = command
|
26
|
-
|
31
|
+
# TODO (typing): Probably should not be nilable
|
32
|
+
@version = T.let(version.to_s.split("\n").first, T.nilable(String))
|
27
33
|
@required = required
|
28
34
|
end
|
29
35
|
|
36
|
+
sig {returns(String)}
|
30
37
|
def message
|
31
38
|
"#{@command} version too low: #{@version}. #{@required} needed."
|
32
39
|
end
|
33
40
|
end
|
34
41
|
class UnknownRevision < BraidError
|
42
|
+
sig {returns(String)}
|
35
43
|
def message
|
36
44
|
"unknown revision: #{super}"
|
37
45
|
end
|
38
46
|
end
|
39
47
|
class LocalChangesPresent < BraidError
|
48
|
+
sig {returns(String)}
|
40
49
|
def message
|
41
50
|
'local changes are present'
|
42
51
|
end
|
43
52
|
end
|
44
53
|
class MergeError < BraidError
|
54
|
+
sig {returns(String)}
|
45
55
|
attr_reader :conflicts_text
|
46
56
|
|
57
|
+
sig {params(conflicts_text: String).void}
|
47
58
|
def initialize(conflicts_text)
|
48
59
|
@conflicts_text = conflicts_text
|
49
60
|
end
|
50
61
|
|
62
|
+
sig {returns(String)}
|
51
63
|
def message
|
52
64
|
'could not merge'
|
53
65
|
end
|
@@ -55,18 +67,24 @@ module Braid
|
|
55
67
|
|
56
68
|
# The command proxy is meant to encapsulate commands such as git, that work with subcommands.
|
57
69
|
class Proxy
|
70
|
+
extend T::Sig
|
58
71
|
include Singleton
|
59
72
|
|
60
|
-
|
61
|
-
|
73
|
+
# TODO (typing): We could make this method abstract if our fake Sorbet
|
74
|
+
# runtime supported abstract methods.
|
75
|
+
sig {returns(String)}
|
76
|
+
def self.command
|
77
|
+
raise InternalError, 'Proxy.command not overridden'
|
62
78
|
end
|
63
79
|
|
64
80
|
# hax!
|
81
|
+
sig {returns(String)}
|
65
82
|
def version
|
66
|
-
_, out, _ = exec!(
|
83
|
+
_, out, _ = exec!([self.class.command, '--version'])
|
67
84
|
out.sub(/^.* version/, '').strip.sub(/ .*$/, '').strip
|
68
85
|
end
|
69
86
|
|
87
|
+
sig {params(required: String).returns(T::Boolean)}
|
70
88
|
def require_version(required)
|
71
89
|
# Gem::Version is intended for Ruby gem versions, but various web sites
|
72
90
|
# suggest it as a convenient way of comparing version strings in
|
@@ -75,75 +93,117 @@ module Braid
|
|
75
93
|
Gem::Version.new(version) >= Gem::Version.new(required)
|
76
94
|
end
|
77
95
|
|
96
|
+
sig {params(required: String).void}
|
78
97
|
def require_version!(required)
|
79
98
|
require_version(required) || raise(VersionTooLow.new(self.class.command, version, required))
|
80
99
|
end
|
81
100
|
|
82
101
|
private
|
83
102
|
|
103
|
+
sig {params(name: String).returns(T::Array[String])}
|
84
104
|
def command(name)
|
85
105
|
# stub
|
86
|
-
name
|
106
|
+
[name]
|
87
107
|
end
|
88
108
|
|
89
|
-
|
90
|
-
|
109
|
+
sig {params(arg: String, args: T::Array[String]).returns(String)}
|
110
|
+
def invoke(arg, args)
|
111
|
+
exec!(command(arg) + args)[1].strip # return stdout
|
91
112
|
end
|
92
113
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
114
|
+
# Some of the unit tests want to mock out `exec`, but they have no way to
|
115
|
+
# construct a real Process::Status and thus use an integer instead. We
|
116
|
+
# have to accommodate this in the type annotation to avoid runtime type
|
117
|
+
# check failures during the tests. In normal use of Braid, this will
|
118
|
+
# always be a real Process::Status. Fortunately, allowing Integer doesn't
|
119
|
+
# seem to cause any other problems right now.
|
120
|
+
ProcessStatusOrInteger = T.type_alias { T.any(Process::Status, Integer) }
|
99
121
|
|
122
|
+
sig {params(cmd: T::Array[String]).returns([ProcessStatusOrInteger, String, String])}
|
100
123
|
def exec(cmd)
|
101
|
-
cmd.strip!
|
102
|
-
|
103
124
|
Operations::with_modified_environment({'LANG' => 'C'}) do
|
104
125
|
log(cmd)
|
105
|
-
|
126
|
+
# The special `[cmd[0], cmd[0]]` syntax ensures that `cmd[0]` is
|
127
|
+
# interpreted as the path of the executable and not a shell command
|
128
|
+
# even if `cmd` has only one element. See the documentation:
|
129
|
+
# https://ruby-doc.org/core-3.1.2/Process.html#method-c-spawn.
|
130
|
+
# Granted, this shouldn't matter for Braid for two reasons: (1)
|
131
|
+
# `cmd[0]` is always "git", which doesn't contain any shell special
|
132
|
+
# characters, and (2) `cmd` always has at least one additional
|
133
|
+
# argument (the Git subcommand). However, it's still nice to make our
|
134
|
+
# intent clear.
|
135
|
+
out, err, status = T.unsafe(Open3).capture3([cmd[0], cmd[0]], *cmd[1..])
|
106
136
|
[status, out, err]
|
107
137
|
end
|
108
138
|
end
|
109
139
|
|
140
|
+
sig {params(cmd: T::Array[String]).returns([ProcessStatusOrInteger, String, String])}
|
110
141
|
def exec!(cmd)
|
111
142
|
status, out, err = exec(cmd)
|
112
143
|
raise ShellExecutionError.new(err, out) unless status == 0
|
113
144
|
[status, out, err]
|
114
145
|
end
|
115
146
|
|
147
|
+
sig {params(cmd: T::Array[String]).returns(ProcessStatusOrInteger)}
|
116
148
|
def system(cmd)
|
117
|
-
cmd.strip!
|
118
|
-
|
119
149
|
# Without this, "braid diff" output came out in the wrong order on Windows.
|
120
150
|
$stdout.flush
|
121
151
|
$stderr.flush
|
122
152
|
Operations::with_modified_environment({'LANG' => 'C'}) do
|
123
|
-
|
153
|
+
# See the comment in `exec` about the `[cmd[0], cmd[0]]` syntax.
|
154
|
+
T.unsafe(Kernel).system([cmd[0], cmd[0]], *cmd[1..])
|
124
155
|
return $?
|
125
156
|
end
|
126
157
|
end
|
127
158
|
|
159
|
+
sig {params(str: String).void}
|
128
160
|
def msg(str)
|
129
161
|
puts "Braid: #{str}"
|
130
162
|
end
|
131
163
|
|
164
|
+
sig {params(cmd: T::Array[String]).void}
|
132
165
|
def log(cmd)
|
133
|
-
|
166
|
+
# Note: `Shellwords.shelljoin` follows Bourne shell quoting rules, as
|
167
|
+
# its documentation states. This may not be what a Windows user
|
168
|
+
# expects, but it's not worth the trouble to try to find a library that
|
169
|
+
# produces something better on Windows, especially because it's unclear
|
170
|
+
# which of Windows's several different quoted formats we would use
|
171
|
+
# (e.g., CommandLineToArgvW, cmd.exe, or PowerShell). The most
|
172
|
+
# important thing is to use _some_ unambiguous representation.
|
173
|
+
msg "Executing `#{Shellwords.shelljoin(cmd)}` in #{Dir.pwd}" if verbose?
|
134
174
|
end
|
135
175
|
|
176
|
+
sig {returns(T::Boolean)}
|
136
177
|
def verbose?
|
137
178
|
Braid.verbose
|
138
179
|
end
|
139
180
|
end
|
140
181
|
|
141
182
|
class Git < Proxy
|
183
|
+
|
184
|
+
sig {returns(String)}
|
185
|
+
def self.command
|
186
|
+
'git'
|
187
|
+
end
|
188
|
+
|
189
|
+
# A string representing a Git object ID (i.e., hash). This type alias is
|
190
|
+
# used as documentation and is not enforced, so there's a risk that we
|
191
|
+
# mistakenly mark something as an ObjectID when it can actually be a
|
192
|
+
# String that is not an ObjectID.
|
193
|
+
ObjectID = T.type_alias { String }
|
194
|
+
|
195
|
+
# A string containing an expression that can be evaluated to an object ID
|
196
|
+
# by `git rev-parse`. This type is enforced even less than `ObjectID` (in
|
197
|
+
# some cases, we apply it directly to user input without validation), but
|
198
|
+
# it still serves to document our intent.
|
199
|
+
ObjectExpr = T.type_alias { String }
|
200
|
+
|
142
201
|
# Get the physical path to a file in the git repository (e.g.,
|
143
202
|
# 'MERGE_MSG'), taking into account worktree configuration. The returned
|
144
203
|
# path may be absolute or relative to the current working directory.
|
204
|
+
sig {params(path: String).returns(String)}
|
145
205
|
def repo_file_path(path)
|
146
|
-
invoke(
|
206
|
+
invoke('rev-parse', ['--git-path', path])
|
147
207
|
end
|
148
208
|
|
149
209
|
# If the current directory is not inside a git repository at all, this
|
@@ -151,8 +211,9 @@ module Braid
|
|
151
211
|
# propagated as a ShellExecutionError. is_inside_worktree can return
|
152
212
|
# false when inside a bare repository and in certain other rare cases such
|
153
213
|
# as when the GIT_WORK_TREE environment variable is set.
|
214
|
+
sig {returns(T::Boolean)}
|
154
215
|
def is_inside_worktree
|
155
|
-
invoke(
|
216
|
+
invoke('rev-parse', ['--is-inside-work-tree']) == 'true'
|
156
217
|
end
|
157
218
|
|
158
219
|
# Get the prefix of the current directory relative to the worktree. Empty
|
@@ -160,21 +221,23 @@ module Braid
|
|
160
221
|
# In some cases in which the current directory is not inside a worktree at
|
161
222
|
# all, this will successfully return an empty string, so it may be
|
162
223
|
# desirable to check is_inside_worktree first.
|
224
|
+
sig {returns(String)}
|
163
225
|
def relative_working_dir
|
164
|
-
invoke(
|
226
|
+
invoke('rev-parse', ['--show-prefix'])
|
165
227
|
end
|
166
228
|
|
167
|
-
|
168
|
-
|
229
|
+
sig {params(message: T.nilable(String), args: T::Array[String]).returns(T::Boolean)}
|
230
|
+
def commit(message, args = [])
|
231
|
+
cmd = ['git', 'commit', '--no-verify']
|
169
232
|
message_file = nil
|
170
233
|
if message # allow nil
|
171
234
|
message_file = Tempfile.new('braid_commit')
|
172
235
|
message_file.print("Braid: #{message}")
|
173
236
|
message_file.flush
|
174
237
|
message_file.close
|
175
|
-
cmd
|
238
|
+
cmd += ['-F', T.must(message_file.path)]
|
176
239
|
end
|
177
|
-
cmd
|
240
|
+
cmd += args
|
178
241
|
status, out, err = exec(cmd)
|
179
242
|
message_file.unlink if message_file
|
180
243
|
|
@@ -183,54 +246,59 @@ module Braid
|
|
183
246
|
elsif out.match(/nothing.* to commit/)
|
184
247
|
false
|
185
248
|
else
|
186
|
-
raise ShellExecutionError,
|
249
|
+
raise ShellExecutionError.new(err, out)
|
187
250
|
end
|
188
251
|
end
|
189
252
|
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
def checkout(treeish)
|
196
|
-
invoke(:checkout, treeish)
|
197
|
-
true
|
253
|
+
sig {params(remote: T.nilable(String), args: T::Array[String]).void}
|
254
|
+
def fetch(remote = nil, args = [])
|
255
|
+
args = ['-n', remote] + args if remote
|
256
|
+
exec!(['git', 'fetch'] + args)
|
198
257
|
end
|
199
258
|
|
200
259
|
# Returns the base commit or nil.
|
260
|
+
sig {params(target: ObjectExpr, source: ObjectExpr).returns(T.nilable(ObjectID))}
|
201
261
|
def merge_base(target, source)
|
202
|
-
invoke(
|
262
|
+
invoke('merge-base', [target, source])
|
203
263
|
rescue ShellExecutionError
|
204
264
|
nil
|
205
265
|
end
|
206
266
|
|
207
|
-
|
208
|
-
|
267
|
+
sig {params(expr: ObjectExpr).returns(ObjectID)}
|
268
|
+
def rev_parse(expr)
|
269
|
+
invoke('rev-parse', [expr])
|
209
270
|
rescue ShellExecutionError
|
210
|
-
raise UnknownRevision,
|
271
|
+
raise UnknownRevision, expr
|
211
272
|
end
|
212
273
|
|
213
274
|
# Implies tracking.
|
275
|
+
#
|
276
|
+
# TODO (typing): Remove the return value if we're confident that nothing
|
277
|
+
# uses it, here and in similar cases.
|
278
|
+
sig {params(remote: String, path: String).returns(TrueClass)}
|
214
279
|
def remote_add(remote, path)
|
215
|
-
invoke(
|
280
|
+
invoke('remote', ['add', remote, path])
|
216
281
|
true
|
217
282
|
end
|
218
283
|
|
284
|
+
sig {params(remote: String).returns(TrueClass)}
|
219
285
|
def remote_rm(remote)
|
220
|
-
invoke(
|
286
|
+
invoke('remote', ['rm', remote])
|
221
287
|
true
|
222
288
|
end
|
223
289
|
|
224
290
|
# Checks git remotes.
|
291
|
+
sig {params(remote: String).returns(T.nilable(String))}
|
225
292
|
def remote_url(remote)
|
226
293
|
key = "remote.#{remote}.url"
|
227
|
-
invoke(
|
294
|
+
invoke('config', [key])
|
228
295
|
rescue ShellExecutionError
|
229
296
|
nil
|
230
297
|
end
|
231
298
|
|
299
|
+
sig {params(target: ObjectExpr).returns(TrueClass)}
|
232
300
|
def reset_hard(target)
|
233
|
-
invoke(
|
301
|
+
invoke('reset', ['--hard', target])
|
234
302
|
true
|
235
303
|
end
|
236
304
|
|
@@ -242,47 +310,65 @@ module Braid
|
|
242
310
|
# 'recursive' part (i.e., merge of bases) does not come into play and only
|
243
311
|
# the trees matter. But for some reason, Git's smartest tree merge
|
244
312
|
# algorithm is only available via the 'recursive' strategy.
|
313
|
+
sig {params(base_treeish: ObjectExpr, local_treeish: ObjectExpr, remote_treeish: ObjectExpr).returns(TrueClass)}
|
245
314
|
def merge_trees(base_treeish, local_treeish, remote_treeish)
|
246
|
-
invoke(
|
315
|
+
invoke('merge-recursive', [base_treeish, '--', local_treeish, remote_treeish])
|
247
316
|
true
|
248
317
|
rescue ShellExecutionError => error
|
249
318
|
# 'CONFLICT' messages go to stdout.
|
250
319
|
raise MergeError, error.out
|
251
320
|
end
|
252
321
|
|
322
|
+
sig {params(prefix: String).returns(String)}
|
253
323
|
def read_ls_files(prefix)
|
254
|
-
invoke('ls-files', prefix)
|
324
|
+
invoke('ls-files', [prefix])
|
255
325
|
end
|
256
326
|
|
257
327
|
class BlobWithMode
|
328
|
+
extend T::Sig
|
329
|
+
sig {params(hash: ObjectID, mode: String).void}
|
258
330
|
def initialize(hash, mode)
|
259
331
|
@hash = hash
|
260
332
|
@mode = mode
|
261
333
|
end
|
262
|
-
|
334
|
+
sig {returns(ObjectID)}
|
335
|
+
attr_reader :hash
|
336
|
+
sig {returns(String)}
|
337
|
+
attr_reader :mode
|
263
338
|
end
|
264
339
|
# Allow the class to be referenced as `git.BlobWithMode`.
|
340
|
+
sig {returns(T.class_of(BlobWithMode))}
|
265
341
|
def BlobWithMode
|
266
342
|
Git::BlobWithMode
|
267
343
|
end
|
344
|
+
# An ObjectID used as a TreeItem represents a tree.
|
345
|
+
TreeItem = T.type_alias { T.any(ObjectID, BlobWithMode) }
|
268
346
|
|
269
347
|
# Get the item at the given path in the given tree. If it's a tree, just
|
270
348
|
# return its hash; if it's a blob, return a BlobWithMode object. (This is
|
271
349
|
# how we remember the mode for single-file mirrors.)
|
350
|
+
# TODO (typing): Should `path` be nilable?
|
351
|
+
sig {params(tree: ObjectExpr, path: T.nilable(String)).returns(TreeItem)}
|
272
352
|
def get_tree_item(tree, path)
|
273
353
|
if path.nil? || path == ''
|
274
354
|
tree
|
275
355
|
else
|
276
|
-
m =
|
277
|
-
|
278
|
-
|
279
|
-
|
356
|
+
m = /^([^ ]*) ([^ ]*) ([^\t]*)\t.*$/.match(invoke('ls-tree', [tree, path]))
|
357
|
+
if m.nil?
|
358
|
+
# This can happen if the user runs `braid add` with a `--path` that
|
359
|
+
# doesn't exist. TODO: Make the error message more user-friendly in
|
360
|
+
# that case.
|
361
|
+
raise BraidError, 'No tree item exists at the given path'
|
362
|
+
end
|
363
|
+
mode = T.must(m[1])
|
364
|
+
type = T.must(m[2])
|
365
|
+
hash = T.must(m[3])
|
280
366
|
if type == 'tree'
|
281
367
|
hash
|
282
368
|
elsif type == 'blob'
|
283
369
|
return BlobWithMode.new(hash, mode)
|
284
370
|
else
|
285
|
-
raise
|
371
|
+
raise BraidError, 'Tree item is not a tree or a blob'
|
286
372
|
end
|
287
373
|
end
|
288
374
|
end
|
@@ -291,35 +377,47 @@ module Braid
|
|
291
377
|
# path. If update_worktree is true, then update the worktree, otherwise
|
292
378
|
# disregard the state of the worktree (most useful with a temporary index
|
293
379
|
# file).
|
380
|
+
sig {params(item: TreeItem, path: String, update_worktree: T::Boolean).void}
|
294
381
|
def add_item_to_index(item, path, update_worktree)
|
295
382
|
if item.is_a?(BlobWithMode)
|
296
|
-
invoke(
|
383
|
+
invoke('update-index', ['--add', '--cacheinfo', "#{item.mode},#{item.hash},#{path}"])
|
297
384
|
if update_worktree
|
298
385
|
# XXX If this fails, we've already updated the index.
|
299
|
-
invoke(
|
386
|
+
invoke('checkout-index', [path])
|
300
387
|
end
|
301
388
|
else
|
302
389
|
# According to
|
303
390
|
# https://lore.kernel.org/git/e48a281a4d3db0a04c0609fcb8658e4fcc797210.1646166271.git.gitgitgadget@gmail.com/,
|
304
391
|
# `--prefix=` is valid if the path is empty.
|
305
|
-
invoke(
|
392
|
+
invoke('read-tree', ["--prefix=#{path}", update_worktree ? '-u' : '-i', item])
|
306
393
|
end
|
307
394
|
end
|
308
395
|
|
309
396
|
# Read tree into the root of the index. This may not be the preferred way
|
310
397
|
# to do it, but it seems to work.
|
398
|
+
sig {params(treeish: ObjectExpr).void}
|
311
399
|
def read_tree_im(treeish)
|
312
|
-
invoke(
|
313
|
-
|
400
|
+
invoke('read-tree', ['-im', treeish])
|
401
|
+
end
|
402
|
+
|
403
|
+
sig {params(treeish: ObjectExpr).void}
|
404
|
+
def read_tree_um(treeish)
|
405
|
+
invoke('read-tree', ['-um', treeish])
|
314
406
|
end
|
315
407
|
|
316
408
|
# Write a tree object for the current index and return its ID.
|
409
|
+
sig {returns(ObjectID)}
|
317
410
|
def write_tree
|
318
|
-
invoke(
|
411
|
+
invoke('write-tree', [])
|
319
412
|
end
|
320
413
|
|
321
414
|
# Execute a block using a temporary git index file, initially empty.
|
322
|
-
|
415
|
+
sig {
|
416
|
+
type_parameters(:R).params(
|
417
|
+
blk: T.proc.returns(T.type_parameter(:R))
|
418
|
+
).returns(T.type_parameter(:R))
|
419
|
+
}
|
420
|
+
def with_temporary_index(&blk)
|
323
421
|
Dir.mktmpdir('braid_index') do |dir|
|
324
422
|
Operations::with_modified_environment(
|
325
423
|
{'GIT_INDEX_FILE' => File.join(dir, 'index')}) do
|
@@ -328,6 +426,7 @@ module Braid
|
|
328
426
|
end
|
329
427
|
end
|
330
428
|
|
429
|
+
sig {params(main_content: T.nilable(ObjectExpr), item_path: String, item: TreeItem).returns(ObjectID)}
|
331
430
|
def make_tree_with_item(main_content, item_path, item)
|
332
431
|
with_temporary_index do
|
333
432
|
# If item_path is '', then rm_r_cached will fail. But in that case,
|
@@ -342,66 +441,117 @@ module Braid
|
|
342
441
|
end
|
343
442
|
end
|
344
443
|
|
444
|
+
sig {params(args: T::Array[String]).returns(T.nilable(String))}
|
345
445
|
def config(args)
|
346
|
-
invoke(
|
446
|
+
invoke('config', args) rescue nil
|
347
447
|
end
|
348
448
|
|
449
|
+
sig {params(path: String).void}
|
450
|
+
def add(path)
|
451
|
+
invoke('add', [path])
|
452
|
+
end
|
453
|
+
|
454
|
+
sig {params(path: String).void}
|
455
|
+
def rm(path)
|
456
|
+
invoke('rm', [path])
|
457
|
+
end
|
458
|
+
|
459
|
+
sig {params(path: String).returns(TrueClass)}
|
349
460
|
def rm_r(path)
|
350
|
-
invoke(
|
461
|
+
invoke('rm', ['-r', path])
|
351
462
|
true
|
352
463
|
end
|
353
464
|
|
354
465
|
# Remove from index only.
|
466
|
+
sig {params(path: String).returns(TrueClass)}
|
355
467
|
def rm_r_cached(path)
|
356
|
-
invoke(
|
468
|
+
invoke('rm', ['-r', '--cached', path])
|
357
469
|
true
|
358
470
|
end
|
359
471
|
|
472
|
+
sig {params(path: String, treeish: ObjectExpr).returns(ObjectID)}
|
360
473
|
def tree_hash(path, treeish = 'HEAD')
|
361
|
-
out = invoke(
|
362
|
-
out.split[2]
|
474
|
+
out = invoke('ls-tree', [treeish, '-d', path])
|
475
|
+
T.must(out.split[2])
|
363
476
|
end
|
364
477
|
|
365
|
-
|
478
|
+
sig {params(args: T::Array[String]).returns(String)}
|
479
|
+
def diff(args)
|
480
|
+
invoke('diff', args)
|
481
|
+
end
|
482
|
+
|
483
|
+
sig {params(args: T::Array[String]).returns(ProcessStatusOrInteger)}
|
484
|
+
def diff_to_stdout(args)
|
366
485
|
# For now, ignore the exit code. It can be 141 (SIGPIPE) if the user
|
367
486
|
# quits the pager before reading all the output.
|
368
|
-
system(
|
487
|
+
system(['git', 'diff'] + args)
|
369
488
|
end
|
370
489
|
|
490
|
+
sig {returns(T::Boolean)}
|
371
491
|
def status_clean?
|
372
|
-
_, out, _ = exec('git status')
|
492
|
+
_, out, _ = exec(['git', 'status'])
|
373
493
|
!out.split("\n").grep(/nothing to commit/).empty?
|
374
494
|
end
|
375
495
|
|
496
|
+
sig {void}
|
376
497
|
def ensure_clean!
|
377
498
|
status_clean? || raise(LocalChangesPresent)
|
378
499
|
end
|
379
500
|
|
501
|
+
sig {returns(ObjectID)}
|
380
502
|
def head
|
381
503
|
rev_parse('HEAD')
|
382
504
|
end
|
383
505
|
|
384
|
-
|
385
|
-
|
386
|
-
|
506
|
+
sig {void}
|
507
|
+
def init
|
508
|
+
invoke('init', [])
|
509
|
+
end
|
510
|
+
|
511
|
+
sig {params(args: T::Array[String]).void}
|
512
|
+
def clone(args)
|
513
|
+
invoke('clone', args)
|
514
|
+
end
|
515
|
+
|
516
|
+
# Wrappers for Git commands that were called via `method_missing` before
|
517
|
+
# the move to static typing but for which the existing calls don't follow
|
518
|
+
# a clear enough pattern around which we could design a narrower API than
|
519
|
+
# forwarding an arbitrary argument list. We may narrow the API in the
|
520
|
+
# future if it becomes clear what it should be.
|
521
|
+
|
522
|
+
sig {params(args: T::Array[String]).returns(String)}
|
523
|
+
def rev_list(args)
|
524
|
+
invoke('rev-list', args)
|
525
|
+
end
|
526
|
+
|
527
|
+
sig {params(args: T::Array[String]).void}
|
528
|
+
def update_ref(args)
|
529
|
+
invoke('update-ref', args)
|
530
|
+
end
|
531
|
+
|
532
|
+
sig {params(args: T::Array[String]).void}
|
533
|
+
def push(args)
|
534
|
+
invoke('push', args)
|
387
535
|
end
|
388
536
|
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
invoke(:clone, *args)
|
537
|
+
sig {params(args: T::Array[String]).returns(String)}
|
538
|
+
def ls_remote(args)
|
539
|
+
invoke('ls-remote', args)
|
393
540
|
end
|
394
541
|
|
395
542
|
private
|
396
543
|
|
544
|
+
sig {params(name: String).returns(T::Array[String])}
|
397
545
|
def command(name)
|
398
|
-
|
546
|
+
[self.class.command, name]
|
399
547
|
end
|
400
548
|
end
|
401
549
|
|
402
550
|
class GitCache
|
551
|
+
extend T::Sig
|
403
552
|
include Singleton
|
404
553
|
|
554
|
+
sig {params(url: String).void}
|
405
555
|
def fetch(url)
|
406
556
|
dir = path(url)
|
407
557
|
|
@@ -416,30 +566,36 @@ module Braid
|
|
416
566
|
end
|
417
567
|
else
|
418
568
|
FileUtils.mkdir_p(local_cache_dir)
|
419
|
-
git.clone('--mirror', url, dir)
|
569
|
+
git.clone(['--mirror', url, dir])
|
420
570
|
end
|
421
571
|
end
|
422
572
|
|
573
|
+
sig {params(url: String).returns(String)}
|
423
574
|
def path(url)
|
424
575
|
File.join(local_cache_dir, url.gsub(/[\/:@]/, '_'))
|
425
576
|
end
|
426
577
|
|
427
578
|
private
|
428
579
|
|
580
|
+
sig {returns(String)}
|
429
581
|
def local_cache_dir
|
430
582
|
Braid.local_cache_dir
|
431
583
|
end
|
432
584
|
|
585
|
+
sig {returns(Git)}
|
433
586
|
def git
|
434
587
|
Git.instance
|
435
588
|
end
|
436
589
|
end
|
437
590
|
|
438
591
|
module VersionControl
|
592
|
+
extend T::Sig
|
593
|
+
sig {returns(Git)}
|
439
594
|
def git
|
440
595
|
Git.instance
|
441
596
|
end
|
442
597
|
|
598
|
+
sig {returns(GitCache)}
|
443
599
|
def git_cache
|
444
600
|
GitCache.instance
|
445
601
|
end
|
@@ -22,7 +22,7 @@ module Braid
|
|
22
22
|
).returns(T.type_parameter(:R))
|
23
23
|
}
|
24
24
|
def self.with_modified_environment(dict, &blk)
|
25
|
-
orig_dict = {}
|
25
|
+
orig_dict = T.let({}, T::Hash[String, T.nilable(String)])
|
26
26
|
dict.each { |name, value|
|
27
27
|
orig_dict[name] = ENV[name]
|
28
28
|
ENV[name] = value
|