git_dump 0.1.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 +15 -0
- data/.gitignore +14 -0
- data/.rubocop.yml +62 -0
- data/.travis.yml +37 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +20 -0
- data/README.markdown +75 -0
- data/git_dump.gemspec +22 -0
- data/lib/git_dump.rb +41 -0
- data/lib/git_dump/cmd.rb +118 -0
- data/lib/git_dump/entry.rb +31 -0
- data/lib/git_dump/path_object.rb +11 -0
- data/lib/git_dump/repo.rb +42 -0
- data/lib/git_dump/repo/git.rb +331 -0
- data/lib/git_dump/repo/rugged.rb +97 -0
- data/lib/git_dump/tree.rb +31 -0
- data/lib/git_dump/tree/base.rb +54 -0
- data/lib/git_dump/tree/builder.rb +62 -0
- data/lib/git_dump/version.rb +55 -0
- data/lib/git_dump/version/base.rb +21 -0
- data/lib/git_dump/version/builder.rb +77 -0
- data/script/benchmark +81 -0
- data/spec/git_dump_spec.rb +392 -0
- data/spec/spec_helper.rb +0 -0
- metadata +97 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
begin
|
2
|
+
require 'git_dump/repo/rugged'
|
3
|
+
rescue LoadError
|
4
|
+
require 'git_dump/repo/git'
|
5
|
+
end
|
6
|
+
require 'git_dump/cmd'
|
7
|
+
require 'git_dump/version'
|
8
|
+
require 'git_dump/version/builder'
|
9
|
+
|
10
|
+
class GitDump
|
11
|
+
# Main class: create/initialize repository, find versions, provide interface
|
12
|
+
# to git
|
13
|
+
class Repo
|
14
|
+
include defined?(Rugged) ? Rugged : Git
|
15
|
+
|
16
|
+
class << self
|
17
|
+
# List remote version ids
|
18
|
+
alias_method :remote_version_ids, :remote_tag_names
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :path
|
22
|
+
|
23
|
+
def initialize(path, options)
|
24
|
+
@path = path
|
25
|
+
resolve(path, options)
|
26
|
+
end
|
27
|
+
|
28
|
+
# New version builder
|
29
|
+
def new_version
|
30
|
+
Version::Builder.new(self)
|
31
|
+
end
|
32
|
+
|
33
|
+
# List of versions
|
34
|
+
def versions
|
35
|
+
Version.list(self)
|
36
|
+
end
|
37
|
+
|
38
|
+
def inspect
|
39
|
+
"#<#{self.class} path=#{path}>"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,331 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
class GitDump
|
5
|
+
class Repo
|
6
|
+
# Interface to git using system calls and pipes
|
7
|
+
module Git
|
8
|
+
# Exception during initialization
|
9
|
+
class InitException < StandardError; end
|
10
|
+
|
11
|
+
def self.included(base)
|
12
|
+
base.extend(ClassMethods)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Methods which work outside of git repository
|
16
|
+
module ClassMethods
|
17
|
+
# List remote tag names
|
18
|
+
def remote_tag_names(url)
|
19
|
+
Cmd.git('ls-remote', '--tags', url).stripped_lines.map do |line|
|
20
|
+
if (m = %r!^[0-9a-f]{40}\trefs/tags/(.*)$!.match(line))
|
21
|
+
m[1]
|
22
|
+
else
|
23
|
+
fail "Unexpected: #{line}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Add blob for content to repository, return sha
|
30
|
+
def data_sha(content)
|
31
|
+
@data_sha_command ||= git(*%w[hash-object -w --no-filters --stdin])
|
32
|
+
@data_sha_command.popen('r+') do |f|
|
33
|
+
if content.respond_to?(:read)
|
34
|
+
f.write(content.read(4096)) until content.eof?
|
35
|
+
else
|
36
|
+
f.write(content)
|
37
|
+
end
|
38
|
+
f.close_write
|
39
|
+
f.read.chomp
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Add blob for content at path to repository, return sha
|
44
|
+
def path_sha(path)
|
45
|
+
@path_sha_pipe ||=
|
46
|
+
git(*%w[hash-object -w --no-filters --stdin-paths]).popen('r+')
|
47
|
+
@path_sha_pipe.puts(path)
|
48
|
+
@path_sha_pipe.gets.chomp
|
49
|
+
end
|
50
|
+
|
51
|
+
# Add blob for entries to repository, return sha
|
52
|
+
# Each entry is a hash with following keys:
|
53
|
+
# :type => :blob or :tree
|
54
|
+
# :name => name string
|
55
|
+
# :sha => sha of content
|
56
|
+
# :mode => last three octets of mode
|
57
|
+
def treeify(entries)
|
58
|
+
@treefier ||= git('mktree', '--batch').popen('r+')
|
59
|
+
entries.map do |entry|
|
60
|
+
values = normalize_entry(entry).values_at(:mode, :type, :sha, :name)
|
61
|
+
line = format("%06o %s %s\t%s", *values)
|
62
|
+
@treefier.puts line
|
63
|
+
end
|
64
|
+
@treefier.puts
|
65
|
+
@treefier.gets.chomp
|
66
|
+
end
|
67
|
+
|
68
|
+
# Create commit for tree_sha, return sha
|
69
|
+
# options:
|
70
|
+
# :time => author date (by default now)
|
71
|
+
# :message => commit message (by default empty)
|
72
|
+
def commit(tree_sha, options = {})
|
73
|
+
env = git_env({
|
74
|
+
:author_date => options[:time],
|
75
|
+
:author_name => options[:name],
|
76
|
+
:author_email => options[:email],
|
77
|
+
:committer_name => options[:name],
|
78
|
+
:committer_email => options[:email],
|
79
|
+
})
|
80
|
+
|
81
|
+
args = %w[commit-tree]
|
82
|
+
args << '-F' << '-' if options[:message]
|
83
|
+
args << tree_sha << {:env => env, :no_stdin => !options[:message]}
|
84
|
+
|
85
|
+
git(*args).popen(options[:message] ? 'r+' : 'r') do |f|
|
86
|
+
if options[:message]
|
87
|
+
f.write options[:message]
|
88
|
+
f.close_write
|
89
|
+
end
|
90
|
+
f.read.chomp
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Create tag for commit_sha with name constructed from name_parts, return
|
95
|
+
# name. name_parts can be an array or a string separated by /
|
96
|
+
# options:
|
97
|
+
# :time => tagger date
|
98
|
+
# :message => tag message (by default empty)
|
99
|
+
def tag(commit_sha, name_parts, options = {})
|
100
|
+
name = tag_name_from_parts(name_parts)
|
101
|
+
|
102
|
+
env = git_env({
|
103
|
+
:committer_date => options[:time],
|
104
|
+
:committer_name => options[:name],
|
105
|
+
:committer_email => options[:email],
|
106
|
+
})
|
107
|
+
|
108
|
+
args = %w[tag]
|
109
|
+
args << '-F' << '-' << '--cleanup=verbatim' if options[:message]
|
110
|
+
args << name << commit_sha << {:env => env}
|
111
|
+
|
112
|
+
git(*args).popen(options[:message] ? 'r+' : 'r') do |f|
|
113
|
+
if options[:message]
|
114
|
+
f.write options[:message]
|
115
|
+
f.close_write
|
116
|
+
end
|
117
|
+
f.read.chomp
|
118
|
+
end
|
119
|
+
|
120
|
+
name
|
121
|
+
end
|
122
|
+
|
123
|
+
# Return pipe with contents of blob identified by sha
|
124
|
+
def blob_pipe(sha, &block)
|
125
|
+
git('cat-file', 'blob', sha).popen('rb', &block)
|
126
|
+
end
|
127
|
+
|
128
|
+
# Return contents of blob identified by sha
|
129
|
+
# If io is specified, then content will be written to io
|
130
|
+
def blob_read(sha, io = nil)
|
131
|
+
@blob_read_pipe ||= git(*%w[cat-file --batch]).popen('rb+')
|
132
|
+
@blob_read_pipe.puts(sha)
|
133
|
+
size = @blob_read_pipe.gets.split(' ')[2].to_i
|
134
|
+
result = if io
|
135
|
+
while size > 0
|
136
|
+
chunk = [size, 4096].min
|
137
|
+
io.write(@blob_read_pipe.read(chunk))
|
138
|
+
size -= chunk
|
139
|
+
end
|
140
|
+
io
|
141
|
+
else
|
142
|
+
@blob_read_pipe.read(size)
|
143
|
+
end
|
144
|
+
@blob_read_pipe.gets
|
145
|
+
result
|
146
|
+
end
|
147
|
+
|
148
|
+
# Write contents of blob to file at path and set its mode
|
149
|
+
def blob_unpack(sha, path, mode)
|
150
|
+
Tempfile.open('git_dump', File.dirname(path)) do |temp|
|
151
|
+
temp.binmode
|
152
|
+
blob_read(sha, temp)
|
153
|
+
temp.close
|
154
|
+
|
155
|
+
File.chmod(mode, temp.path)
|
156
|
+
File.rename(temp.path, path)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Read tree at sha returning list of entries
|
161
|
+
# Each entry is a hash like one for treeify
|
162
|
+
def tree_entries(sha)
|
163
|
+
git('ls-tree', sha).stripped_lines.map do |line|
|
164
|
+
if (m = /^(\d{6}) (blob|tree) ([0-9a-f]{40})\t(.*)$/.match(line))
|
165
|
+
{
|
166
|
+
:mode => m[1].to_i(8),
|
167
|
+
:type => m[2].to_sym,
|
168
|
+
:sha => m[3],
|
169
|
+
:name => m[4],
|
170
|
+
}
|
171
|
+
else
|
172
|
+
fail "Unexpected: #{line}"
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
TAG_ENTRIES_FIELDS = %w[
|
178
|
+
%(objecttype)%00
|
179
|
+
%(objectname)%00
|
180
|
+
%(refname)%00
|
181
|
+
%(authordate:rfc2822)%(*authordate:rfc2822)%00
|
182
|
+
%(committerdate:rfc2822)%(*committerdate:rfc2822)%00
|
183
|
+
%(contents)%00
|
184
|
+
%(*contents)%00
|
185
|
+
]
|
186
|
+
|
187
|
+
# Return list of entries per tag ref
|
188
|
+
# Each entry is a hash with following keys:
|
189
|
+
# :sha => tag or commit sha
|
190
|
+
# :name => ref name
|
191
|
+
def tag_entries
|
192
|
+
ref_fields(TAG_ENTRIES_FIELDS, 'refs/tags').map do |values|
|
193
|
+
{
|
194
|
+
:sha => values[1],
|
195
|
+
:name => values[2].sub(%r{\Arefs/tags/}, ''),
|
196
|
+
:author_time => Time.rfc2822(values[3]),
|
197
|
+
:commit_time => Time.rfc2822(values[4]),
|
198
|
+
:tag_message => values[0] == 'tag' ? values[5] : nil,
|
199
|
+
:commit_message => values[0] == 'tag' ? values[6] : values[5],
|
200
|
+
}
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
# Remove tag with name id
|
205
|
+
def remove_tag(id)
|
206
|
+
args = %W[tag --delete #{id}]
|
207
|
+
args << {:no_stdout => true}
|
208
|
+
git(*args).run
|
209
|
+
end
|
210
|
+
|
211
|
+
# Receive tag with name id from repo at url
|
212
|
+
# Use :progress => true to show progress
|
213
|
+
def fetch(url, id, options = {})
|
214
|
+
transfer(:fetch, url, id, options)
|
215
|
+
end
|
216
|
+
|
217
|
+
# Send tag with name id to repo at url
|
218
|
+
# Use :progress => true to show progress
|
219
|
+
def push(url, id, options = {})
|
220
|
+
transfer(:push, url, id, options)
|
221
|
+
end
|
222
|
+
|
223
|
+
# Run garbage collection
|
224
|
+
# Use :auto => true to run only if GC is required
|
225
|
+
# Use :aggressive => true to run GC more aggressively
|
226
|
+
def gc(options = {})
|
227
|
+
args = %w[gc --quiet]
|
228
|
+
args << '--auto' if options[:auto]
|
229
|
+
args << '--aggressive' if options[:aggressive]
|
230
|
+
git(*args).run
|
231
|
+
end
|
232
|
+
|
233
|
+
private
|
234
|
+
|
235
|
+
# Construct git command specifying git-dir
|
236
|
+
def git(command, *args)
|
237
|
+
Cmd.git("--git-dir=#{@git_dir}", command, *args)
|
238
|
+
end
|
239
|
+
|
240
|
+
def git_env(options)
|
241
|
+
env = {}
|
242
|
+
[:author, :committer].each do |role|
|
243
|
+
[:name, :email, :date].each do |part|
|
244
|
+
value = options[:"#{role}_#{part}"]
|
245
|
+
next unless value
|
246
|
+
value = value.strftime('%s %z') if part == :date
|
247
|
+
env["GIT_#{role}_#{part}".upcase] = value
|
248
|
+
end
|
249
|
+
end
|
250
|
+
env
|
251
|
+
end
|
252
|
+
|
253
|
+
def normalize_entry(entry)
|
254
|
+
out = {
|
255
|
+
:type => entry[:type].to_sym,
|
256
|
+
:name => entry[:name].to_s,
|
257
|
+
:sha => entry[:sha].to_s,
|
258
|
+
}
|
259
|
+
|
260
|
+
out[:mode] = if out[:type] == :tree
|
261
|
+
0o040_000
|
262
|
+
else
|
263
|
+
(entry[:mode] & 0100) == 0 ? 0o100_644 : 0o100_755
|
264
|
+
end
|
265
|
+
|
266
|
+
unless out[:sha] =~ /\A[0-9a-f]{40}\z/
|
267
|
+
fail "Expected sha1 hash, got #{out[:sha]}"
|
268
|
+
end
|
269
|
+
|
270
|
+
out
|
271
|
+
end
|
272
|
+
|
273
|
+
def tag_name_from_parts(parts)
|
274
|
+
parts = parts.split('/') unless parts.is_a?(Array)
|
275
|
+
|
276
|
+
parts.map do |part|
|
277
|
+
part.gsub(/[^a-zA-Z0-9\-_,]+/, '_')
|
278
|
+
end.reject(&:empty?).join('/')
|
279
|
+
end
|
280
|
+
|
281
|
+
def ref_fields(fields, pattern)
|
282
|
+
list = []
|
283
|
+
format = fields.join
|
284
|
+
git('for-each-ref', "--format=#{format}", pattern).popen do |io|
|
285
|
+
until io.eof?
|
286
|
+
list << Array.new(fields.length){ io.gets("\0").chomp("\0") }
|
287
|
+
io.gets
|
288
|
+
end
|
289
|
+
end
|
290
|
+
list
|
291
|
+
end
|
292
|
+
|
293
|
+
def transfer(command, url, id, options)
|
294
|
+
ref = "refs/tags/#{id}"
|
295
|
+
args = %W[#{command} --no-tags #{url} #{ref}:#{ref}]
|
296
|
+
args << '--quiet' unless options[:progress]
|
297
|
+
git(*args).run
|
298
|
+
end
|
299
|
+
|
300
|
+
def resolve(path, options)
|
301
|
+
create(path, options) unless File.exist?(path)
|
302
|
+
|
303
|
+
unless File.directory?(path)
|
304
|
+
fail InitException, "#{path} is not a directory"
|
305
|
+
end
|
306
|
+
|
307
|
+
begin
|
308
|
+
options = {:chdir => path, :no_stderr => true}
|
309
|
+
relative = Cmd.git('rev-parse', '--git-dir', options).capture.strip
|
310
|
+
rescue Cmd::Failure => e
|
311
|
+
raise InitException, e.message, e.backtrace
|
312
|
+
end
|
313
|
+
|
314
|
+
@git_dir = File.expand_path(relative, path)
|
315
|
+
end
|
316
|
+
|
317
|
+
def create(path, options)
|
318
|
+
unless options[:create]
|
319
|
+
fail InitException, "#{path} does not exist and got no :create option"
|
320
|
+
end
|
321
|
+
|
322
|
+
bare_arg = options[:create] != :non_bare ? '--bare' : '--no-bare'
|
323
|
+
begin
|
324
|
+
Cmd.git('init', '-q', bare_arg, path, :no_stderr => true).run
|
325
|
+
rescue Cmd::Failure => e
|
326
|
+
raise InitException, e.message, e.backtrace
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'rugged'
|
2
|
+
require 'git_dump/repo/git'
|
3
|
+
|
4
|
+
class GitDump
|
5
|
+
class Repo
|
6
|
+
# Interface to git using libgit2 through rugged
|
7
|
+
module Rugged
|
8
|
+
include Git
|
9
|
+
|
10
|
+
def self.included(base)
|
11
|
+
base.extend(ClassMethods)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Add blob for content to repository, return sha
|
15
|
+
def data_sha(content)
|
16
|
+
if content.respond_to?(:read)
|
17
|
+
::Rugged::Blob.from_io(repo, content)
|
18
|
+
else
|
19
|
+
::Rugged::Blob.from_buffer(repo, content)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Add blob for content at path to repository, return sha
|
24
|
+
def path_sha(path)
|
25
|
+
::Rugged::Blob.from_disk(repo, path)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Add blob for entries to repository, return sha
|
29
|
+
# Each entry is a hash with following keys:
|
30
|
+
# :type => :blob or :tree
|
31
|
+
# :name => name string
|
32
|
+
# :sha => sha of content
|
33
|
+
# :mode => last three octets of mode
|
34
|
+
def treeify(entries)
|
35
|
+
builder = ::Rugged::Tree::Builder.new
|
36
|
+
entries.map do |entry|
|
37
|
+
entry = normalize_entry(entry)
|
38
|
+
builder << {
|
39
|
+
:type => entry[:type],
|
40
|
+
:name => entry[:name],
|
41
|
+
:oid => entry[:sha],
|
42
|
+
:filemode => entry[:mode],
|
43
|
+
}
|
44
|
+
end
|
45
|
+
builder.write(repo)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Return contents of blob identified by sha
|
49
|
+
# If io is specified, then content will be written to io
|
50
|
+
def blob_read(sha, io = nil)
|
51
|
+
if io
|
52
|
+
super(sha, io)
|
53
|
+
else
|
54
|
+
::Rugged::Object.new(repo, sha).content
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Read tree at sha returning list of entries
|
59
|
+
# Each entry is a hash like one for treeify
|
60
|
+
def tree_entries(sha)
|
61
|
+
object = repo.lookup(sha)
|
62
|
+
tree = object.type == :tree ? object : object.tree
|
63
|
+
tree.map do |entry|
|
64
|
+
{
|
65
|
+
:type => entry[:type],
|
66
|
+
:name => entry[:name],
|
67
|
+
:sha => entry[:oid],
|
68
|
+
:mode => entry[:filemode],
|
69
|
+
}
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Return list of entries per tag ref
|
74
|
+
# Each entry is a hash with following keys:
|
75
|
+
# :sha => tag or commit sha
|
76
|
+
# :name => ref name
|
77
|
+
def tag_entries
|
78
|
+
repo.tags.map do |tag|
|
79
|
+
{
|
80
|
+
:name => tag.name,
|
81
|
+
:sha => tag.target.oid,
|
82
|
+
:author_time => tag.target.author[:time],
|
83
|
+
:commit_time => tag.target.committer[:time],
|
84
|
+
:tag_message => tag.annotation && tag.annotation.message,
|
85
|
+
:commit_message => tag.target.message,
|
86
|
+
}
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def repo
|
93
|
+
@repo ||= ::Rugged::Repository.new(path)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|