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.
@@ -0,0 +1,11 @@
1
+ class GitDump
2
+ # Base class for Tree, Tree::Builder and Entry
3
+ class PathObject
4
+ attr_reader :repo, :path, :name
5
+ def initialize(repo, dir, name)
6
+ @repo = repo
7
+ @path = dir ? "#{dir}/#{name}" : name if name
8
+ @name = name
9
+ end
10
+ end
11
+ end
@@ -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