thuban 0.3.0 → 0.4.0
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/CHANGELOG.md +12 -0
- data/README.md +150 -7
- data/lib/thuban/pack_reader.rb +216 -0
- data/lib/thuban/remote/credentials.rb +209 -0
- data/lib/thuban/remote/fetch.rb +287 -0
- data/lib/thuban/remote/http.rb +233 -0
- data/lib/thuban/remote/local.rb +55 -0
- data/lib/thuban/remote/protocol.rb +91 -0
- data/lib/thuban/remote/push.rb +250 -0
- data/lib/thuban/remote/repository.rb +223 -0
- data/lib/thuban/remote/ssh.rb +248 -0
- data/lib/thuban/remote.rb +27 -0
- data/lib/thuban/version.rb +1 -1
- data/lib/thuban.rb +10 -0
- data/sig/thuban.rbs +65 -0
- metadata +14 -4
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "shellwords"
|
|
5
|
+
require "timeout"
|
|
6
|
+
require "uri"
|
|
7
|
+
|
|
8
|
+
module Thuban
|
|
9
|
+
module Remote
|
|
10
|
+
class SSHConnection < Connection
|
|
11
|
+
MAX_COMMAND_ARGUMENTS = 64
|
|
12
|
+
MAX_COMMAND_ARGUMENT_SIZE = 4096
|
|
13
|
+
USER_PATTERN = /\A[A-Za-z0-9][A-Za-z0-9._-]{0,63}\z/n
|
|
14
|
+
HOST_PATTERN = /\A(?:[A-Za-z0-9](?:[A-Za-z0-9.-]{0,251}[A-Za-z0-9])?|\[[0-9A-Fa-f:.]+\])\z/n
|
|
15
|
+
PATH_PATTERN = /\A[0-9A-Za-z._~+\/:@=-]{1,4096}\z/n
|
|
16
|
+
|
|
17
|
+
def initialize(url, credentials: nil, ssh: nil, timeout: 30)
|
|
18
|
+
raise AuthenticationError, "HTTP credentials cannot be used with SSH remotes" unless credentials.nil?
|
|
19
|
+
|
|
20
|
+
@user, @host, @port, @path = parse_ssh_url(url)
|
|
21
|
+
@ssh_command = parse_ssh_command(ssh)
|
|
22
|
+
@timeout = Float(timeout)
|
|
23
|
+
raise ArgumentError, "timeout must be between 0 and 300 seconds" unless @timeout.positive? && @timeout <= 300
|
|
24
|
+
|
|
25
|
+
@process_lock = Mutex.new
|
|
26
|
+
@process = nil
|
|
27
|
+
@closed = false
|
|
28
|
+
rescue AuthenticationError
|
|
29
|
+
raise
|
|
30
|
+
rescue ArgumentError, TypeError
|
|
31
|
+
raise ArgumentError, "timeout must be between 0 and 300 seconds" if defined?(@timeout)
|
|
32
|
+
|
|
33
|
+
raise TransportError, "invalid SSH remote"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def refs
|
|
37
|
+
ensure_open
|
|
38
|
+
@refs = with_process("git-upload-pack") do |stdin, stdout|
|
|
39
|
+
reader = Protocol::Reader.new(stdout, max_bytes: MAX_ADVERTISEMENT_SIZE)
|
|
40
|
+
first = reader.read
|
|
41
|
+
@protocol_version = 0
|
|
42
|
+
result = read_v0_refs(reader, first)
|
|
43
|
+
stdin.close
|
|
44
|
+
result
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def close
|
|
49
|
+
process = @process_lock.synchronize do
|
|
50
|
+
@closed = true
|
|
51
|
+
@process
|
|
52
|
+
end
|
|
53
|
+
cleanup(process)
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def receive_refs
|
|
60
|
+
return @receive_refs if @receive_refs
|
|
61
|
+
|
|
62
|
+
fetch_capabilities = @capabilities
|
|
63
|
+
begin
|
|
64
|
+
ensure_open
|
|
65
|
+
with_process("git-receive-pack") do |stdin, stdout|
|
|
66
|
+
reader = Protocol::Reader.new(stdout, max_bytes: MAX_ADVERTISEMENT_SIZE)
|
|
67
|
+
first = reader.read
|
|
68
|
+
result = read_v0_refs(reader, first)
|
|
69
|
+
@receive_capabilities = @capabilities
|
|
70
|
+
stdin.close
|
|
71
|
+
@receive_refs = result
|
|
72
|
+
end
|
|
73
|
+
ensure
|
|
74
|
+
@capabilities = fetch_capabilities
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def request_each(method, suffix, query: nil, headers: {}, body: nil, content_type:, limit:)
|
|
79
|
+
services = {"/git-upload-pack" => "git-upload-pack", "/git-receive-pack" => "git-receive-pack"}
|
|
80
|
+
service = services[suffix]
|
|
81
|
+
raise TransportError, "invalid SSH request" unless method == :post && service && query.nil? && body
|
|
82
|
+
|
|
83
|
+
received = 0
|
|
84
|
+
with_process(service, check_status: true) do |stdin, stdout|
|
|
85
|
+
discard_advertisement(stdout)
|
|
86
|
+
stdin.write(body)
|
|
87
|
+
stdin.close
|
|
88
|
+
while (chunk = stdout.read(65_536))
|
|
89
|
+
received += chunk.bytesize
|
|
90
|
+
raise TransportError, "SSH response exceeds size limit" if received > limit
|
|
91
|
+
|
|
92
|
+
yield chunk
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
received
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def discard_advertisement(stdout)
|
|
99
|
+
reader = Protocol::Reader.new(stdout, max_bytes: MAX_ADVERTISEMENT_SIZE)
|
|
100
|
+
loop do
|
|
101
|
+
packet = reader.read
|
|
102
|
+
return if packet == Protocol::FLUSH
|
|
103
|
+
raise TransportError, "truncated SSH advertisement" unless packet.is_a?(String)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def with_process(service, check_status: false)
|
|
108
|
+
process = start_process(service)
|
|
109
|
+
result = Timeout.timeout(@timeout) do
|
|
110
|
+
value = yield(process[:stdin], process[:stdout])
|
|
111
|
+
if check_status
|
|
112
|
+
status = process[:waiter].value
|
|
113
|
+
raise TransportError, "SSH transport failed" unless status.success?
|
|
114
|
+
end
|
|
115
|
+
value
|
|
116
|
+
end
|
|
117
|
+
result
|
|
118
|
+
rescue Timeout::Error
|
|
119
|
+
raise TransportError, "SSH transport timed out", cause: nil
|
|
120
|
+
rescue TransportError
|
|
121
|
+
raise
|
|
122
|
+
rescue Errno::ENOENT
|
|
123
|
+
raise TransportError, "SSH executable not found", cause: nil
|
|
124
|
+
rescue IOError, SystemCallError
|
|
125
|
+
raise TransportError, "SSH transport failed", cause: nil
|
|
126
|
+
ensure
|
|
127
|
+
@process_lock&.synchronize { @process = nil if @process.equal?(process) }
|
|
128
|
+
cleanup(process)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def start_process(service)
|
|
132
|
+
ensure_open
|
|
133
|
+
grouped = !Gem.win_platform?
|
|
134
|
+
options = grouped ? {pgroup: true} : {}
|
|
135
|
+
stdin, stdout, stderr, waiter = Open3.popen3(process_environment, *command(service), **options)
|
|
136
|
+
[stdin, stdout, stderr].each(&:binmode)
|
|
137
|
+
process = {stdin: stdin, stdout: stdout, stderr: stderr, waiter: waiter, grouped: grouped}
|
|
138
|
+
process[:errors] = Thread.new { while stderr.read(4096); end rescue nil }
|
|
139
|
+
@process_lock.synchronize do
|
|
140
|
+
if @closed
|
|
141
|
+
cleanup(process)
|
|
142
|
+
raise TransportError, "connection is closed"
|
|
143
|
+
end
|
|
144
|
+
@process = process
|
|
145
|
+
end
|
|
146
|
+
process
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def command(service)
|
|
150
|
+
destination = @user ? "#{@user}@#{@host}" : @host
|
|
151
|
+
port = @port ? ["-p", @port.to_s] : []
|
|
152
|
+
@ssh_command + ["-o", "BatchMode=yes", *port, "--", destination,
|
|
153
|
+
"#{service} #{Shellwords.escape(@path)}"]
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def process_environment
|
|
157
|
+
{"GIT_PROTOCOL" => nil, "GIT_TERMINAL_PROMPT" => "0", "GIT_TRACE" => nil,
|
|
158
|
+
"GIT_TRACE2" => nil, "GIT_TRACE_PACKET" => nil}
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def safe_message(_data) = "remote reported an error"
|
|
162
|
+
|
|
163
|
+
def cleanup(process)
|
|
164
|
+
return unless process
|
|
165
|
+
|
|
166
|
+
[process[:stdin], process[:stdout], process[:stderr]].each { |io| io.close unless io.closed? }
|
|
167
|
+
terminate(process[:waiter], process[:grouped]) if process[:waiter].alive? && !process[:waiter].join(0.2)
|
|
168
|
+
process[:errors]&.join
|
|
169
|
+
rescue IOError, Errno::ECHILD
|
|
170
|
+
nil
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def terminate(waiter, grouped)
|
|
174
|
+
unless grouped
|
|
175
|
+
Process.kill("KILL", waiter.pid)
|
|
176
|
+
waiter.join
|
|
177
|
+
return
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
target = -waiter.pid
|
|
181
|
+
Process.kill("TERM", target)
|
|
182
|
+
return if waiter.join(0.2)
|
|
183
|
+
|
|
184
|
+
Process.kill("KILL", target)
|
|
185
|
+
waiter.join
|
|
186
|
+
rescue Errno::EPERM
|
|
187
|
+
begin
|
|
188
|
+
Process.kill("TERM", waiter.pid)
|
|
189
|
+
Process.kill("KILL", waiter.pid) unless waiter.join(0.2)
|
|
190
|
+
waiter.join
|
|
191
|
+
rescue SystemCallError
|
|
192
|
+
nil
|
|
193
|
+
end
|
|
194
|
+
rescue SystemCallError
|
|
195
|
+
nil
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def parse_ssh_url(url)
|
|
199
|
+
raise TypeError unless url.is_a?(String) && url.bytesize <= 8192
|
|
200
|
+
|
|
201
|
+
target = url.match?(/\Assh:\/\//i) ? parse_uri(url) : parse_scp(url)
|
|
202
|
+
user, host, port, path = target
|
|
203
|
+
raise ArgumentError unless (!user || USER_PATTERN.match?(user)) && HOST_PATTERN.match?(host) &&
|
|
204
|
+
(!port || (1..65_535).cover?(port)) && PATH_PATTERN.match?(path) && !path.start_with?("-")
|
|
205
|
+
|
|
206
|
+
target
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def parse_uri(url)
|
|
210
|
+
uri = URI.parse(url)
|
|
211
|
+
raise AuthenticationError, "passwords in SSH remote URLs are not supported" if uri.password
|
|
212
|
+
raise ArgumentError unless uri.scheme == "ssh" && uri.host && !uri.host.empty? &&
|
|
213
|
+
uri.path && !uri.path.empty? && !uri.query && !uri.fragment
|
|
214
|
+
|
|
215
|
+
[decode(uri.user), uri.host, uri.port, decode(uri.path)]
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def parse_scp(url)
|
|
219
|
+
match = /\A(?:(?<user>[A-Za-z0-9][A-Za-z0-9._-]{0,63})@)?(?<host>\[[0-9A-Fa-f:.]+\]|[^:]+):(?<path>.+)\z/n.match(url)
|
|
220
|
+
raise ArgumentError unless match
|
|
221
|
+
|
|
222
|
+
[match[:user], match[:host], nil, match[:path]]
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def decode(value)
|
|
226
|
+
value && URI::RFC2396_PARSER.unescape(value)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def parse_ssh_command(value)
|
|
230
|
+
value = ENV["GIT_SSH_COMMAND"] if value.nil? && !ENV["GIT_SSH_COMMAND"].to_s.empty?
|
|
231
|
+
arguments = case value
|
|
232
|
+
when nil then ["ssh"]
|
|
233
|
+
when String then Shellwords.split(value)
|
|
234
|
+
when Array then value.dup
|
|
235
|
+
else raise TypeError
|
|
236
|
+
end
|
|
237
|
+
valid = arguments.length.between?(1, MAX_COMMAND_ARGUMENTS) && arguments.all? do |argument|
|
|
238
|
+
argument.is_a?(String) && !argument.empty? && argument.bytesize <= MAX_COMMAND_ARGUMENT_SIZE &&
|
|
239
|
+
!argument.match?(/[\0\r\n]/)
|
|
240
|
+
end
|
|
241
|
+
raise ArgumentError unless valid
|
|
242
|
+
|
|
243
|
+
arguments.map! { |argument| argument.dup.freeze }
|
|
244
|
+
arguments.freeze
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Thuban
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
class TransportError < Error; end
|
|
6
|
+
class AuthenticationError < TransportError; end
|
|
7
|
+
|
|
8
|
+
Ref = Struct.new(:name, :oid, :symref_target, :peeled, keyword_init: true)
|
|
9
|
+
Progress = Struct.new(:phase, :current, :total, :bytes, keyword_init: true)
|
|
10
|
+
|
|
11
|
+
module Remote
|
|
12
|
+
def self.open(url, credentials: nil, ssh: nil, timeout: 30)
|
|
13
|
+
local_drive = url.is_a?(String) && url.match?(/\A[A-Za-z]:[\\\/]/)
|
|
14
|
+
local_path = local_drive || (url.is_a?(String) && url.start_with?("/", "./", "../", "~"))
|
|
15
|
+
if url.is_a?(String) && (url.match?(/\Assh:\/\//i) || (!local_path && !url.include?("://") && url.include?(":")))
|
|
16
|
+
return SSHConnection.new(url, credentials: credentials, ssh: ssh, timeout: timeout)
|
|
17
|
+
end
|
|
18
|
+
raise ArgumentError, "ssh configuration requires an SSH remote" if ssh
|
|
19
|
+
|
|
20
|
+
if url.is_a?(String) && !url.match?(/\Ahttps?:\/\//i)
|
|
21
|
+
return LocalConnection.new(url, credentials: credentials, timeout: timeout)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
Connection.new(url, credentials: credentials, timeout: timeout)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/thuban/version.rb
CHANGED
data/lib/thuban.rb
CHANGED
|
@@ -25,3 +25,13 @@ require_relative "thuban/commit_writer"
|
|
|
25
25
|
require_relative "thuban/history_writer"
|
|
26
26
|
require_relative "thuban/stash"
|
|
27
27
|
require_relative "thuban/pack_writer"
|
|
28
|
+
require_relative "thuban/pack_reader"
|
|
29
|
+
require_relative "thuban/remote"
|
|
30
|
+
require_relative "thuban/remote/credentials"
|
|
31
|
+
require_relative "thuban/remote/protocol"
|
|
32
|
+
require_relative "thuban/remote/http"
|
|
33
|
+
require_relative "thuban/remote/fetch"
|
|
34
|
+
require_relative "thuban/remote/push"
|
|
35
|
+
require_relative "thuban/remote/ssh"
|
|
36
|
+
require_relative "thuban/remote/local"
|
|
37
|
+
require_relative "thuban/remote/repository"
|
data/sig/thuban.rbs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
module Thuban
|
|
2
2
|
VERSION: String
|
|
3
3
|
type object = [String, String]
|
|
4
|
+
type push_update = [String, String?, String]
|
|
4
5
|
|
|
5
6
|
interface _Differ
|
|
6
7
|
def edits: (Porrima::lines, Porrima::lines) -> Array[Porrima::Edit]
|
|
@@ -10,12 +11,72 @@ module Thuban
|
|
|
10
11
|
def write: (String) -> Integer
|
|
11
12
|
end
|
|
12
13
|
|
|
14
|
+
interface _PackInput
|
|
15
|
+
def read: (Integer) -> String?
|
|
16
|
+
end
|
|
17
|
+
|
|
13
18
|
class CorruptObject < StandardError
|
|
14
19
|
end
|
|
15
20
|
|
|
16
21
|
class RefLockError < StandardError
|
|
17
22
|
end
|
|
18
23
|
|
|
24
|
+
class Error < StandardError
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class TransportError < Error
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class AuthenticationError < TransportError
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class Ref < Struct[untyped]
|
|
34
|
+
attr_accessor name: String
|
|
35
|
+
attr_accessor oid: String?
|
|
36
|
+
attr_accessor symref_target: String?
|
|
37
|
+
attr_accessor peeled: String?
|
|
38
|
+
def self.new: (name: String, oid: String?, ?symref_target: String?, ?peeled: String?) -> instance
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class Progress < Struct[untyped]
|
|
42
|
+
attr_accessor phase: Symbol
|
|
43
|
+
attr_accessor current: Integer?
|
|
44
|
+
attr_accessor total: Integer?
|
|
45
|
+
attr_accessor bytes: Integer?
|
|
46
|
+
def self.new: (phase: Symbol, ?current: Integer?, ?total: Integer?, ?bytes: Integer?) -> instance
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
module Remote
|
|
50
|
+
type ssh_command = String | Array[String]
|
|
51
|
+
def self.open: (String url, ?credentials: untyped, ?ssh: ssh_command?, ?timeout: Numeric) -> Connection
|
|
52
|
+
|
|
53
|
+
module Credentials
|
|
54
|
+
MAX_HELPER_OUTPUT: Integer
|
|
55
|
+
def self.static: (username: String, password: String) -> untyped
|
|
56
|
+
def self.bearer: (token: String) -> untyped
|
|
57
|
+
def self.helper: (?String? name) -> untyped
|
|
58
|
+
def self.callback: () { (String) -> untyped } -> untyped
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class Connection
|
|
62
|
+
def initialize: (String url, ?credentials: untyped, ?timeout: Numeric) -> void
|
|
63
|
+
def refs: () -> Array[Ref]
|
|
64
|
+
def fetch: (Repository repository, wants: Enumerable[String], ?haves: Enumerable[String], ?depth: Integer?, ?filter: String?) ?{ (Progress) -> void } -> Array[String]
|
|
65
|
+
def push: (Repository repository, Enumerable[push_update] updates, ?atomic: bool) ?{ (Progress) -> void } -> Array[Ref]
|
|
66
|
+
def close: () -> nil
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
class SSHConnection < Connection
|
|
70
|
+
MAX_COMMAND_ARGUMENTS: Integer
|
|
71
|
+
MAX_COMMAND_ARGUMENT_SIZE: Integer
|
|
72
|
+
def initialize: (String url, ?credentials: untyped, ?ssh: ssh_command?, ?timeout: Numeric) -> void
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class LocalConnection < SSHConnection
|
|
76
|
+
def initialize: (String url, ?credentials: untyped, ?timeout: Numeric) -> void
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
19
80
|
class Commit < Struct[untyped]
|
|
20
81
|
attr_accessor oid: String
|
|
21
82
|
attr_accessor tree: String?
|
|
@@ -96,6 +157,9 @@ module Thuban
|
|
|
96
157
|
def stash_push: (?message: String?, ?include_untracked: bool) -> String?
|
|
97
158
|
def stash_pop: (?Integer index) -> String
|
|
98
159
|
def stash_list: () -> Array[Commit]
|
|
160
|
+
def remotes: () -> Hash[String, String]
|
|
161
|
+
def fetch: (?String remote, ?refspecs: String | Enumerable[String]?, ?depth: Integer?, ?filter: String?) ?{ (Progress) -> void } -> Array[Ref]
|
|
162
|
+
def push: (?String remote, refspecs: String | Enumerable[String], ?force: bool, ?lease: String | Hash[String, String]?, ?atomic: bool) ?{ (Progress) -> void } -> Array[Ref]
|
|
99
163
|
end
|
|
100
164
|
|
|
101
165
|
class ObjectDatabase
|
|
@@ -128,6 +192,7 @@ module Thuban
|
|
|
128
192
|
def read: (String oid) ?{ (String) -> object } -> object
|
|
129
193
|
def self.apply_delta: (String base, String delta) -> String
|
|
130
194
|
def self.write: (_PackOutput io, Enumerable[object] objects) ?{ (Integer current, Integer total) -> void } -> String
|
|
195
|
+
def self.read_stream: (_PackInput io, ObjectDatabase odb) ?{ (Integer current, Integer total) -> void } -> Array[String]
|
|
131
196
|
end
|
|
132
197
|
|
|
133
198
|
class Index
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: thuban
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yudai Takada
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.
|
|
18
|
+
version: 0.2.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.
|
|
25
|
+
version: 0.2.0
|
|
26
26
|
email:
|
|
27
27
|
- t.yudai92@gmail.com
|
|
28
28
|
executables: []
|
|
@@ -44,9 +44,19 @@ files:
|
|
|
44
44
|
- lib/thuban/object_database.rb
|
|
45
45
|
- lib/thuban/object_writer.rb
|
|
46
46
|
- lib/thuban/pack.rb
|
|
47
|
+
- lib/thuban/pack_reader.rb
|
|
47
48
|
- lib/thuban/pack_writer.rb
|
|
48
49
|
- lib/thuban/ref_lock_error.rb
|
|
49
50
|
- lib/thuban/ref_store.rb
|
|
51
|
+
- lib/thuban/remote.rb
|
|
52
|
+
- lib/thuban/remote/credentials.rb
|
|
53
|
+
- lib/thuban/remote/fetch.rb
|
|
54
|
+
- lib/thuban/remote/http.rb
|
|
55
|
+
- lib/thuban/remote/local.rb
|
|
56
|
+
- lib/thuban/remote/protocol.rb
|
|
57
|
+
- lib/thuban/remote/push.rb
|
|
58
|
+
- lib/thuban/remote/repository.rb
|
|
59
|
+
- lib/thuban/remote/ssh.rb
|
|
50
60
|
- lib/thuban/repository.rb
|
|
51
61
|
- lib/thuban/signature.rb
|
|
52
62
|
- lib/thuban/stash.rb
|
|
@@ -79,5 +89,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
79
89
|
requirements: []
|
|
80
90
|
rubygems_version: 3.6.9
|
|
81
91
|
specification_version: 4
|
|
82
|
-
summary: A pure Ruby implementation for local
|
|
92
|
+
summary: A pure Ruby Git implementation for local repositories and remote transfers
|
|
83
93
|
test_files: []
|