gitrb 0.0.1

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,373 @@
1
+ require 'zlib'
2
+ require 'digest/sha1'
3
+ require 'yaml'
4
+ require 'fileutils'
5
+ require 'logger'
6
+
7
+ require 'gitrb/repository'
8
+ require 'gitrb/object'
9
+ require 'gitrb/blob'
10
+ require 'gitrb/diff'
11
+ require 'gitrb/tree'
12
+ require 'gitrb/tag'
13
+ require 'gitrb/user'
14
+ require 'gitrb/pack'
15
+ require 'gitrb/commit'
16
+ require 'gitrb/trie'
17
+
18
+ module Gitrb
19
+ class NotFound < StandardError; end
20
+
21
+ class Repository
22
+ attr_reader :path, :index, :root, :branch, :lock_file, :head, :bare
23
+
24
+ # Initialize a repository.
25
+ def initialize(options = {})
26
+ @bare = options[:bare] || false
27
+ @branch = options[:branch] || 'master'
28
+ @logger = options[:logger] || Logger.new(nil)
29
+
30
+ @path = options[:path]
31
+ @path.chomp!('/')
32
+ @path += '/.git' if !@bare
33
+
34
+ if options[:create] && !File.exists?("#{@path}/objects")
35
+ FileUtils.mkpath(@path) if !File.exists?(@path)
36
+ raise ArgumentError, "Not a valid Git repository: '#{@path}'" if !File.directory?(@path)
37
+ if @bare
38
+ Dir.chdir(@path) { git_init('--bare') }
39
+ else
40
+ Dir.chdir(@path[0..-6]) { git_init }
41
+ end
42
+ else
43
+ raise ArgumentError, "Not a valid Git repository: '#{@path}'" if !File.directory?("#{@path}/objects")
44
+ end
45
+
46
+ load_packs
47
+ load
48
+ end
49
+
50
+ # Switch branch
51
+ def branch=(branch)
52
+ @branch = branch
53
+ load
54
+ end
55
+
56
+ # Has our repository been changed on disk?
57
+ def changed?
58
+ head.nil? or head.id != read_head_id
59
+ end
60
+
61
+ # Load the repository, if it has been changed on disk.
62
+ def refresh
63
+ load if changed?
64
+ end
65
+
66
+ # Is there any transaction going on?
67
+ def in_transaction?
68
+ Thread.current['gitrb_repository_lock']
69
+ end
70
+
71
+ # Diff
72
+ def diff(from, to, path = nil)
73
+ from = from.id if Commit === from
74
+ to = to.id if Commit === to
75
+ Diff.new(from, to, git_diff('--full-index', from, to, '--', path))
76
+ end
77
+
78
+ # All changes made inside a transaction are atomic. If some
79
+ # exception occurs the transaction will be rolled back.
80
+ #
81
+ # Example:
82
+ # repository.transaction { repository['a'] = 'b' }
83
+ #
84
+ def transaction(message = "")
85
+ start_transaction
86
+ result = yield
87
+ commit(message)
88
+ result
89
+ rescue
90
+ rollback
91
+ raise
92
+ ensure
93
+ finish_transaction
94
+ end
95
+
96
+ # Start a transaction.
97
+ #
98
+ # Tries to get lock on lock file, load the this repository if
99
+ # has changed in the repository.
100
+ def start_transaction
101
+ file = File.open("#{head_path}.lock", "w")
102
+ file.flock(File::LOCK_EX)
103
+ Thread.current['gitrb_repository_lock'] = file
104
+ refresh
105
+ end
106
+
107
+ # Rerepository the state of the repository.
108
+ #
109
+ # Any changes made to the repository are discarded.
110
+ def rollback
111
+ @objects.clear
112
+ load
113
+ finish_transaction
114
+ end
115
+
116
+ # Finish the transaction.
117
+ #
118
+ # Release the lock file.
119
+ def finish_transaction
120
+ Thread.current['gitrb_repository_lock'].close rescue nil
121
+ Thread.current['gitrb_repository_lock'] = nil
122
+ File.unlink("#{head_path}.lock") rescue nil
123
+ end
124
+
125
+ # Write a commit object to disk and set the head of the current branch.
126
+ #
127
+ # Returns the commit object
128
+ def commit(message = '', author = nil, committer = nil)
129
+ return if !root.modified?
130
+
131
+ author ||= default_user
132
+ committer ||= author
133
+ root.save
134
+
135
+ commit = Commit.new(:repository => self,
136
+ :tree => root,
137
+ :parent => head,
138
+ :author => author,
139
+ :committer => committer,
140
+ :message => message)
141
+ commit.save
142
+
143
+ write_head_id(commit.id)
144
+ load
145
+
146
+ commit
147
+ end
148
+
149
+ # Returns a list of commits starting from head commit.
150
+ def log(limit = 10, start = nil, path = nil)
151
+ args = ['--format=tformat:%H%n%P%n%T%n%an%n%ae%n%at%n%cn%n%ce%n%ct%n%x00%s%n%b%x00', "-#{limit}", ]
152
+ args << start if start
153
+ args << "--" << path if path
154
+ log = git_log(*args).split(/\n*\x00\n*/)
155
+ commits = []
156
+ log.each_slice(2) do |data, message|
157
+ data = data.split("\n")
158
+ commits << Commit.new(:repository => self,
159
+ :id => data[0],
160
+ :parent => data[1].empty? ? nil : Reference.new(:repository => self, :id => data[1]),
161
+ :tree => Reference.new(:repository => self, :id => data[2]),
162
+ :author => User.new(data[3], data[4], Time.at(data[5].to_i)),
163
+ :committer => User.new(data[6], data[7], Time.at(data[8].to_i)),
164
+ :message => message.strip)
165
+ end
166
+ commits
167
+ rescue => ex
168
+ return [] if ex.message =~ /bad default revision 'HEAD'/
169
+ raise
170
+ end
171
+
172
+ # Get an object by its id.
173
+ #
174
+ # Returns a tree, blob, commit or tag object.
175
+ def get(id)
176
+ return nil if id.nil? || id.length < 5
177
+ list = @objects.find(id).to_a
178
+ return list.first if list.size == 1
179
+
180
+ @logger.debug "gitrb: Loading #{id}"
181
+
182
+ path = object_path(id)
183
+ if File.exists?(path) || (glob = Dir.glob(path + '*')).size >= 1
184
+ if glob
185
+ raise NotFound, "Sha not unique" if glob.size > 1
186
+ path = glob[0]
187
+ end
188
+
189
+ buf = File.open(path, "rb") { |f| f.read }
190
+
191
+ raise NotFound, "Not a loose object: #{id}" if !legacy_loose_object?(buf)
192
+
193
+ header, content = Zlib::Inflate.inflate(buf).split("\0", 2)
194
+ type, size = header.split(' ', 2)
195
+
196
+ raise NotFound, "Bad object: #{id}" if content.length != size.to_i
197
+ else
198
+ list = @packs.find(id).to_a
199
+ return nil if list.size != 1
200
+
201
+ pack, offset = list.first
202
+ content, type = pack.get_object(offset)
203
+ end
204
+
205
+ raise NotFound, "Object not found" if !type
206
+
207
+ @logger.debug "gitrb: Loaded #{id}"
208
+
209
+ object = Gitrb::Object.factory(type, :repository => self, :id => id, :data => content)
210
+ @objects.insert(id, object)
211
+ object
212
+ end
213
+
214
+ def get_tree(id) get_type(id, 'tree') end
215
+ def get_blob(id) get_type(id, 'blob') end
216
+ def get_commit(id) get_type(id, 'commit') end
217
+
218
+ # Write a raw object to the repository.
219
+ #
220
+ # Returns the object.
221
+ def put(object)
222
+ content = object.dump
223
+ data = "#{object.type} #{content.bytesize rescue content.length}\0#{content}"
224
+ id = sha(data)
225
+ path = object_path(id)
226
+
227
+ @logger.debug "gitrb: Storing #{id}"
228
+
229
+ if !File.exists?(path)
230
+ FileUtils.mkpath(File.dirname(path))
231
+ File.open(path, 'wb') do |f|
232
+ f.write Zlib::Deflate.deflate(data)
233
+ end
234
+ end
235
+
236
+ @logger.debug "gitrb: Stored #{id}"
237
+
238
+ object.repository = self
239
+ object.id = id
240
+ @objects.insert(id, object)
241
+
242
+ object
243
+ end
244
+
245
+ def method_missing(name, *args, &block)
246
+ cmd = name.to_s
247
+ if cmd[0..3] == 'git_'
248
+ ENV['GIT_DIR'] = path
249
+ args = args.flatten.compact.map {|s| "'" + s.to_s.gsub("'", "'\\\\''") + "'" }.join(' ')
250
+ cmd = cmd[4..-1].tr('_', '-')
251
+ cmd = "git #{cmd} #{args} 2>&1"
252
+
253
+ @logger.debug "gitrb: #{cmd}"
254
+
255
+ out = if block_given?
256
+ IO.popen(cmd, &block)
257
+ else
258
+ `#{cmd}`.chomp
259
+ end
260
+
261
+ if $?.exitstatus > 0
262
+ return '' if $?.exitstatus == 1 && out == ''
263
+ raise RuntimeError, "#{cmd}: #{out}"
264
+ end
265
+
266
+ out
267
+ else
268
+ super
269
+ end
270
+ end
271
+
272
+ def default_user
273
+ name = git_config('user.name').chomp
274
+ email = git_config('user.email').chomp
275
+ if name.empty?
276
+ require 'etc'
277
+ user = Etc.getpwnam(Etc.getlogin)
278
+ name = user.gecos
279
+ end
280
+ if email.empty?
281
+ require 'etc'
282
+ email = Etc.getlogin + '@' + `hostname -f`.chomp
283
+ end
284
+ User.new(name, email)
285
+ end
286
+
287
+ def dup
288
+ super.instance_eval do
289
+ @objects = Trie.new
290
+ load
291
+ self
292
+ end
293
+ end
294
+
295
+ protected
296
+
297
+ def get_type(id, expected)
298
+ object = get(id)
299
+ raise NotFound, "Wrong type #{object.type}, expected #{expected}" if object && object.type != expected
300
+ object
301
+ end
302
+
303
+ def load_packs
304
+ @packs = Trie.new
305
+ @objects = Trie.new
306
+
307
+ packs_path = "#{@path}/objects/pack"
308
+ if File.directory?(packs_path)
309
+ Dir.open(packs_path) do |dir|
310
+ entries = dir.select { |entry| entry =~ /\.pack$/i }
311
+ entries.each do |entry|
312
+ @logger.debug "gitrb: Loading pack #{entry}"
313
+ pack = Pack.new(File.join(packs_path, entry))
314
+ pack.each_object {|id, offset| @packs.insert(id, [pack, offset]) }
315
+ end
316
+ end
317
+ end
318
+ end
319
+
320
+ def load
321
+ if id = read_head_id
322
+ @head = get_commit(id)
323
+ @root = @head.tree
324
+ else
325
+ @head = nil
326
+ @root = Tree.new(:repository => self)
327
+ end
328
+ @logger.debug "gitrb: Reloaded, head is #{@head ? head.id : 'nil'}"
329
+ end
330
+
331
+ # Returns the hash value of an object string.
332
+ def sha(str)
333
+ Digest::SHA1.hexdigest(str)[0, 40]
334
+ end
335
+
336
+ # Returns the path to the current head file.
337
+ def head_path
338
+ "#{path}/refs/heads/#{branch}"
339
+ end
340
+
341
+ # Returns the path to the object file for given id.
342
+ def object_path(id)
343
+ "#{path}/objects/#{id[0...2]}/#{id[2..39]}"
344
+ end
345
+
346
+ # Read the id of the head commit.
347
+ #
348
+ # Returns the object id of the last commit.
349
+ def read_head_id
350
+ if File.exists?(head_path)
351
+ File.read(head_path).strip
352
+ elsif File.exists?("#{path}/packed-refs")
353
+ File.open("#{path}/packed-refs", "rb") do |io|
354
+ while line = io.gets
355
+ line.strip!
356
+ next if line[0..0] == '#'
357
+ line = line.split(' ')
358
+ return line[0] if line[1] == "refs/heads/#{branch}"
359
+ end
360
+ end
361
+ end
362
+ end
363
+
364
+ def write_head_id(id)
365
+ File.open(head_path, "wb") {|file| file.write(id) }
366
+ end
367
+
368
+ def legacy_loose_object?(buf)
369
+ buf.getord(0) == 0x78 && ((buf.getord(0) << 8) + buf.getord(1)) % 31 == 0
370
+ end
371
+
372
+ end
373
+ end
data/lib/gitrb/tag.rb ADDED
@@ -0,0 +1,35 @@
1
+ module Gitrb
2
+
3
+ class Tag < Gitrb::Object
4
+ attr_accessor :object, :tagtype, :tagger, :message
5
+
6
+ def initialize(options = {})
7
+ super(options)
8
+ parse(options[:data]) if options[:data]
9
+ end
10
+
11
+ def ==(other)
12
+ Tag === other and id == other.id
13
+ end
14
+
15
+ def parse(data)
16
+ headers, @message = data.split("\n\n", 2)
17
+
18
+ headers.split("\n").each do |header|
19
+ key, value = header.split(' ', 2)
20
+ case key
21
+ when 'type'
22
+ @tagtype = value
23
+ when 'object'
24
+ @object = Reference.new(:repository => repository, :id => value)
25
+ when 'tagger'
26
+ @tagger = User.parse(value)
27
+ end
28
+ end
29
+
30
+ self
31
+ end
32
+
33
+ end
34
+
35
+ end
data/lib/gitrb/tree.rb ADDED
@@ -0,0 +1,163 @@
1
+ class StringIO
2
+ if RUBY_VERSION > '1.9'
3
+ def read_bytes_until(char)
4
+ str = ''
5
+ while ((ch = getc) != char) && !eof
6
+ str << ch
7
+ end
8
+ str
9
+ end
10
+ else
11
+ def read_bytes_until(char)
12
+ str = ''
13
+ while ((ch = getc.chr) != char) && !eof
14
+ str << ch
15
+ end
16
+ str
17
+ end
18
+ end
19
+ end
20
+
21
+ module Gitrb
22
+
23
+ class Tree < Gitrb::Object
24
+ include Enumerable
25
+
26
+ attr_accessor :mode, :repository
27
+
28
+ # Initialize a tree
29
+ def initialize(options = {})
30
+ super(options)
31
+ @children = {}
32
+ @mode = options[:mode] || "040000"
33
+ parse(options[:data]) if options[:data]
34
+ @modified = true if !id
35
+ end
36
+
37
+ def type
38
+ 'tree'
39
+ end
40
+
41
+ def ==(other)
42
+ Tree === other && id == other.id
43
+ end
44
+
45
+ # Set new repository (modified flag is reset)
46
+ def id=(id)
47
+ super
48
+ @modified = false
49
+ end
50
+
51
+ # Has this tree been modified?
52
+ def modified?
53
+ @modified || @children.values.any? { |entry| entry.type == 'tree' && entry.modified? }
54
+ end
55
+
56
+ def dump
57
+ @children.to_a.sort {|a,b| a.first <=> b.first }.map do |name, child|
58
+ child.save if !(Reference === child) || child.resolved?
59
+ "#{child.mode} #{name}\0#{[child.id].pack("H*")}"
60
+ end.join
61
+ end
62
+
63
+ # Save this treetree back to the git repository.
64
+ #
65
+ # Returns the object id of the tree.
66
+ def save
67
+ repository.put(self) if modified?
68
+ id
69
+ end
70
+
71
+ # Read entry with specified name.
72
+ def get(name)
73
+ @children[name]
74
+ end
75
+
76
+ # Write entry with specified name.
77
+ def put(name, value)
78
+ raise RuntimeError, "no blob or tree" if !(Blob === value || Tree === value)
79
+ value.repository = repository
80
+ @modified = true
81
+ @children[name] = value
82
+ value
83
+ end
84
+
85
+ # Remove entry with specified name.
86
+ def remove(name)
87
+ @modified = true
88
+ @children.delete(name.to_s)
89
+ end
90
+
91
+ # Does this key exist in the children?
92
+ def has_key?(name)
93
+ @children.has_key?(name.to_s)
94
+ end
95
+
96
+ def normalize_path(path)
97
+ (path[0, 1] == '/' ? path[1..-1] : path).split('/')
98
+ end
99
+
100
+ # Read a value on specified path.
101
+ def [](path)
102
+ return self if path.empty?
103
+ normalize_path(path).inject(self) do |tree, key|
104
+ raise RuntimeError, 'Not a tree' if tree.type != 'tree'
105
+ tree.get(key) or return nil
106
+ end
107
+ end
108
+
109
+ # Write a value on specified path.
110
+ def []=(path, value)
111
+ list = normalize_path(path)
112
+ tree = list[0..-2].to_a.inject(self) do |tree, name|
113
+ raise RuntimeError, 'Not a tree' if tree.type != 'tree'
114
+ tree.get(name) || tree.put(name, Tree.new(:repository => repository))
115
+ end
116
+ tree.put(list.last, value)
117
+ end
118
+
119
+ # Delete a value on specified path.
120
+ def delete(path)
121
+ list = normalize_path(path)
122
+
123
+ tree = list[0..-2].to_a.inject(self) do |tree, key|
124
+ tree.get(key) or return
125
+ end
126
+
127
+ tree.remove(list.last)
128
+ end
129
+
130
+ # Iterate over all children
131
+ def each(&block)
132
+ @children.sort.each do |name, child|
133
+ yield(name, child)
134
+ end
135
+ end
136
+
137
+ def names
138
+ @children.keys.sort
139
+ end
140
+
141
+ def values
142
+ map { |name, child| child }
143
+ end
144
+
145
+ alias children values
146
+
147
+ private
148
+
149
+ # Read the contents of a raw git object.
150
+ def parse(data)
151
+ @children.clear
152
+ data = StringIO.new(data)
153
+ while !data.eof?
154
+ mode = data.read_bytes_until(' ')
155
+ name = data.read_bytes_until("\0")
156
+ id = data.read(20).unpack("H*").first
157
+ @children[name] = Reference.new(:repository => repository, :id => id, :mode => mode)
158
+ end
159
+ end
160
+
161
+ end
162
+
163
+ end
data/lib/gitrb/trie.rb ADDED
@@ -0,0 +1,75 @@
1
+ module Gitrb
2
+ class Trie
3
+ include Enumerable
4
+
5
+ attr_reader :key, :value
6
+
7
+ def initialize(key = '', value = nil, children = [])
8
+ @key = key
9
+ @value = value
10
+ @children = children
11
+ end
12
+
13
+ def clear
14
+ @key = ''
15
+ @value = nil
16
+ @children.clear
17
+ end
18
+
19
+ def find(key)
20
+ if key.empty?
21
+ self
22
+ else
23
+ child = @children[key[0].ord]
24
+ if child && key[0...child.key.length] == child.key
25
+ child.find(key[child.key.length..-1])
26
+ else
27
+ nil
28
+ end
29
+ end
30
+ end
31
+
32
+ def insert(key, value)
33
+ if key.empty?
34
+ @value = value
35
+ self
36
+ else
37
+ idx = key[0].ord
38
+ child = @children[idx]
39
+ if child
40
+ child.split(key) if key[0...child.key.length] != child.key
41
+ child.insert(key[child.key.length..-1], value)
42
+ else
43
+ @children[idx] = Trie.new(key, value)
44
+ end
45
+ end
46
+ end
47
+
48
+ def each(&block)
49
+ yield(@value) if !@key.empty?
50
+ @children.compact.each {|c| c.each(&block) }
51
+ end
52
+
53
+ def values
54
+ to_a
55
+ end
56
+
57
+ def dup
58
+ Trie.new(@key.dup, @value ? @value.dup : nil, @children.map {|c| c ? c.dup : nil })
59
+ end
60
+
61
+ def inspect
62
+ "#<Gitrb::Trie @key=#{@key.inspect}, @value=#{@value.inspect}, @children=#{@children.compact.inspect}>"
63
+ end
64
+
65
+ def split(key)
66
+ prefix = 0
67
+ prefix += 1 while key[prefix] == @key[prefix]
68
+ child = Trie.new(@key[prefix..-1], @value, @children)
69
+ @children = []
70
+ @children[@key[prefix].ord] = child
71
+ @key = @key[0...prefix]
72
+ @value = nil
73
+ end
74
+ end
75
+ end
data/lib/gitrb/user.rb ADDED
@@ -0,0 +1,22 @@
1
+ module Gitrb
2
+
3
+ class User
4
+ attr_accessor :name, :email, :date
5
+
6
+ def initialize(name, email, date = Time.now)
7
+ @name, @email, @date = name, email, date
8
+ end
9
+
10
+ def dump
11
+ "#{name} <#{email}> #{date.localtime.to_i} #{date.gmt_offset < 0 ? '-' : '+'}#{date.gmt_offset / 60}"
12
+ end
13
+
14
+ def self.parse(user)
15
+ if match = user.match(/(.*)<(.*)> (\d+) ([+-]?\d+)/)
16
+ new match[1].strip, match[2].strip, Time.at(match[3].to_i)
17
+ end
18
+ end
19
+
20
+ end
21
+
22
+ end
@@ -0,0 +1,30 @@
1
+ require "#{File.dirname(__FILE__)}/../lib/gitrb"
2
+ require "#{File.dirname(__FILE__)}/helper"
3
+ require 'pp'
4
+
5
+ describe Gitrb do
6
+
7
+ REPO = '/tmp/gitrb_test.git'
8
+
9
+ attr_reader :repo
10
+
11
+ before(:each) do
12
+ FileUtils.rm_rf REPO
13
+ Dir.mkdir REPO
14
+
15
+ @repo = Gitrb::Repository.new(:path => REPO, :create => true)
16
+ end
17
+
18
+ it 'should fail to initialize without a valid git repository' do
19
+ lambda {
20
+ Gitrb::Repository.new('/foo', 'master', true)
21
+ }.should raise_error(ArgumentError)
22
+ end
23
+
24
+ it 'should save and load entries' do
25
+ repo.root['a'] = Gitrb::Blob.new(:data => 'Hello')
26
+ repo.commit
27
+
28
+ repo.root['a'].data.should == 'Hello'
29
+ end
30
+ end