git_store 0.3.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,425 @@
1
+ #
2
+ # converted from the gitrb project
3
+ #
4
+ # authors:
5
+ # Matthias Lederhofer <matled@gmx.net>
6
+ # Simon 'corecode' Schubert <corecode@fs.ei.tum.de>
7
+ # Scott Chacon <schacon@gmail.com>
8
+ #
9
+ # provides native ruby access to git objects and pack files
10
+ #
11
+
12
+ require 'zlib'
13
+
14
+ class GitStore
15
+ PACK_SIGNATURE = "PACK"
16
+ PACK_IDX_SIGNATURE = "\377tOc"
17
+ 85
18
+ OBJ_NONE = 0
19
+ OBJ_COMMIT = 1
20
+ OBJ_TREE = 2
21
+ OBJ_BLOB = 3
22
+ OBJ_TAG = 4
23
+
24
+ OBJ_TYPES = [nil, 'commit', 'tree', 'blob', 'tag'].freeze
25
+
26
+ class Mmap
27
+ def initialize(file, version = 1)
28
+ @file = file
29
+ @offset = nil
30
+ if version == 2
31
+ @global_offset = 8
32
+ else
33
+ @global_offset = 0
34
+ end
35
+ end
36
+
37
+ def unmap
38
+ @file = nil
39
+ end
40
+
41
+ def [](*idx)
42
+ idx = idx[0] if idx.length == 1
43
+ case idx
44
+ when Range
45
+ offset = idx.first
46
+ len = idx.last - idx.first + idx.exclude_end? ? 0 : 1
47
+ when Fixnum
48
+ offset = idx
49
+ len = nil
50
+ when Array
51
+ offset, len = idx
52
+ else
53
+ raise RuntimeError, "invalid index param: #{idx.class}"
54
+ end
55
+ if @offset != offset
56
+ @file.seek(offset + @global_offset)
57
+ end
58
+ @offset = offset + len ? len : 1
59
+ if not len
60
+ @file.read(1)[0]
61
+ else
62
+ @file.read(len)
63
+ end
64
+ end
65
+ end
66
+
67
+ class PackFormatError < StandardError
68
+ end
69
+
70
+ class PackStorage
71
+ OBJ_OFS_DELTA = 6
72
+ OBJ_REF_DELTA = 7
73
+
74
+ FanOutCount = 256
75
+ SHA1Size = 20
76
+ IdxOffsetSize = 4
77
+ OffsetSize = 4
78
+ CrcSize = 4
79
+ OffsetStart = FanOutCount * IdxOffsetSize
80
+ SHA1Start = OffsetStart + OffsetSize
81
+ EntrySize = OffsetSize + SHA1Size
82
+ EntrySizeV2 = SHA1Size + CrcSize + OffsetSize
83
+
84
+ def initialize(file)
85
+ if file =~ /\.idx$/
86
+ file = file[0...-3] + 'pack'
87
+ end
88
+ @name = file
89
+ @cache = {}
90
+ init_pack
91
+ end
92
+
93
+ def with_idx(index_file = nil)
94
+ if !index_file
95
+ index_file = @name
96
+ idxfile = File.open(@name[0...-4]+'idx')
97
+ else
98
+ idxfile = File.open(index_file)
99
+ end
100
+
101
+ # read header
102
+ sig = idxfile.read(4)
103
+ ver = idxfile.read(4).unpack("N")[0]
104
+
105
+ if sig == PACK_IDX_SIGNATURE
106
+ if(ver != 2)
107
+ raise PackFormatError, "pack #@name has unknown pack file version #{ver}"
108
+ end
109
+ @version = 2
110
+ else
111
+ @version = 1
112
+ end
113
+
114
+ idx = Mmap.new(idxfile, @version)
115
+ yield idx
116
+ idx.unmap
117
+ idxfile.close
118
+ end
119
+
120
+ def with_packfile
121
+ packfile = File.open(@name)
122
+ result = yield packfile
123
+ packfile.close
124
+
125
+ result
126
+ end
127
+
128
+ def cache_objects
129
+ @cache = {}
130
+ with_packfile do |packfile|
131
+ each_entry do |sha, offset|
132
+ data, type = unpack_object(packfile, offset, {:caching => true})
133
+ if data
134
+ @cache[sha] = [type, data]
135
+ end
136
+ end
137
+ end
138
+ end
139
+
140
+ def name
141
+ @name
142
+ end
143
+
144
+ def close
145
+ # shouldnt be anything open now
146
+ end
147
+
148
+ # given an index file, list out the shas that it's packfile contains
149
+ def get_shas
150
+ shas = []
151
+ each_sha1 { |sha| shas << sha.unpack("H*")[0] }
152
+ shas
153
+ end
154
+
155
+ def [](sha1)
156
+ if obj = @cache[sha1]
157
+ return obj
158
+ end
159
+
160
+ offset = find_object(sha1)
161
+ return nil if !offset
162
+ @cache[sha1] = obj = parse_object(offset)
163
+ return obj
164
+ end
165
+
166
+ def init_pack
167
+ with_idx do |idx|
168
+ @offsets = [0]
169
+ FanOutCount.times do |i|
170
+ pos = idx[i * IdxOffsetSize,IdxOffsetSize].unpack('N')[0]
171
+ if pos < @offsets[i]
172
+ raise PackFormatError, "pack #@name has discontinuous index #{i}"
173
+ end
174
+ @offsets << pos
175
+ end
176
+ @size = @offsets[-1]
177
+ end
178
+ end
179
+
180
+ def each_entry
181
+ with_idx do |idx|
182
+ if @version == 2
183
+ data = read_data_v2(idx)
184
+ data.each do |sha1, crc, offset|
185
+ yield sha1, offset
186
+ end
187
+ else
188
+ pos = OffsetStart
189
+ @size.times do
190
+ offset = idx[pos,OffsetSize].unpack('N')[0]
191
+ sha1 = idx[pos+OffsetSize,SHA1Size]
192
+ pos += EntrySize
193
+ yield sha1, offset
194
+ end
195
+ end
196
+ end
197
+ end
198
+
199
+ def read_data_v2(idx)
200
+ data = []
201
+ pos = OffsetStart
202
+ @size.times do |i|
203
+ data[i] = [idx[pos,SHA1Size], 0, 0]
204
+ pos += SHA1Size
205
+ end
206
+ @size.times do |i|
207
+ crc = idx[pos,CrcSize]
208
+ data[i][1] = crc
209
+ pos += CrcSize
210
+ end
211
+ @size.times do |i|
212
+ offset = idx[pos,OffsetSize].unpack('N')[0]
213
+ data[i][2] = offset
214
+ pos += OffsetSize
215
+ end
216
+ data
217
+ end
218
+ private :read_data_v2
219
+
220
+ def each_sha1
221
+ with_idx do |idx|
222
+ if @version == 2
223
+ data = read_data_v2(idx)
224
+ data.each do |sha1, crc, offset|
225
+ yield sha1
226
+ end
227
+ else
228
+ pos = SHA1Start
229
+ @size.times do
230
+ sha1 = idx[pos,SHA1Size]
231
+ pos += EntrySize
232
+ yield sha1
233
+ end
234
+ end
235
+ end
236
+ end
237
+
238
+ def find_object_in_index(idx, sha1)
239
+ slot = sha1[0]
240
+ return nil if !slot
241
+ first, last = @offsets[slot,2]
242
+ while first < last
243
+ mid = (first + last) / 2
244
+ if @version == 2
245
+ midsha1 = idx[OffsetStart + (mid * SHA1Size), SHA1Size]
246
+ cmp = midsha1 <=> sha1
247
+
248
+ if cmp < 0
249
+ first = mid + 1
250
+ elsif cmp > 0
251
+ last = mid
252
+ else
253
+ pos = OffsetStart + (@size * (SHA1Size + CrcSize)) + (mid * OffsetSize)
254
+ offset = idx[pos, OffsetSize].unpack('N')[0]
255
+ return offset
256
+ end
257
+ else
258
+ midsha1 = idx[SHA1Start + mid * EntrySize,SHA1Size]
259
+ cmp = midsha1 <=> sha1
260
+
261
+ if cmp < 0
262
+ first = mid + 1
263
+ elsif cmp > 0
264
+ last = mid
265
+ else
266
+ pos = OffsetStart + mid * EntrySize
267
+ offset = idx[pos,OffsetSize].unpack('N')[0]
268
+ return offset
269
+ end
270
+ end
271
+ end
272
+ nil
273
+ end
274
+
275
+ def find_object(sha1)
276
+ obj = nil
277
+ with_idx do |idx|
278
+ obj = find_object_in_index(idx, sha1)
279
+ end
280
+ obj
281
+ end
282
+ private :find_object
283
+
284
+ def parse_object(offset)
285
+ data, type = with_packfile do |packfile|
286
+ unpack_object(packfile, offset)
287
+ end
288
+
289
+ return data, OBJ_TYPES[type]
290
+ end
291
+
292
+ def unpack_object(packfile, offset, options = {})
293
+ obj_offset = offset
294
+ packfile.seek(offset)
295
+
296
+ c = packfile.read(1)[0]
297
+ size = c & 0xf
298
+ type = (c >> 4) & 7
299
+ shift = 4
300
+ offset += 1
301
+ while c & 0x80 != 0
302
+ c = packfile.read(1)[0]
303
+ size |= ((c & 0x7f) << shift)
304
+ shift += 7
305
+ offset += 1
306
+ end
307
+
308
+ return [false, false] if !(type == OBJ_COMMIT || type == OBJ_TREE) && options[:caching]
309
+
310
+ case type
311
+ when OBJ_OFS_DELTA, OBJ_REF_DELTA
312
+ data, type = unpack_deltified(packfile, type, offset, obj_offset, size, options)
313
+ #puts type
314
+ when OBJ_COMMIT, OBJ_TREE, OBJ_BLOB, OBJ_TAG
315
+ data = unpack_compressed(offset, size)
316
+ else
317
+ raise PackFormatError, "invalid type #{type}"
318
+ end
319
+ [data, type]
320
+ end
321
+ private :unpack_object
322
+
323
+ def unpack_deltified(packfile, type, offset, obj_offset, size, options = {})
324
+ packfile.seek(offset)
325
+ data = packfile.read(SHA1Size)
326
+
327
+ if type == OBJ_OFS_DELTA
328
+ i = 0
329
+ c = data[i]
330
+ base_offset = c & 0x7f
331
+ while c & 0x80 != 0
332
+ c = data[i += 1]
333
+ base_offset += 1
334
+ base_offset <<= 7
335
+ base_offset |= c & 0x7f
336
+ end
337
+ base_offset = obj_offset - base_offset
338
+ offset += i + 1
339
+ else
340
+ base_offset = find_object(data)
341
+ offset += SHA1Size
342
+ end
343
+
344
+ base, type = unpack_object(packfile, base_offset)
345
+
346
+ return [false, false] if !(type == OBJ_COMMIT || type == OBJ_TREE) && options[:caching]
347
+
348
+ delta = unpack_compressed(offset, size)
349
+ [patch_delta(base, delta), type]
350
+ end
351
+ private :unpack_deltified
352
+
353
+ def unpack_compressed(offset, destsize)
354
+ outdata = ""
355
+ with_packfile do |packfile|
356
+ packfile.seek(offset)
357
+ zstr = Zlib::Inflate.new
358
+ while outdata.size < destsize
359
+ indata = packfile.read(4096)
360
+ if indata.size == 0
361
+ raise PackFormatError, 'error reading pack data'
362
+ end
363
+ outdata += zstr.inflate(indata)
364
+ end
365
+ if outdata.size > destsize
366
+ raise PackFormatError, 'error reading pack data'
367
+ end
368
+ zstr.close
369
+ end
370
+ outdata
371
+ end
372
+ private :unpack_compressed
373
+
374
+ def patch_delta(base, delta)
375
+ src_size, pos = patch_delta_header_size(delta, 0)
376
+ if src_size != base.size
377
+ raise PackFormatError, 'invalid delta data'
378
+ end
379
+
380
+ dest_size, pos = patch_delta_header_size(delta, pos)
381
+ dest = ""
382
+ while pos < delta.size
383
+ c = delta[pos]
384
+ pos += 1
385
+ if c & 0x80 != 0
386
+ pos -= 1
387
+ cp_off = cp_size = 0
388
+ cp_off = delta[pos += 1] if c & 0x01 != 0
389
+ cp_off |= delta[pos += 1] << 8 if c & 0x02 != 0
390
+ cp_off |= delta[pos += 1] << 16 if c & 0x04 != 0
391
+ cp_off |= delta[pos += 1] << 24 if c & 0x08 != 0
392
+ cp_size = delta[pos += 1] if c & 0x10 != 0
393
+ cp_size |= delta[pos += 1] << 8 if c & 0x20 != 0
394
+ cp_size |= delta[pos += 1] << 16 if c & 0x40 != 0
395
+ cp_size = 0x10000 if cp_size == 0
396
+ pos += 1
397
+ dest += base[cp_off,cp_size]
398
+ elsif c != 0
399
+ dest += delta[pos,c]
400
+ pos += c
401
+ else
402
+ raise PackFormatError, 'invalid delta data'
403
+ end
404
+ end
405
+ dest
406
+ end
407
+ private :patch_delta
408
+
409
+ def patch_delta_header_size(delta, pos)
410
+ size = 0
411
+ shift = 0
412
+ begin
413
+ c = delta[pos]
414
+ if c == nil
415
+ raise PackFormatError, 'invalid delta header'
416
+ end
417
+ pos += 1
418
+ size |= (c & 0x7f) << shift
419
+ shift += 7
420
+ end while c & 0x80 != 0
421
+ [size, pos]
422
+ end
423
+ private :patch_delta_header_size
424
+ end
425
+ end
@@ -0,0 +1,40 @@
1
+ class GitStore
2
+
3
+ class Tag
4
+ attr_accessor :store, :id, :object, :type, :tagger, :message
5
+
6
+ def initialize(store, id = nil, data = nil)
7
+ @store = store
8
+ @id = id
9
+
10
+ parse(data) if data
11
+ end
12
+
13
+ def ==(other)
14
+ Tag === other and id == other.id
15
+ end
16
+
17
+ def parse(data)
18
+ headers, @message = data.split(/\n\n/, 2)
19
+
20
+ headers.split(/\n/).each do |header|
21
+ key, value = header.split(/ /, 2)
22
+ case key
23
+ when 'type'
24
+ @type = value
25
+
26
+ when 'object'
27
+ @object = store.get(value)
28
+
29
+ when 'tagger'
30
+ @tagger = User.parse(value)
31
+
32
+ end
33
+ end
34
+
35
+ self
36
+ end
37
+
38
+ end
39
+
40
+ end
@@ -0,0 +1,183 @@
1
+ class GitStore
2
+
3
+ class Tree
4
+ include Enumerable
5
+
6
+ attr_reader :store, :table
7
+ attr_accessor :id, :data, :mode
8
+
9
+ # Initialize a tree
10
+ def initialize(store, id = nil, data = nil)
11
+ @store = store
12
+ @id = id
13
+ @table = {}
14
+ @mode = "040000"
15
+ parse(data) if data
16
+ end
17
+
18
+ def ==(other)
19
+ Tree === other and id == other.id
20
+ end
21
+
22
+ # Has this tree been modified?
23
+ def modified?
24
+ @modified or @table.values.any? { |entry| Tree === entry and entry.modified? }
25
+ end
26
+
27
+ # Find or create a subtree with specified name.
28
+ def tree(name)
29
+ get(name) or put(name, Tree.new(store))
30
+ end
31
+
32
+ # Read the contents of a raw git object.
33
+ def parse(data)
34
+ @table.clear
35
+
36
+ while data.size > 0
37
+ mode, data = data.split(" ", 2)
38
+ name, data = data.split("\0", 2)
39
+ id = data.slice!(0, 20).unpack("H*").first
40
+
41
+ @table[name] = store.get(id)
42
+ end
43
+ end
44
+
45
+ def dump
46
+ @table.map { |k, v| "#{ v.mode } #{ k }\0#{ [v.write].pack("H*") }" }.join
47
+ end
48
+
49
+ # Write this treetree back to the git repository.
50
+ #
51
+ # Returns the object id of the tree.
52
+ def write
53
+ return id if not modified?
54
+ @modified = false
55
+ @id = store.put(self)
56
+ end
57
+
58
+ # Read entry with specified name.
59
+ def get(name)
60
+ entry = @table[name]
61
+
62
+ case entry
63
+ when Blob
64
+ entry.object ||= handler_for(name).read(entry.data)
65
+
66
+ when Tree
67
+ entry
68
+ end
69
+ end
70
+
71
+ def handler_for(name)
72
+ store.handler_for(name)
73
+ end
74
+
75
+ # Write entry with specified name.
76
+ def put(name, value)
77
+ @modified = true
78
+
79
+ if value.is_a?(Tree)
80
+ @table[name] = value
81
+ else
82
+ @table[name] = Blob.new(store, nil, handler_for(name).write(value))
83
+ end
84
+
85
+ value
86
+ end
87
+
88
+ # Remove entry with specified name.
89
+ def remove(name)
90
+ @modified = true
91
+ @table.delete(name.to_s)
92
+ end
93
+
94
+ # Does this key exist in the table?
95
+ def has_key?(name)
96
+ @table.has_key?(name.to_s)
97
+ end
98
+
99
+ def normalize_path(path)
100
+ (path[0, 1] == '/' ? path[1..-1] : path).split('/')
101
+ end
102
+
103
+ # Read a value on specified path.
104
+ def [](path)
105
+ normalize_path(path).inject(self) do |tree, key|
106
+ tree.get(key) or return nil
107
+ end
108
+ end
109
+
110
+ # Write a value on specified path.
111
+ def []=(path, value)
112
+ list = normalize_path(path)
113
+ tree = list[0..-2].to_a.inject(self) { |tree, name| tree.tree(name) }
114
+ tree.put(list.last, value)
115
+ end
116
+
117
+ # Delete a value on specified path.
118
+ def delete(path)
119
+ list = normalize_path(path)
120
+
121
+ tree = list[0..-2].to_a.inject(self) do |tree, key|
122
+ tree.get(key) or return
123
+ end
124
+
125
+ tree.remove(list.last)
126
+ end
127
+
128
+ # Iterate over all objects found in this subtree.
129
+ def each(path = [], &block)
130
+ @table.sort.each do |name, entry|
131
+ child_path = path + [name]
132
+ case entry
133
+ when Blob
134
+ entry.object ||= handler_for(name).read(entry.data)
135
+ yield child_path.join("/"), entry.object
136
+
137
+ when Tree
138
+ entry.each(child_path, &block)
139
+ end
140
+ end
141
+ end
142
+
143
+ def each_blob(path = [], &block)
144
+ @table.sort.each do |name, entry|
145
+ child_path = path + [name]
146
+
147
+ case entry
148
+ when Blob
149
+ yield child_path.join("/"), entry
150
+
151
+ when Tree
152
+ entry.each_blob(child_path, &block)
153
+ end
154
+ end
155
+ end
156
+
157
+ def paths
158
+ map { |path, data| path }
159
+ end
160
+
161
+ def values
162
+ map { |path, data| data }
163
+ end
164
+
165
+ # Convert this tree into a hash object.
166
+ def to_hash
167
+ @table.inject({}) do |hash, (name, entry)|
168
+ if entry.is_a?(Tree)
169
+ hash[name] = entry.to_hash
170
+ else
171
+ hash[name] = entry.object ||= handler_for(name).read(entry.data)
172
+ end
173
+ hash
174
+ end
175
+ end
176
+
177
+ def inspect
178
+ "#<GitStore::Tree #{id} #{mode} #{to_hash.inspect}>"
179
+ end
180
+
181
+ end
182
+
183
+ end
@@ -0,0 +1,29 @@
1
+ class GitStore
2
+
3
+ class User
4
+ attr_accessor :name, :email, :time
5
+
6
+ def initialize(name, email, time)
7
+ @name, @email, @time = name, email, time
8
+ end
9
+
10
+ def dump
11
+ "#{ name } <#{email}> #{ time.to_i } #{ time.strftime('%z') }"
12
+ end
13
+
14
+ def self.from_config
15
+ name = IO.popen("git config user.name") { |io| io.gets.chomp }
16
+ email = IO.popen("git config user.email") { |io| io.gets.chomp }
17
+
18
+ new name, email, Time.now
19
+ end
20
+
21
+ def self.parse(user)
22
+ if match = user.match(/(.*)<(.*)> (\d+) ([+-]\d+)/)
23
+ new match[1].strip, match[2].strip, Time.at(match[3].to_i + match[4].to_i * 3600)
24
+ end
25
+ end
26
+
27
+ end
28
+
29
+ end