braid 1.1.8 → 1.1.9
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 +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: []
|