gitrb 0.0.1

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