added Matthias and Simon to credits for the gitrb code
[rubygit.git] / lib / git / raw / object.rb
blob5c3969d7e677573cf84ec2f318f82ae62197546e
2 # converted from the gitrb project
4 # authors: 
5 #    Matthias Lederhofer <matled@gmx.net>
6 #    Simon 'corecode' Schubert <corecode@fs.ei.tum.de>
8 # provides native ruby access to git objects and pack files
11 require 'digest/sha1'
13 module Git
14   module Raw
16   # class for author/committer/tagger lines
17   class UserInfo
18     attr_accessor :name, :email, :date, :offset
20     def initialize(str)
21       m = /^(.*?) <(.*)> (\d+) ([+-])0*(\d+?)$/.match(str)
22       if !m
23         raise RuntimeError, "invalid %s header in commit" % key
24       end
25       @name = m[1]
26       @email = m[2]
27       @date = Time.at(Integer(m[3]))
28       @offset = (m[4] == "-" ? -1 : 1)*Integer(m[5])
29     end
31     def to_s
32       "%s <%s> %s %+05d" % [@name, @email, @date.to_i, @offset]
33     end
34   end
36   # base class for all git objects (blob, tree, commit, tag)
37   class Object
38     attr_accessor :repository
40     def Object.from_raw(rawobject, repository = nil)
41       case rawobject.type
42       when :blob
43         return Blob.from_raw(rawobject, repository)
44       when :tree
45         return Tree.from_raw(rawobject, repository)
46       when :commit
47         return Commit.from_raw(rawobject, repository)
48       when :tag
49         return Tag.from_raw(rawobject, repository)
50       else
51         raise RuntimeError, "got invalid object-type"
52       end
53     end
55     def initialize
56       raise NotImplemented, "abstract class"
57     end
59     def type
60       raise NotImplemented, "abstract class"
61     end
63     def raw_content
64       raise NotImplemented, "abstract class"
65     end
67     def sha1
68       Digest::SHA1.hexdigest("%s %d\0" % \
69                              [self.type, self.raw_content.length] + \
70                              self.raw_content)
71     end
72   end
74   class Blob < Object
75     attr_accessor :content
77     def self.from_raw(rawobject, repository)
78       new(rawobject.content)
79     end
81     def initialize(content, repository=nil)
82       @content = content
83       @repository = repository
84     end
86     def type
87       :blob
88     end
90     def raw_content
91       @content
92     end
93   end
95   class DirectoryEntry
96     S_IFMT  = 00170000
97     S_IFLNK =  0120000
98     S_IFREG =  0100000
99     S_IFDIR =  0040000
101     attr_accessor :mode, :name, :sha1
102     def initialize(buf)
103       m = /^(\d+) (.*)\0(.{20})$/m.match(buf)
104       if !m
105         raise RuntimeError, "invalid directory entry"
106       end
107       @mode = 0
108       m[1].each_byte do |i|
109         @mode = (@mode << 3) | (i-'0'[0])
110       end
111       @name = m[2]
112       @sha1 = m[3].unpack("H*")[0]
114       if ![S_IFLNK, S_IFDIR, S_IFREG].include?(@mode & S_IFMT)
115         raise RuntimeError, "unknown type for directory entry"
116       end
117     end
119     def type
120       case @mode & S_IFMT
121       when S_IFLNK
122         @type = :link
123       when S_IFDIR
124         @type = :directory
125       when S_IFREG
126         @type = :file
127       else
128         raise RuntimeError, "unknown type for directory entry"
129       end
130     end
132     def type=(type)
133       case @type
134       when :link
135         @mode = (@mode & ~S_IFMT) | S_IFLNK
136       when :directory
137         @mode = (@mode & ~S_IFMT) | S_IFDIR
138       when :file
139         @mode = (@mode & ~S_IFMT) | S_IFREG
140       else
141         raise RuntimeError, "invalid type"
142       end
143     end
145     def format_type
146       case type
147       when :link
148         'link'
149       when :directory
150         'tree'
151       when :file
152         'blob'
153       end
154     end
156     def format_mode
157       "%06o" % @mode
158     end
159     
160     def raw
161       "%o %s\0%s" % [@mode, @name, [@sha1].pack("H*")]
162     end
163   end
165   class Tree < Object
166     attr_accessor :entry
168     def self.from_raw(rawobject, repository=nil)
169       entries = []
170       rawobject.content.scan(/\d+ .*?\0.{20}/m) do |raw|
171         entries << DirectoryEntry.new(raw)
172       end
173       new(entries, repository)
174     end
176     def initialize(entries=[], repository = nil)
177       @entry = entries
178       @repository = repository
179     end
181     def type
182       :tree
183     end
185     def raw_content
186       # TODO: sort correctly
187       #@entry.sort { |a,b| a.name <=> b.name }.
188       @entry.collect { |e| [[e.format_mode, e.format_type, e.sha1].join(' '), e.name].join("\t") }.join("\n")
189     end
190   end
192   class Commit < Object
193     attr_accessor :author, :committer, :tree, :parent, :message
195     def self.from_raw(rawobject, repository=nil)
196       parent = []
197       tree = author = committer = nil
199       headers, message = rawobject.content.split(/\n\n/, 2)
200       headers = headers.split(/\n/).map { |header| header.split(/ /, 2) }
201       headers.each do |key, value|
202         case key
203         when "tree"
204           tree = value
205         when "parent"
206           parent.push(value)
207         when "author"
208           author = UserInfo.new(value)
209         when "committer"
210           committer = UserInfo.new(value)
211         else
212           warn "unknown header '%s' in commit %s" % \
213             [key, rawobject.sha1.unpack("H*")[0]]
214         end
215       end
216       if not tree && author && committer
217         raise RuntimeError, "incomplete raw commit object"
218       end
219       new(tree, parent, author, committer, message, repository)
220     end
222     def initialize(tree, parent, author, committer, message, repository=nil)
223       @tree = tree
224       @author = author
225       @parent = parent
226       @committer = committer
227       @message = message
228       @repository = repository
229     end
231     def type
232       :commit
233     end
235     def raw_content
236       "tree %s\n%sauthor %s\ncommitter %s\n\n" % [
237         @tree,
238         @parent.collect { |i| "parent %s\n" % i }.join,
239         @author, @committer] + @message
240     end
241   end
243   class Tag < Object
244     attr_accessor :object, :type, :tag, :tagger, :message
246     def self.from_raw(rawobject, repository=nil)
247       headers, message = rawobject.content.split(/\n\n/, 2)
248       headers = headers.split(/\n/).map { |header| header.split(/ /, 2) }
249       headers.each do |key, value|
250         case key
251         when "object"
252           object = value
253         when "type"
254           if !["blob", "tree", "commit", "tag"].include?(value)
255             raise RuntimeError, "invalid type in tag"
256           end
257           type = value.to_sym
258         when "tag"
259           tag = value
260         when "tagger"
261           tagger = UserInfo.new(value)
262         else
263           warn "unknown header '%s' in tag" % \
264             [key, rawobject.sha1.unpack("H*")[0]]
265         end
266         if not object && type && tag && tagger
267           raise RuntimeError, "incomplete raw tag object"
268         end
269       end
270       new(object, type, tag, tagger, repository)
271     end
273     def initialize(object, type, tag, tagger, repository=nil)
274       @object = object
275       @type = type
276       @tag = tag
277       @tagger = tagger
278       @repository = repository
279     end
281     def raw_content
282       "object %s\ntype %s\ntag %s\ntagger %s\n\n" % \
283         [@object, @type, @tag, @tagger] + @message
284     end
286     def type
287       :tag
288     end
289   end