schacon-grit 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/History.txt +13 -0
  2. data/Manifest.txt +57 -0
  3. data/README.txt +213 -0
  4. data/Rakefile +29 -0
  5. data/grit.gemspec +60 -0
  6. data/lib/grit.rb +53 -0
  7. data/lib/grit/actor.rb +36 -0
  8. data/lib/grit/blob.rb +117 -0
  9. data/lib/grit/commit.rb +221 -0
  10. data/lib/grit/commit_stats.rb +104 -0
  11. data/lib/grit/config.rb +44 -0
  12. data/lib/grit/diff.rb +70 -0
  13. data/lib/grit/errors.rb +7 -0
  14. data/lib/grit/git-ruby.rb +175 -0
  15. data/lib/grit/git-ruby/commit_db.rb +52 -0
  16. data/lib/grit/git-ruby/file_index.rb +186 -0
  17. data/lib/grit/git-ruby/git_object.rb +344 -0
  18. data/lib/grit/git-ruby/internal/loose.rb +136 -0
  19. data/lib/grit/git-ruby/internal/mmap.rb +59 -0
  20. data/lib/grit/git-ruby/internal/pack.rb +332 -0
  21. data/lib/grit/git-ruby/internal/raw_object.rb +37 -0
  22. data/lib/grit/git-ruby/object.rb +319 -0
  23. data/lib/grit/git-ruby/repository.rb +675 -0
  24. data/lib/grit/git.rb +130 -0
  25. data/lib/grit/head.rb +83 -0
  26. data/lib/grit/index.rb +102 -0
  27. data/lib/grit/lazy.rb +31 -0
  28. data/lib/grit/ref.rb +95 -0
  29. data/lib/grit/repo.rb +381 -0
  30. data/lib/grit/status.rb +149 -0
  31. data/lib/grit/tag.rb +71 -0
  32. data/lib/grit/tree.rb +100 -0
  33. data/test/test_actor.rb +35 -0
  34. data/test/test_blob.rb +79 -0
  35. data/test/test_commit.rb +184 -0
  36. data/test/test_config.rb +58 -0
  37. data/test/test_diff.rb +18 -0
  38. data/test/test_git.rb +70 -0
  39. data/test/test_grit.rb +32 -0
  40. data/test/test_head.rb +41 -0
  41. data/test/test_real.rb +19 -0
  42. data/test/test_reality.rb +17 -0
  43. data/test/test_remote.rb +14 -0
  44. data/test/test_repo.rb +277 -0
  45. data/test/test_tag.rb +25 -0
  46. data/test/test_tree.rb +96 -0
  47. metadata +110 -0
