f10d853bb7b3114adbd92df94f75543e5f3a5a50
[rubygit.git] / lib / git / raw / object.rb
blobf10d853bb7b3114adbd92df94f75543e5f3a5a50
1 require 'digest/sha1'
3 module Git
4   module Raw
6   # class for author/committer/tagger lines
7   class UserInfo
8     attr_accessor :name, :email, :date, :offset
10     def initialize(str)
11       m = /^(.*?) <(.*)> (\d+) ([+-])0*(\d+?)$/.match(str)
12       if !m
13         raise RuntimeError, "invalid %s header in commit" % key
14       end
15       @name = m[1]
16       @email = m[2]
17       @date = Time.at(Integer(m[3]))
18       @offset = (m[4] == "-" ? -1 : 1)*Integer(m[5])
19     end
21     def to_s
22       "%s <%s> %s %+05d" % [@name, @email, @date.to_i, @offset]
23     end
24   end
26   # base class for all git objects (blob, tree, commit, tag)
27   class Object
28     attr_accessor :repository
30     def Object.from_raw(rawobject, repository = nil)
31       case rawobject.type
32       when :blob
33         return Blob.from_raw(rawobject, repository)
34       when :tree
35         return Tree.from_raw(rawobject, repository)
36       when :commit
37         return Commit.from_raw(rawobject, repository)
38       when :tag
39         return Tag.from_raw(rawobject, repository)
40       else
41         raise RuntimeError, "got invalid object-type"
42       end
43     end
45     def initialize
46       raise NotImplemented, "abstract class"
47     end
49     def type
50       raise NotImplemented, "abstract class"
51     end
53     def raw_content
54       raise NotImplemented, "abstract class"
55     end
57     def sha1
58       Digest::SHA1.hexdigest("%s %d\0" % \
59                              [self.type, self.raw_content.length] + \
60                              self.raw_content)
61     end
62   end
64   class Blob < Object
65     attr_accessor :content
67     def self.from_raw(rawobject, repository)
68       new(rawobject.content)
69     end
71     def initialize(content, repository=nil)
72       @content = content
73       @repository = repository
74     end
76     def type
77       :blob
78     end
80     def raw_content
81       @content
82     end
83   end
85   class DirectoryEntry
86     S_IFMT  = 00170000
87     S_IFLNK =  0120000
88     S_IFREG =  0100000
89     S_IFDIR =  0040000
91     attr_accessor :mode, :name, :sha1
92     def initialize(buf)
93       m = /^(\d+) (.*)\0(.{20})$/m.match(buf)
94       if !m
95         raise RuntimeError, "invalid directory entry"
96       end
97       @mode = 0
98       m[1].each_byte do |i|
99         @mode = (@mode << 3) | (i-'0'[0])
100       end
101       @name = m[2]
102       @sha1 = m[3].unpack("H*")[0]
104       if ![S_IFLNK, S_IFDIR, S_IFREG].include?(@mode & S_IFMT)
105         raise RuntimeError, "unknown type for directory entry"
106       end
107     end
109     def type
110       case @mode & S_IFMT
111       when S_IFLNK
112         @type = :link
113       when S_IFDIR
114         @type = :directory
115       when S_IFREG
116         @type = :file
117       else
118         raise RuntimeError, "unknown type for directory entry"
119       end
120     end
122     def type=(type)
123       case @type
124       when :link
125         @mode = (@mode & ~S_IFMT) | S_IFLNK
126       when :directory
127         @mode = (@mode & ~S_IFMT) | S_IFDIR
128       when :file
129         @mode = (@mode & ~S_IFMT) | S_IFREG
130       else
131         raise RuntimeError, "invalid type"
132       end
133     end
135     def raw
136       "%o %s\0%s" % [@mode, @name, [@sha1].pack("H*")]
137     end
138   end
140   class Tree < Object
141     attr_accessor :entry
143     def self.from_raw(rawobject, repository=nil)
144       entries = []
145       rawobject.content.scan(/\d+ .*?\0.{20}/m) do |raw|
146         entries << DirectoryEntry.new(raw)
147       end
148       new(entries, repository)
149     end
151     def initialize(entries=[], repository = nil)
152       @entry = entries
153       @repository = repository
154     end
156     def type
157       :tree
158     end
160     def raw_content
161       # TODO: sort correctly
162       #@entry.sort { |a,b| a.name <=> b.name }.
163       @entry.
164         collect { |e| e.raw }.join
165     end
166   end
168   class Commit < Object
169     attr_accessor :author, :committer, :tree, :parent, :message
171     def self.from_raw(rawobject, repository=nil)
172       parent = []
173       tree = author = committer = nil
175       headers, message = rawobject.content.split(/\n\n/, 2)
176       headers = headers.split(/\n/).map { |header| header.split(/ /, 2) }
177       headers.each do |key, value|
178         case key
179         when "tree"
180           tree = value
181         when "parent"
182           parent.push(value)
183         when "author"
184           author = UserInfo.new(value)
185         when "committer"
186           committer = UserInfo.new(value)
187         else
188           warn "unknown header '%s' in commit %s" % \
189             [key, rawobject.sha1.unpack("H*")[0]]
190         end
191       end
192       if not tree && author && committer
193         raise RuntimeError, "incomplete raw commit object"
194       end
195       new(tree, parent, author, committer, message, repository)
196     end
198     def initialize(tree, parent, author, committer, message, repository=nil)
199       @tree = tree
200       @author = author
201       @parent = parent
202       @committer = committer
203       @message = message
204       @repository = repository
205     end
207     def type
208       :commit
209     end
211     def raw_content
212       "tree %s\n%sauthor %s\ncommitter %s\n\n" % [
213         @tree,
214         @parent.collect { |i| "parent %s\n" % i }.join,
215         @author, @committer] + @message
216     end
217   end
219   class Tag < Object
220     attr_accessor :object, :type, :tag, :tagger, :message
222     def self.from_raw(rawobject, repository=nil)
223       headers, message = rawobject.content.split(/\n\n/, 2)
224       headers = headers.split(/\n/).map { |header| header.split(/ /, 2) }
225       headers.each do |key, value|
226         case key
227         when "object"
228           object = value
229         when "type"
230           if !["blob", "tree", "commit", "tag"].include?(value)
231             raise RuntimeError, "invalid type in tag"
232           end
233           type = value.to_sym
234         when "tag"
235           tag = value
236         when "tagger"
237           tagger = UserInfo.new(value)
238         else
239           warn "unknown header '%s' in tag" % \
240             [key, rawobject.sha1.unpack("H*")[0]]
241         end
242         if not object && type && tag && tagger
243           raise RuntimeError, "incomplete raw tag object"
244         end
245       end
246       new(object, type, tag, tagger, repository)
247     end
249     def initialize(object, type, tag, tagger, repository=nil)
250       @object = object
251       @type = type
252       @tag = tag
253       @tagger = tagger
254       @repository = repository
255     end
257     def raw_content
258       "object %s\ntype %s\ntag %s\ntagger %s\n\n" % \
259         [@object, @type, @tag, @tagger] + @message
260     end
262     def type
263       :tag
264     end
265   end