git_dump 0.1.0

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