├── git-server.rb └── objects.rb /git-server.rb: -------------------------------------------------------------------------------- 1 | # Implements git-recieve-pack in ruby, so I can understand the damn thing 2 | require 'socket' 3 | require 'pp' 4 | require 'zlib' 5 | require 'fileutils' 6 | require 'digest' 7 | require 'objects' 8 | 9 | class GitServer 10 | 11 | NULL_SHA = '0000000000000000000000000000000000000000' 12 | #CAPABILITIES = " report-status delete-refs " 13 | #CAPABILITIES = " multi_ack thin-pack side-band side-band-64k ofs-delta shallow no-progress " 14 | CAPABILITIES = " " 15 | 16 | OBJ_NONE = 0 17 | OBJ_COMMIT = 1 18 | OBJ_TREE = 2 19 | OBJ_BLOB = 3 20 | OBJ_TAG = 4 21 | OBJ_OFS_DELTA = 6 22 | OBJ_REF_DELTA = 7 23 | 24 | OBJ_TYPES = [nil, :commit, :tree, :blob, :tag, nil, :ofs_delta, :ref_delta].freeze 25 | 26 | def initialize(path) 27 | @path = path 28 | end 29 | 30 | def self.start_server(path) 31 | server = self.new(path) 32 | server.listen 33 | end 34 | 35 | def listen 36 | server = TCPServer.new('127.0.0.1', 9418) 37 | while (session = server.accept) 38 | t = GitServerThread.new(session, @path) 39 | t.do_action 40 | return 41 | end 42 | end 43 | 44 | class GitServerThread 45 | 46 | def initialize(session, path) 47 | @path = path 48 | @session = session 49 | @capabilities_sent = false 50 | end 51 | 52 | def do_action 53 | header_data = read_header 54 | case header_data[1] 55 | when 'git-receive-pack': 56 | receive_pack(header_data[2]) 57 | when 'git-upload-pack': 58 | upload_pack(header_data[2]) 59 | else 60 | @session.print 'error: wrong thingy' 61 | end 62 | @session.close 63 | end 64 | 65 | def receive_pack(path) 66 | @delta_list = {} 67 | 68 | @git_dir = File.join(@path, path) 69 | git_init(@git_dir) if !File.exists?(@git_dir) 70 | 71 | send_refs 72 | packet_flush 73 | read_refs 74 | read_pack 75 | write_refs 76 | end 77 | 78 | def write_refs 79 | @refs.each do |sha_old, sha_new, path| 80 | ref = File.join(@git_dir, path) 81 | FileUtils.mkdir_p(File.dirname(ref)) 82 | File.open(ref, 'w+') { |f| f.write(sha_new) } 83 | end 84 | end 85 | 86 | def git_init(dir, bare = false) 87 | FileUtils.mkdir_p(dir) if !File.exists?(dir) 88 | 89 | FileUtils.cd(dir) do 90 | if(File.exists?('objects')) 91 | return false # already initialized 92 | else 93 | # initialize directory 94 | create_initial_config(bare) 95 | FileUtils.mkdir_p('refs/heads') 96 | FileUtils.mkdir_p('refs/tags') 97 | FileUtils.mkdir_p('objects/info') 98 | FileUtils.mkdir_p('objects/pack') 99 | FileUtils.mkdir_p('branches') 100 | add_file('description', 'Unnamed repository; edit this file to name it for gitweb.') 101 | add_file('HEAD', "ref: refs/heads/master\n") 102 | FileUtils.mkdir_p('hooks') 103 | FileUtils.cd('hooks') do 104 | add_file('applypatch-msg', '# add shell script and make executable to enable') 105 | add_file('post-commit', '# add shell script and make executable to enable') 106 | add_file('post-receive', '# add shell script and make executable to enable') 107 | add_file('post-update', '# add shell script and make executable to enable') 108 | add_file('pre-applypatch', '# add shell script and make executable to enable') 109 | add_file('pre-commit', '# add shell script and make executable to enable') 110 | add_file('pre-rebase', '# add shell script and make executable to enable') 111 | add_file('update', '# add shell script and make executable to enable') 112 | end 113 | FileUtils.mkdir_p('info') 114 | add_file('info/exclude', "# *.[oa]\n# *~") 115 | end 116 | end 117 | end 118 | 119 | def create_initial_config(bare = false) 120 | bare ? bare_status = 'true' : bare_status = 'false' 121 | config = "[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n\tbare = #{bare_status}\n\tlogallrefupdates = true" 122 | add_file('config', config) 123 | end 124 | 125 | def add_file(name, contents) 126 | File.open(name, 'w') do |f| 127 | f.write contents 128 | end 129 | end 130 | 131 | def read_refs 132 | @refs = [] 133 | while(data = packet_read_line) do 134 | sha_old, sha_new, path = data.split(' ') 135 | @refs << [sha_old, sha_new, path] 136 | end 137 | end 138 | 139 | def read_pack 140 | (sig, ver, entries) = read_pack_header 141 | unpack_all(entries) 142 | end 143 | 144 | def unpack_all(entries) 145 | return if !entries 146 | 1.upto(entries) do |number| 147 | unpack_object(number) 148 | end 149 | puts 'checksum:' + @session.recv(20).unpack("H*")[0] 150 | end 151 | 152 | def unpack_object(number) 153 | c = @session.recv(1)[0] 154 | size = c & 0xf 155 | type = (c >> 4) & 7 156 | shift = 4 157 | while c & 0x80 != 0 158 | c = @session.recv(1)[0] 159 | size |= ((c & 0x7f) << shift) 160 | shift += 7 161 | end 162 | 163 | case type 164 | when OBJ_OFS_DELTA, OBJ_REF_DELTA 165 | sha = unpack_deltified(type, size) 166 | #puts "WRITE " + OBJ_TYPES[type].to_s + sha 167 | return 168 | when OBJ_COMMIT, OBJ_TREE, OBJ_BLOB, OBJ_TAG 169 | sha = unpack_compressed(type, size) 170 | #puts "WRITE " + OBJ_TYPES[type].to_s + sha 171 | return 172 | else 173 | puts "invalid type #{type}" 174 | end 175 | end 176 | 177 | def unpack_compressed(type, size) 178 | object_data = get_data(size) 179 | sha = put_raw_object(object_data, OBJ_TYPES[type].to_s) 180 | check_delta(sha) 181 | end 182 | 183 | def check_delta(sha) 184 | unpack_delta_cached(sha) if @delta_list[sha] 185 | sha 186 | end 187 | 188 | def unpack_delta_cached(sha) 189 | base, type = get_raw_object(sha) 190 | @delta_list[sha].each do |patch| 191 | obj_data = patch_delta(base, patch) 192 | sha = put_raw_object(obj_data, type) 193 | check_delta(sha) 194 | end 195 | @delta_list[sha] = nil 196 | end 197 | 198 | def has_object?(sha1) 199 | File.exists?(File.join(@git_dir, 'objects', sha1[0...2], sha1[2..39])) 200 | end 201 | 202 | 203 | def get_raw_object(sha1) 204 | path = File.join(@git_dir, 'objects', sha1[0...2], sha1[2..39]) 205 | return false if !File.exists?(path) 206 | buf = File.read(path) 207 | 208 | if buf.length < 2 209 | puts "object file too small" 210 | end 211 | 212 | if legacy_loose_object?(buf) 213 | content = Zlib::Inflate.inflate(buf) 214 | header, content = content.split(/\0/, 2) 215 | if !header || !content 216 | puts "invalid object header" 217 | end 218 | type, size = header.split(/ /, 2) 219 | if !%w(blob tree commit tag).include?(type) || size !~ /^\d+$/ 220 | puts "invalid object header" 221 | end 222 | type = type.to_sym 223 | size = size.to_i 224 | else 225 | type, size, used = unpack_object_header_gently(buf) 226 | content = Zlib::Inflate.inflate(buf[used..-1]) 227 | end 228 | puts "size mismatch" if content.length != size 229 | return [content, type] 230 | end 231 | 232 | def legacy_loose_object?(buf) 233 | word = (buf[0] << 8) + buf[1] 234 | buf[0] == 0x78 && word % 31 == 0 235 | end 236 | 237 | def unpack_object_header_gently(buf) 238 | used = 0 239 | c = buf[used] 240 | used += 1 241 | 242 | type = (c >> 4) & 7; 243 | size = c & 15; 244 | shift = 4; 245 | while c & 0x80 != 0 246 | if buf.length <= used 247 | raise LooseObjectError, "object file too short" 248 | end 249 | c = buf[used] 250 | used += 1 251 | 252 | size += (c & 0x7f) << shift 253 | shift += 7 254 | end 255 | type = OBJ_TYPES[type] 256 | if ![:blob, :tree, :commit, :tag].include?(type) 257 | raise LooseObjectError, "invalid loose object type" 258 | end 259 | return [type, size, used] 260 | end 261 | 262 | def put_raw_object(content, type) 263 | size = content.length.to_s 264 | 265 | header = "#{type} #{size}\0" 266 | store = header + content 267 | 268 | sha1 = Digest::SHA1.hexdigest(store) 269 | path = File.join(@git_dir, 'objects', sha1[0...2], sha1[2..40]) 270 | 271 | if !File.exists?(path) 272 | content = Zlib::Deflate.deflate(store) 273 | 274 | FileUtils.mkdir_p(File.join(@git_dir, 'objects', sha1[0...2])) 275 | File.open(path, 'w') do |f| 276 | f.write content 277 | end 278 | end 279 | return sha1 280 | end 281 | 282 | def unpack_deltified(type, size) 283 | if type == OBJ_REF_DELTA 284 | base_sha = @session.recv(20) 285 | sha1 = base_sha.unpack("H*")[0] 286 | delta = get_data(size) 287 | if has_object?(sha1) 288 | base, type = get_raw_object(sha1) 289 | obj_data = patch_delta(base, delta) 290 | return put_raw_object(obj_data, type) 291 | else 292 | @delta_list[sha1] ||= [] 293 | @delta_list[sha1] << delta 294 | end 295 | else 296 | i = 0 297 | c = data[i] 298 | base_offset = c & 0x7f 299 | while c & 0x80 != 0 300 | c = data[i += 1] 301 | base_offset += 1 302 | base_offset <<= 7 303 | base_offset |= c & 0x7f 304 | end 305 | offset += i + 1 306 | return false ## NOT SUPPORTED YET ## 307 | end 308 | return nil 309 | end 310 | 311 | def get_data(size) 312 | stream = Zlib::Inflate.new 313 | buf = '' 314 | while(true) do 315 | buf += stream.inflate(@session.recv(1)) 316 | if (stream.total_out == size && stream.finished?) 317 | break; 318 | end 319 | end 320 | stream.close 321 | buf 322 | end 323 | 324 | def patch_delta(base, delta) 325 | src_size, pos = patch_delta_header_size(delta, 0) 326 | if src_size != base.size 327 | raise PackFormatError, 'invalid delta data' 328 | end 329 | 330 | dest_size, pos = patch_delta_header_size(delta, pos) 331 | dest = "" 332 | while pos < delta.size 333 | c = delta[pos] 334 | pos += 1 335 | if c & 0x80 != 0 336 | pos -= 1 337 | cp_off = cp_size = 0 338 | cp_off = delta[pos += 1] if c & 0x01 != 0 339 | cp_off |= delta[pos += 1] << 8 if c & 0x02 != 0 340 | cp_off |= delta[pos += 1] << 16 if c & 0x04 != 0 341 | cp_off |= delta[pos += 1] << 24 if c & 0x08 != 0 342 | cp_size = delta[pos += 1] if c & 0x10 != 0 343 | cp_size |= delta[pos += 1] << 8 if c & 0x20 != 0 344 | cp_size |= delta[pos += 1] << 16 if c & 0x40 != 0 345 | cp_size = 0x10000 if cp_size == 0 346 | pos += 1 347 | dest += base[cp_off,cp_size] 348 | elsif c != 0 349 | dest += delta[pos,c] 350 | pos += c 351 | else 352 | raise PackFormatError, 'invalid delta data' 353 | end 354 | end 355 | dest 356 | end 357 | 358 | def patch_delta_header_size(delta, pos) 359 | size = 0 360 | shift = 0 361 | begin 362 | c = delta[pos] 363 | if c == nil 364 | raise PackFormatError, 'invalid delta header' 365 | end 366 | pos += 1 367 | size |= (c & 0x7f) << shift 368 | shift += 7 369 | end while c & 0x80 != 0 370 | [size, pos] 371 | end 372 | 373 | def read_pack_header 374 | sig = @session.recv(4) 375 | ver = @session.recv(4).unpack("N")[0] 376 | entries = @session.recv(4).unpack("N")[0] 377 | [sig, ver, entries] 378 | end 379 | 380 | def packet_read_line 381 | size = @session.recv(4) 382 | hsize = size.hex 383 | if hsize > 0 384 | @session.recv(hsize - 4) 385 | else 386 | false 387 | end 388 | end 389 | 390 | def packet_flush 391 | @session.send('0000', 0) 392 | end 393 | 394 | def send_ack 395 | @session.send("0007NAK", 0) 396 | end 397 | 398 | def refs 399 | @refs = [] 400 | Dir.chdir(@git_dir) do 401 | Dir.glob("refs/**/*") do |file| 402 | @refs << [File.read(file), file] if File.file?(file) 403 | end 404 | end 405 | @refs 406 | end 407 | 408 | def send_refs 409 | refs.each do |ref| 410 | send_ref(ref[1], ref[0]) 411 | end 412 | send_ref("capabilities^{}", NULL_SHA) if !@capabiliies_sent 413 | end 414 | 415 | def send_ref(path, sha) 416 | if (@capabilities_sent) 417 | packet = "%s %s\n" % [sha, path] 418 | else 419 | packet = "%s %s%c%s\n" % [sha, path, 0, CAPABILITIES] 420 | end 421 | write_server(packet) 422 | @capabilities_sent = true 423 | end 424 | 425 | def write_server(data) 426 | string = '000' + sprintf("%x", data.length + 4) 427 | string = string[string.length - 4, 4] 428 | 429 | @session.send(string, 0) 430 | @session.send(data, 0) 431 | end 432 | 433 | def upload_pack(path) 434 | @git_dir = File.join(@path, path) 435 | send_refs 436 | packet_flush 437 | receive_needs 438 | send_ack 439 | upload_pack_file 440 | end 441 | 442 | def receive_needs 443 | @need_refs = [] 444 | while(data = packet_read_line) do 445 | cmd, sha = data.split(' ') 446 | @need_refs << [cmd, sha] 447 | end 448 | puts 'done:' 449 | puts @session.recv(9) 450 | @need_refs 451 | end 452 | 453 | def upload_pack_file 454 | @send_objects = {} 455 | @need_refs.each do |cmd, sha| 456 | if cmd == 'want' && sha != NULL_SHA 457 | @send_objects[sha] = ' commit' 458 | build_object_list_from_commit(sha) 459 | end 460 | end 461 | @send_objects = @send_objects.sort { |a, b| a[1] <=> b[1] } 462 | pp @send_objects 463 | 464 | build_pack_file 465 | end 466 | 467 | def build_pack_file 468 | @digest = Digest::SHA1.new 469 | 470 | # build_header 471 | write_pack('PACK') 472 | write_pack([2].pack("N")) 473 | write_pack([@send_objects.length].pack("N")) 474 | 475 | # build_pack 476 | @send_objects.each do |sha, name| 477 | # build pack header 478 | content, type = get_raw_object(sha) 479 | size = content.length 480 | btype = type_to_flag(type) 481 | 482 | c = (btype << 4) | (size & 15) 483 | c |= 0x80 484 | size = (size >> 4) 485 | write_pack(c.chr) 486 | while (size > 0) do 487 | c = size & 0x7f 488 | size = (size >> 7) 489 | if size > 0 490 | c |= 0x80; 491 | end 492 | write_pack(c.chr) 493 | end 494 | 495 | # pack object data 496 | write_pack(Zlib::Deflate.deflate(content)) 497 | end 498 | 499 | @session.send([@digest.hexdigest].pack("H*"), 0) 500 | end 501 | 502 | def type_to_flag(type) 503 | case type.to_s 504 | when 'commit': return OBJ_COMMIT 505 | when 'tree': return OBJ_TREE 506 | when 'blob': return OBJ_BLOB 507 | when 'tag': return OBJ_TAG 508 | end 509 | end 510 | 511 | def write_pack(bits) 512 | @session.send(bits, 0) 513 | @digest << bits 514 | end 515 | 516 | def object_from_sha(sha) 517 | content, type = get_raw_object(sha) 518 | Git::Object.from_raw(Git::RawObject.new(type.to_sym, content)) 519 | end 520 | 521 | def build_object_list_from_commit(sha) 522 | # go through each parent sha 523 | commit = object_from_sha(sha) 524 | # traverse the tree and add all the tree/blob shas 525 | @send_objects[commit.tree] = '/' 526 | build_object_list_from_tree(commit.tree) 527 | commit.parent.each do |p| 528 | @send_objects[p] = ' commit' 529 | build_object_list_from_commit(p) 530 | end 531 | end 532 | 533 | def build_object_list_from_tree(sha) 534 | tree = object_from_sha(sha) 535 | tree.entry.each do |t| 536 | @send_objects[t.sha1] = t.name 537 | if t.type == :tree 538 | build_object_list_from_tree(t.sha1) 539 | end 540 | end 541 | end 542 | 543 | def read_header() 544 | len = @session.recv( 4 ).hex 545 | return false if (len == 0) 546 | command, directory = read_until_null().strip.split(' ') 547 | stuff = read_until_null() 548 | # verify header length? 549 | [len, command, directory, stuff] 550 | end 551 | 552 | def read_until_null(debug = false) 553 | data = '' 554 | while c = @session.recv(1) 555 | #puts "read: #{c}:#{c[0]}" if debug 556 | if c[0] == 0 557 | return data 558 | else 559 | data += c 560 | end 561 | end 562 | data 563 | end 564 | 565 | 566 | end 567 | end 568 | 569 | #FileUtils.rm_r('/tmp/gittest') rescue nil 570 | GitServer.start_server('/tmp/gittest') 571 | 572 | -------------------------------------------------------------------------------- /objects.rb: -------------------------------------------------------------------------------- 1 | # 2 | # converted from the gitrb project 3 | # 4 | # authors: 5 | # Matthias Lederhofer 6 | # Simon 'corecode' Schubert 7 | # 8 | # provides native ruby access to git objects and pack files 9 | # 10 | 11 | require 'digest/sha1' 12 | 13 | module Git 14 | 15 | class RawObject 16 | attr_accessor :type, :content, :length 17 | def initialize(type, content) 18 | @type = type 19 | @content = content 20 | @length = content.length 21 | end 22 | 23 | def sha1 24 | Digest::SHA1.digest("%s %d\0" % [@type, @content.length] + @content) 25 | end 26 | end 27 | 28 | # class for author/committer/tagger lines 29 | class UserInfo 30 | attr_accessor :name, :email, :date, :offset 31 | 32 | def initialize(str) 33 | m = /^(.*?) <(.*)> (\d+) ([+-])0*(\d+?)$/.match(str) 34 | if !m 35 | raise RuntimeError, "invalid %s header in commit" % str 36 | end 37 | @name = m[1] 38 | @email = m[2] 39 | @date = Time.at(Integer(m[3])) 40 | @offset = (m[4] == "-" ? -1 : 1)*Integer(m[5]) 41 | end 42 | 43 | def to_s 44 | "%s <%s> %s %+05d" % [@name, @email, @date.to_i, @offset] 45 | end 46 | end 47 | 48 | # base class for all git objects (blob, tree, commit, tag) 49 | class Object 50 | attr_accessor :repository 51 | 52 | def Object.from_raw(rawobject, repository = nil) 53 | case rawobject.type 54 | when :blob 55 | return Blob.from_raw(rawobject, repository) 56 | when :tree 57 | return Tree.from_raw(rawobject, repository) 58 | when :commit 59 | return Commit.from_raw(rawobject, repository) 60 | when :tag 61 | return Tag.from_raw(rawobject, repository) 62 | else 63 | raise RuntimeError, "got invalid object-type" 64 | end 65 | end 66 | 67 | def initialize 68 | raise NotImplemented, "abstract class" 69 | end 70 | 71 | def type 72 | raise NotImplemented, "abstract class" 73 | end 74 | 75 | def raw_content 76 | raise NotImplemented, "abstract class" 77 | end 78 | 79 | def sha1 80 | Digest::SHA1.hexdigest("%s %d\0" % \ 81 | [self.type, self.raw_content.length] + \ 82 | self.raw_content) 83 | end 84 | end 85 | 86 | class Blob < Object 87 | attr_accessor :content 88 | 89 | def self.from_raw(rawobject, repository) 90 | new(rawobject.content) 91 | end 92 | 93 | def initialize(content, repository=nil) 94 | @content = content 95 | @repository = repository 96 | end 97 | 98 | def type 99 | :blob 100 | end 101 | 102 | def raw_content 103 | @content 104 | end 105 | end 106 | 107 | class DirectoryEntry 108 | S_IFMT = 00170000 109 | S_IFLNK = 0120000 110 | S_IFREG = 0100000 111 | S_IFDIR = 0040000 112 | 113 | attr_accessor :mode, :name, :sha1 114 | def initialize(buf) 115 | m = /^(\d+) (.*)\0(.{20})$/m.match(buf) 116 | if !m 117 | raise RuntimeError, "invalid directory entry" 118 | end 119 | @mode = 0 120 | m[1].each_byte do |i| 121 | @mode = (@mode << 3) | (i-'0'[0]) 122 | end 123 | @name = m[2] 124 | @sha1 = m[3].unpack("H*")[0] 125 | 126 | if ![S_IFLNK, S_IFDIR, S_IFREG].include?(@mode & S_IFMT) 127 | raise RuntimeError, "unknown type for directory entry" 128 | end 129 | end 130 | 131 | def type 132 | case @mode & S_IFMT 133 | when S_IFLNK 134 | @type = :link 135 | when S_IFDIR 136 | @type = :directory 137 | when S_IFREG 138 | @type = :file 139 | else 140 | raise RuntimeError, "unknown type for directory entry" 141 | end 142 | end 143 | 144 | def type=(type) 145 | case @type 146 | when :link 147 | @mode = (@mode & ~S_IFMT) | S_IFLNK 148 | when :directory 149 | @mode = (@mode & ~S_IFMT) | S_IFDIR 150 | when :file 151 | @mode = (@mode & ~S_IFMT) | S_IFREG 152 | else 153 | raise RuntimeError, "invalid type" 154 | end 155 | end 156 | 157 | def format_type 158 | case type 159 | when :link 160 | 'link' 161 | when :directory 162 | 'tree' 163 | when :file 164 | 'blob' 165 | end 166 | end 167 | 168 | def format_mode 169 | "%06o" % @mode 170 | end 171 | 172 | def raw 173 | "%o %s\0%s" % [@mode, @name, [@sha1].pack("H*")] 174 | end 175 | end 176 | 177 | class Tree < Object 178 | attr_accessor :entry 179 | 180 | def self.from_raw(rawobject, repository=nil) 181 | entries = [] 182 | rawobject.content.scan(/\d+ .*?\0.{20}/m) do |raw| 183 | entries << DirectoryEntry.new(raw) 184 | end 185 | new(entries, repository) 186 | end 187 | 188 | def initialize(entries=[], repository = nil) 189 | @entry = entries 190 | @repository = repository 191 | end 192 | 193 | def type 194 | :tree 195 | end 196 | 197 | def raw_content 198 | # TODO: sort correctly 199 | #@entry.sort { |a,b| a.name <=> b.name }. 200 | @entry.collect { |e| [[e.format_mode, e.format_type, e.sha1].join(' '), e.name].join("\t") }.join("\n") 201 | end 202 | 203 | def actual_raw 204 | #@entry.collect { |e| e.raw.join(' '), e.name].join("\t") }.join("\n") 205 | end 206 | end 207 | 208 | class Commit < Object 209 | attr_accessor :author, :committer, :tree, :parent, :message 210 | 211 | def self.from_raw(rawobject, repository=nil) 212 | parent = [] 213 | tree = author = committer = nil 214 | 215 | headers, message = rawobject.content.split(/\n\n/, 2) 216 | headers = headers.split(/\n/).map { |header| header.split(/ /, 2) } 217 | headers.each do |key, value| 218 | case key 219 | when "tree" 220 | tree = value 221 | when "parent" 222 | parent.push(value) 223 | when "author" 224 | author = UserInfo.new(value) 225 | when "committer" 226 | committer = UserInfo.new(value) 227 | else 228 | warn "unknown header '%s' in commit %s" % \ 229 | [key, rawobject.sha1.unpack("H*")[0]] 230 | end 231 | end 232 | if not tree && author && committer 233 | raise RuntimeError, "incomplete raw commit object" 234 | end 235 | new(tree, parent, author, committer, message, repository) 236 | end 237 | 238 | def initialize(tree, parent, author, committer, message, repository=nil) 239 | @tree = tree 240 | @author = author 241 | @parent = parent 242 | @committer = committer 243 | @message = message 244 | @repository = repository 245 | end 246 | 247 | def type 248 | :commit 249 | end 250 | 251 | def raw_content 252 | "tree %s\n%sauthor %s\ncommitter %s\n\n" % [ 253 | @tree, 254 | @parent.collect { |i| "parent %s\n" % i }.join, 255 | @author, @committer] + @message 256 | end 257 | end 258 | 259 | class Tag < Object 260 | attr_accessor :object, :type, :tag, :tagger, :message 261 | 262 | def self.from_raw(rawobject, repository=nil) 263 | headers, message = rawobject.content.split(/\n\n/, 2) 264 | headers = headers.split(/\n/).map { |header| header.split(/ /, 2) } 265 | headers.each do |key, value| 266 | case key 267 | when "object" 268 | object = value 269 | when "type" 270 | if !["blob", "tree", "commit", "tag"].include?(value) 271 | raise RuntimeError, "invalid type in tag" 272 | end 273 | type = value.to_sym 274 | when "tag" 275 | tag = value 276 | when "tagger" 277 | tagger = UserInfo.new(value) 278 | else 279 | warn "unknown header '%s' in tag" % \ 280 | [key, rawobject.sha1.unpack("H*")[0]] 281 | end 282 | if not object && type && tag && tagger 283 | raise RuntimeError, "incomplete raw tag object" 284 | end 285 | end 286 | new(object, type, tag, tagger, repository) 287 | end 288 | 289 | def initialize(object, type, tag, tagger, repository=nil) 290 | @object = object 291 | @type = type 292 | @tag = tag 293 | @tagger = tagger 294 | @repository = repository 295 | end 296 | 297 | def raw_content 298 | "object %s\ntype %s\ntag %s\ntagger %s\n\n" % \ 299 | [@object, @type, @tag, @tagger] + @message 300 | end 301 | 302 | def type 303 | :tag 304 | end 305 | end 306 | 307 | end 308 | --------------------------------------------------------------------------------