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.
- data/LICENSE +22 -0
- data/README.md +1 -0
- data/Rakefile +36 -0
- data/gitrb.gemspec +38 -0
- data/lib/gitrb/blob.rb +35 -0
- data/lib/gitrb/commit.rb +71 -0
- data/lib/gitrb/diff.rb +21 -0
- data/lib/gitrb/object.rb +59 -0
- data/lib/gitrb/pack.rb +347 -0
- data/lib/gitrb/repository.rb +373 -0
- data/lib/gitrb/tag.rb +35 -0
- data/lib/gitrb/tree.rb +163 -0
- data/lib/gitrb/trie.rb +75 -0
- data/lib/gitrb/user.rb +22 -0
- data/test/bare_repository_spec.rb +30 -0
- data/test/benchmark.rb +39 -0
- data/test/commit_spec.rb +73 -0
- data/test/repository_spec.rb +235 -0
- data/test/tree_spec.rb +75 -0
- data/test/trie_spec.rb +26 -0
- metadata +72 -0
@@ -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
|