git_dump 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|