braid 1.1.8 → 1.1.10
Sign up to get free protection for your applications and to get access to all the features.
- 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
|