@@ -0,0 +1,59 @@
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
+ begin
13
+ require 'mmap'
14
+ rescue LoadError
15
+
16
+ module Grit
17
+ module GitRuby
18
+ module Internal
19
+ class Mmap
20
+ def initialize(file)
21
+ @file = file
22
+ @offset = nil
23
+ end
24
+
25
+ def unmap
26
+ @file = nil
27
+ end
28
+
29
+ def [](*idx)
30
+ idx = idx[0] if idx.length == 1
31
+ case idx
32
+ when Range
33
+ offset = idx.first
34
+ len = idx.last - idx.first + idx.exclude_end? ? 0 : 1
35
+ when Fixnum
36
+ offset = idx
37
+ len = nil
38
+ when Array
39
+ offset, len = idx
40
+ else
41
+ raise RuntimeError, "invalid index param: #{idx.class}"
42
+ end
43
+ if @offset != offset
44
+ @file.seek(offset)
45
+ end
46
+ @offset = offset + len ? len : 1
47
+ if not len
48
+ @file.read(1)[0]
49
+ else
50
+ @file.read(len)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ end # rescue LoadError
59
+
@@ -0,0 +1,332 @@
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
+ require 'grit/git-ruby/internal/raw_object'
14
+ require 'grit/git-ruby/internal/mmap'
15
+
16
+ module Grit
17
+ module GitRuby
18
+ module Internal
19
+ class PackFormatError < StandardError
20
+ end
21
+
22
+ class PackStorage
23
+ OBJ_OFS_DELTA = 6
24
+ OBJ_REF_DELTA = 7
25
+
26
+ FanOutCount = 256
27
+ SHA1Size = 20
28
+ IdxOffsetSize = 4
29
+ OffsetSize = 4
30
+ OffsetStart = FanOutCount * IdxOffsetSize
31
+ SHA1Start = OffsetStart + OffsetSize
32
+ EntrySize = OffsetSize + SHA1Size
33
+
34
+ def initialize(file)
35
+ if file =~ /\.idx$/
36
+ file = file[0...-3] + 'pack'
37
+ end
38
+
39
+ @cache = {}
40
+ @name = file
41
+
42
+ with_idx do |idx|
43
+ @offsets = [0]
44
+ FanOutCount.times do |i|
45
+ pos = idx[i * IdxOffsetSize,IdxOffsetSize].unpack('N')[0]
46
+ if pos < @offsets[i]
47
+ raise PackFormatError, "pack #@name has discontinuous index #{i}"
48
+ end
49
+ @offsets << pos
50
+ end
51
+
52
+ @size = @offsets[-1]
53
+ end
54
+ end
55
+
56
+ def with_idx(index_file = nil)
57
+ if !index_file
58
+ index_file = @name
59
+ idxfile = File.open(@name[0...-4]+'idx')
60
+ else
61
+ idxfile = File.open(index_file)
62
+ end
63
+ idx = Mmap.new(idxfile)
64
+ yield idx
65
+ idx.unmap
66
+ idxfile.close
67
+ end
68
+
69
+ def with_packfile
70
+ packfile = File.open(@name)
71
+ yield packfile
72
+ packfile.close
73
+ end
74
+
75
+ # given an index file, list out the shas that it's packfile contains
76
+ def self.get_shas(index_file)
77
+ with_idx(index_file) do |idx|
78
+ @offsets = [0]
79
+ FanOutCount.times do |i|
80
+ pos = idx[i * IdxOffsetSize,IdxOffsetSize].unpack('N')[0]
81
+ if pos < @offsets[i]
82
+ raise PackFormatError, "pack #@name has discontinuous index #{i}"
83
+ end
84
+ @offsets << pos
85
+ end
86
+
87
+ @size = @offsets[-1]
88
+ shas = []
89
+
90
+ pos = SHA1Start
91
+ @size.times do
92
+ sha1 = idx[pos,SHA1Size]
93
+ pos += EntrySize
94
+ shas << sha1.unpack("H*").first
95
+ end
96
+ shas
97
+ end
98
+ end
99
+
100
+ def cache_objects
101
+ @cache = {}
102
+ with_packfile do |packfile|
103
+ each_entry do |sha, offset|
104
+ data, type = unpack_object(packfile, offset, {:caching => true})
105
+ if data
106
+ @cache[sha] = RawObject.new(OBJ_TYPES[type], data)
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ def name
113
+ @name
114
+ end
115
+
116
+ def close
117
+ # shouldnt be anything open now
118
+ end
119
+
120
+ def [](sha1)
121
+ if obj = @cache[sha1]
122
+ return obj
123
+ end
124
+
125
+ offset = find_object(sha1)
126
+ return nil if !offset
127
+ @cache[sha1] = obj = parse_object(offset)
128
+ return obj
129
+ end
130
+
131
+ def each_entry
132
+ with_idx do |idx|
133
+ pos = OffsetStart
134
+ @size.times do
135
+ offset = idx[pos,OffsetSize].unpack('N')[0]
136
+ sha1 = idx[pos+OffsetSize,SHA1Size]
137
+ pos += EntrySize
138
+ yield sha1, offset
139
+ end
140
+ end
141
+ end
142
+
143
+ def each_sha1
144
+ with_idx do |idx|
145
+ # unpacking the offset is quite expensive, so
146
+ # we avoid using #each
147
+ pos = SHA1Start
148
+ @size.times do
149
+ sha1 = idx[pos,SHA1Size]
150
+ pos += EntrySize
151
+ yield sha1
152
+ end
153
+ end
154
+ end
155
+
156
+ def find_object(sha1)
157
+ obj = nil
158
+ with_idx do |idx|
159
+ obj = find_object_in_index(idx, sha1)
160
+ end
161
+ obj
162
+ end
163
+ private :find_object
164
+
165
+ def find_object_in_index(idx, sha1)
166
+ slot = sha1[0]
167
+ return nil if !slot
168
+ first, last = @offsets[slot,2]
169
+ while first < last
170
+ mid = (first + last) / 2
171
+ midsha1 = idx[SHA1Start + mid * EntrySize,SHA1Size]
172
+ cmp = midsha1 <=> sha1
173
+
174
+ if cmp < 0
175
+ first = mid + 1
176
+ elsif cmp > 0
177
+ last = mid
178
+ else
179
+ pos = OffsetStart + mid * EntrySize
180
+ offset = idx[pos,OffsetSize].unpack('N')[0]
181
+ return offset
182
+ end
183
+ end
184
+ nil
185
+ end
186
+
187
+ def parse_object(offset)
188
+ obj = nil
189
+ with_packfile do |packfile|
190
+ data, type = unpack_object(packfile, offset)
191
+ obj = RawObject.new(OBJ_TYPES[type], data)
192
+ end
193
+ obj
194
+ end
195
+ protected :parse_object
196
+
197
+ def unpack_object(packfile, offset, options = {})
198
+ obj_offset = offset
199
+ packfile.seek(offset)
200
+
201
+ c = packfile.read(1)[0]
202
+ size = c & 0xf
203
+ type = (c >> 4) & 7
204
+ shift = 4
205
+ offset += 1
206
+ while c & 0x80 != 0
207
+ c = packfile.read(1)[0]
208
+ size |= ((c & 0x7f) << shift)
209
+ shift += 7
210
+ offset += 1
211
+ end
212
+
213
+ return [false, false] if !(type == OBJ_COMMIT || type == OBJ_TREE) && options[:caching]
214
+
215
+ case type
216
+ when OBJ_OFS_DELTA, OBJ_REF_DELTA
217
+ data, type = unpack_deltified(packfile, type, offset, obj_offset, size, options)
218
+ #puts type
219
+ when OBJ_COMMIT, OBJ_TREE, OBJ_BLOB, OBJ_TAG
220
+ data = unpack_compressed(offset, size)
221
+ else
222
+ raise PackFormatError, "invalid type #{type}"
223
+ end
224
+ [data, type]
225
+ end
226
+ private :unpack_object
227
+
228
+ def unpack_deltified(packfile, type, offset, obj_offset, size, options = {})
229
+ packfile.seek(offset)
230
+ data = packfile.read(SHA1Size)
231
+
232
+ if type == OBJ_OFS_DELTA
233
+ i = 0
234
+ c = data[i]
235
+ base_offset = c & 0x7f
236
+ while c & 0x80 != 0
237
+ c = data[i += 1]
238
+ base_offset += 1
239
+ base_offset <<= 7
240
+ base_offset |= c & 0x7f
241
+ end
242
+ base_offset = obj_offset - base_offset
243
+ offset += i + 1
244
+ else
245
+ base_offset = find_object(data)
246
+ offset += SHA1Size
247
+ end
248
+
249
+ base, type = unpack_object(packfile, base_offset)
250
+
251
+ return [false, false] if !(type == OBJ_COMMIT || type == OBJ_TREE) && options[:caching]
252
+
253
+ delta = unpack_compressed(offset, size)
254
+ [patch_delta(base, delta), type]
255
+ end
256
+ private :unpack_deltified
257
+
258
+ def unpack_compressed(offset, destsize)
259
+ outdata = ""
260
+ with_packfile do |packfile|
261
+ packfile.seek(offset)
262
+ zstr = Zlib::Inflate.new
263
+ while outdata.size < destsize
264
+ indata = packfile.read(4096)
265
+ if indata.size == 0
266
+ raise PackFormatError, 'error reading pack data'
267
+ end
268
+ outdata += zstr.inflate(indata)
269
+ end
270
+ if outdata.size > destsize
271
+ raise PackFormatError, 'error reading pack data'
272
+ end
273
+ zstr.close
274
+ end
275
+ outdata
276
+ end
277
+ private :unpack_compressed
278
+
279
+ def patch_delta(base, delta)
280
+ src_size, pos = patch_delta_header_size(delta, 0)
281
+ if src_size != base.size
282
+ raise PackFormatError, 'invalid delta data'
283
+ end
284
+
285
+ dest_size, pos = patch_delta_header_size(delta, pos)
286
+ dest = ""
287
+ while pos < delta.size
288
+ c = delta[pos]
289
+ pos += 1
290
+ if c & 0x80 != 0
291
+ pos -= 1
292
+ cp_off = cp_size = 0
293
+ cp_off = delta[pos += 1] if c & 0x01 != 0
294
+ cp_off |= delta[pos += 1] << 8 if c & 0x02 != 0
295
+ cp_off |= delta[pos += 1] << 16 if c & 0x04 != 0
296
+ cp_off |= delta[pos += 1] << 24 if c & 0x08 != 0
297
+ cp_size = delta[pos += 1] if c & 0x10 != 0
298
+ cp_size |= delta[pos += 1] << 8 if c & 0x20 != 0
299
+ cp_size |= delta[pos += 1] << 16 if c & 0x40 != 0
300
+ cp_size = 0x10000 if cp_size == 0
301
+ pos += 1
302
+ dest += base[cp_off,cp_size]
303
+ elsif c != 0
304
+ dest += delta[pos,c]
305
+ pos += c
306
+ else
307
+ raise PackFormatError, 'invalid delta data'
308
+ end
309
+ end
310
+ dest
311
+ end
312
+ private :patch_delta
313
+
314
+ def patch_delta_header_size(delta, pos)
315
+ size = 0
316
+ shift = 0
317
+ begin
318
+ c = delta[pos]
319
+ if c == nil
320
+ raise PackFormatError, 'invalid delta header'
321
+ end
322
+ pos += 1
323
+ size |= (c & 0x7f) << shift
324
+ shift += 7
325
+ end while c & 0x80 != 0
326
+ [size, pos]
327
+ end
328
+ private :patch_delta_header_size
329
+ end
330
+ end
331
+ end
332
+ end
@@ -0,0 +1,37 @@
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
+ #
8
+ # provides native ruby access to git objects and pack files
9
+ #
10
+
11
+ require 'digest/sha1'
12
+
13
+ module Grit
14
+ module GitRuby
15
+ module Internal
16
+ OBJ_NONE = 0
17
+ OBJ_COMMIT = 1
18
+ OBJ_TREE = 2
19
+ OBJ_BLOB = 3
20
+ OBJ_TAG = 4
21
+
22
+ OBJ_TYPES = [nil, :commit, :tree, :blob, :tag].freeze
23
+
24
+ class RawObject
25
+ attr_accessor :type, :content
26
+ def initialize(type, content)
27
+ @type = type
28
+ @content = content
29
+ end
30
+
31
+ def sha1
32
+ Digest::SHA1.digest("%s %d\0" % [@type, @content.length] + @content)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,319 @@
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
+ # These classes translate the raw binary data kept in the sha encoded files
13
+ # into parsed data that can then be used in another fashion
14
+ require 'stringio'
15
+
16
+ module Grit
17
+ module GitRuby
18
+
19
+ # class for author/committer/tagger lines
20
+ class UserInfo
21
+ attr_accessor :name, :email, :date, :offset
22
+
23
+ def initialize(str)
24
+ m = /^(.*?) <(.*)> (\d+) ([+-])0*(\d+?)$/.match(str)
25
+ if !m
26
+ raise RuntimeError, "invalid header '%s' in commit" % str
27
+ end
28
+ @name = m[1]
29
+ @email = m[2]
30
+ @date = Time.at(Integer(m[3]))
31
+ @offset = (m[4] == "-" ? -1 : 1)*Integer(m[5])
32
+ end
33
+
34
+ def to_s
35
+ "%s <%s> %s %+05d" % [@name, @email, @date.to_i, @offset]
36
+ end
37
+ end
38
+
39
+ # base class for all git objects (blob, tree, commit, tag)
40
+ class Object
41
+ attr_accessor :repository
42
+
43
+ def Object.from_raw(rawobject, repository = nil)
44
+ case rawobject.type
45
+ when :blob
46
+ return Blob.from_raw(rawobject, repository)
47
+ when :tree
48
+ return Tree.from_raw(rawobject, repository)
49
+ when :commit
50
+ return Commit.from_raw(rawobject, repository)
51
+ when :tag
52
+ return Tag.from_raw(rawobject, repository)
53
+ else
54
+ raise RuntimeError, "got invalid object-type"
55
+ end
56
+ end
57
+
58
+ def initialize
59
+ raise NotImplemented, "abstract class"
60
+ end
61
+
62
+ def type
63
+ raise NotImplemented, "abstract class"
64
+ end
65
+
66
+ def raw_content
67
+ raise NotImplemented, "abstract class"
68
+ end
69
+
70
+ def sha1
71
+ Digest::SHA1.hexdigest("%s %d\0" % \
72
+ [self.type, self.raw_content.length] + \
73
+ self.raw_content)
74
+ end
75
+ end
76
+
77
+ class Blob < Object
78
+ attr_accessor :content
79
+
80
+ def self.from_raw(rawobject, repository)
81
+ new(rawobject.content)
82
+ end
83
+
84
+ def initialize(content, repository=nil)
85
+ @content = content
86
+ @repository = repository
87
+ end
88
+
89
+ def type
90
+ :blob
91
+ end
92
+
93
+ def raw_content
94
+ @content
95
+ end
96
+ end
97
+
98
+ class DirectoryEntry
99
+ S_IFMT = 00170000
100
+ S_IFLNK = 0120000
101
+ S_IFREG = 0100000
102
+ S_IFDIR = 0040000
103
+
104
+ attr_accessor :mode, :name, :sha1
105
+ def initialize(mode, filename, sha1o)
106
+ @mode = 0
107
+ mode.each_byte do |i|
108
+ @mode = (@mode << 3) | (i-'0'[0])
109
+ end
110
+ @name = filename
111
+ @sha1 = sha1o
112
+ if ![S_IFLNK, S_IFDIR, S_IFREG].include?(@mode & S_IFMT)
113
+ raise RuntimeError, "unknown type for directory entry"
114
+ end
115
+ end
116
+
117
+ def type
118
+ case @mode & S_IFMT
119
+ when S_IFLNK
120
+ @type = :link
121
+ when S_IFDIR
122
+ @type = :directory
123
+ when S_IFREG
124
+ @type = :file
125
+ else
126
+ raise RuntimeError, "unknown type for directory entry"
127
+ end
128
+ end
129
+
130
+ def type=(type)
131
+ case @type
132
+ when :link
133
+ @mode = (@mode & ~S_IFMT) | S_IFLNK
134
+ when :directory
135
+ @mode = (@mode & ~S_IFMT) | S_IFDIR
136
+ when :file
137
+ @mode = (@mode & ~S_IFMT) | S_IFREG
138
+ else
139
+ raise RuntimeError, "invalid type"
140
+ end
141
+ end
142
+
143
+ def format_type
144
+ case type
145
+ when :link
146
+ 'link'
147
+ when :directory
148
+ 'tree'
149
+ when :file
150
+ 'blob'
151
+ end
152
+ end
153
+
154
+ def format_mode
155
+ "%06o" % @mode
156
+ end
157
+
158
+ def raw
159
+ "%o %s\0%s" % [@mode, @name, [@sha1].pack("H*")]
160
+ end
161
+ end
162
+
163
+
164
+ def self.read_bytes_until(io, char)
165
+ string = ''
166
+ while ((next_char = io.getc.chr) != char) && !io.eof
167
+ string += next_char
168
+ end
169
+ string
170
+ end
171
+
172
+
173
+ class Tree < Object
174
+ attr_accessor :entry
175
+
176
+ def self.from_raw(rawobject, repository=nil)
177
+ raw = StringIO.new(rawobject.content)
178
+
179
+ entries = []
180
+ while !raw.eof?
181
+ mode = Grit::GitRuby.read_bytes_until(raw, ' ')
182
+ file_name = Grit::GitRuby.read_bytes_until(raw, "\0")
183
+ raw_sha = raw.read(20)
184
+ sha = raw_sha.unpack("H*").first
185
+
186
+ entries << DirectoryEntry.new(mode, file_name, sha)
187
+ end
188
+ new(entries, repository)
189
+ end
190
+
191
+ def initialize(entries=[], repository = nil)
192
+ @entry = entries
193
+ @repository = repository
194
+ end
195
+
196
+ def type
197
+ :tree
198
+ end
199
+
200
+ def raw_content
201
+ # TODO: sort correctly
202
+ #@entry.sort { |a,b| a.name <=> b.name }.
203
+ @entry.collect { |e| [[e.format_mode, e.format_type, e.sha1].join(' '), e.name].join("\t") }.join("\n")
204
+ end
205
+
206
+ def actual_raw
207
+ #@entry.collect { |e| e.raw.join(' '), e.name].join("\t") }.join("\n")
208
+ end
209
+ end
210
+
211
+ class Commit < Object
212
+ attr_accessor :author, :committer, :tree, :parent, :message, :headers
213
+
214
+ def self.from_raw(rawobject, repository=nil)
215
+ parent = []
216
+ tree = author = committer = nil
217
+
218
+ headers, message = rawobject.content.split(/\n\n/, 2)
219
+ all_headers = headers.split(/\n/).map { |header| header.split(/ /, 2) }
220
+ all_headers.each do |key, value|
221
+ case key
222
+ when "tree"
223
+ tree = value
224
+ when "parent"
225
+ parent.push(value)
226
+ when "author"
227
+ author = UserInfo.new(value)
228
+ when "committer"
229
+ committer = UserInfo.new(value)
230
+ else
231
+ warn "unknown header '%s' in commit %s" % \
232
+ [key, rawobject.sha1.unpack("H*")[0]]
233
+ end
234
+ end
235
+ if not tree && author && committer
236
+ raise RuntimeError, "incomplete raw commit object"
237
+ end
238
+ new(tree, parent, author, committer, message, headers, repository)
239
+ end
240
+
241
+ def initialize(tree, parent, author, committer, message, headers, repository=nil)
242
+ @tree = tree
243
+ @author = author
244
+ @parent = parent
245
+ @committer = committer
246
+ @message = message
247
+ @headers = headers
248
+ @repository = repository
249
+ end
250
+
251
+ def type
252
+ :commit
253
+ end
254
+
255
+ def raw_content
256
+ "tree %s\n%sauthor %s\ncommitter %s\n\n" % [
257
+ @tree,
258
+ @parent.collect { |i| "parent %s\n" % i }.join,
259
+ @author, @committer] + @message
260
+ end
261
+
262
+ def raw_log(sha)
263
+ output = "commit #{sha}\n"
264
+ output += @headers + "\n\n"
265
+ output += @message.split("\n").map { |l| ' ' + l }.join("\n") + "\n\n"
266
+ end
267
+
268
+ end
269
+
270
+ class Tag < Object
271
+ attr_accessor :object, :type, :tag, :tagger, :message
272
+
273
+ def self.from_raw(rawobject, repository=nil)
274
+ headers, message = rawobject.content.split(/\n\n/, 2)
275
+ headers = headers.split(/\n/).map { |header| header.split(/ /, 2) }
276
+ headers.each do |key, value|
277
+ case key
278
+ when "object"
279
+ object = value
280
+ when "type"
281
+ if !["blob", "tree", "commit", "tag"].include?(value)
282
+ raise RuntimeError, "invalid type in tag"
283
+ end
284
+ type = value.to_sym
285
+ when "tag"
286
+ tag = value
287
+ when "tagger"
288
+ tagger = UserInfo.new(value)
289
+ else
290
+ warn "unknown header '%s' in tag" % \
291
+ [key, rawobject.sha1.unpack("H*")[0]]
292
+ end
293
+ if not object && type && tag && tagger
294
+ raise RuntimeError, "incomplete raw tag object"
295
+ end
296
+ end
297
+ new(object, type, tag, tagger, repository)
298
+ end
299
+
300
+ def initialize(object, type, tag, tagger, repository=nil)
301
+ @object = object
302
+ @type = type
303
+ @tag = tag
304
+ @tagger = tagger
305
+ @repository = repository
306
+ end
307
+
308
+ def raw_content
309
+ "object %s\ntype %s\ntag %s\ntagger %s\n\n" % \
310
+ [@object, @type, @tag, @tagger] + @message
311
+ end
312
+
313
+ def type
314
+ :tag
315
+ end
316
+ end
317
+
318
+ end
319
+ end