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