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