Andre Nathan <andre / digirati.com.br> writes:

> I've been working with subversion for some time now and thought I'd have
> a look at trying one of the distributed SCMs. I read about many of them,
> and ended up with a choice between mercurial and git.
>
> Since both seem to be quite similar, and mercurial is written in python,
> I was wondering if someone is working (or at least thinking of) writing
> ruby wrappers for git, so we could get rid of the stinky shell scripts
> from cogito.
>
> Any git users here at all? I wonder if there's a libgit that could be
> wrapped in an extension...

I gave it a try some months ago, but lost interest.


require 'dl/import'
require 'dl/struct'

module Git
  extend DL::Importable

  dlload "/Users/chris/src/git/libgit.bundle"

  extern "const char *setup_git_directory()"

  extern "int git_config (void*)"
  #  extern "int git_default_config (const char *, const char *)"

  extern "int get_sha1 (const char *, unsigned char *)"

  extern "void *parse_tree_indirect(const unsigned char *)"

  extern "int read_tree_recursive(void*, const char*, int, int, const char*, void*)"
#  extern "int read_tree_recursive(void*, int, int, int, int, void*)"

  extern "int sha1_object_info(const unsigned char*, void*, long*)"
  extern "void* read_sha1_file(const unsigned char*, void*, long*)"

  extern "int index_fd(unsigned char *, int, void *, int, const char *)"
  extern "int index_pipe(unsigned char *, int, int, const char *)"

  extern "void* parse_object(const unsigned char *)"


  Object = struct [
                   "long flags",
                   "char sha1[20]",
                  ]

  TreeStruct = struct [
                       "void *object",
                       "void *buffer",
                       "long size",
                      ]

  def self.config
    git_config(symbol('git_default_config', 'ISS'))
  end

  def self.hash_object(thing, type="blob")
    buf = DL.malloc(20)

    if thing.kind_of? String    # filename
      rstat = File.stat thing
      stat = DL::PtrData.new(rstat.object_id*2).to_a('P', 5).last
      file = File.open(thing)
      index_fd(buf, file.fileno, stat, 0, type) # will close
    elsif thing.kind_of? IO
      index_pipe(buf, thing.fileno, 0, type)
    else
      raise "Can't hash #{thing}"
    end
    SHA1.new buf.to_s(20)
  end

  class Commit
    def initialize(string)
      replace string
    end

    def replace(string)
      head, @body = string.split("\n\n", 2)

      @fields = {}
      head.each { |line|
        field, value = line.chomp.split(" ", 2)
        
        if field == "tree" || field == "parent"
          value = SHA1[value]
        end

        if field == "parent"
          (@fields[field] ||= []) << value
        else
          @fields[field] = value
        end
      }
    end

    def title
      @body[/.*/]
    end
    
    def [](field)
      @fields[field]
    end
  end

  class SHA1
    attr_reader :data

    def self.[](str)
      if str.size == 20
        new str
      elsif str.size == 40
        p str
        new [str].pack("H*")
      else
        raise "Can't make hash object from #{str}"
      end
    end
    
    def initialize(sha = '\0'*20)
      @data = sha
    end

    def inspect
      type
      "#<%s:0x%x %s %s(%d)>" % [self.class.name, object_id, type, to_s, @size]
    end
    
    def to_s
      @data.unpack("C*").map { |byte| "%02x" % byte }.join
    end

    def type
      buf = DL.malloc(16)
      buf2 = DL.malloc(4)
      Git.sha1_object_info(@data, buf, buf2)
      @size = buf2.to_s(4).unpack("L").first
      @type = buf.to_s
    end

    def read
      buf = DL.malloc(16)
      buf2 = DL.malloc(4)
      buf3 = Git.read_sha1_file(@data, buf, buf2)
      @size = buf2.to_s(4).unpack("L").first
      @type = buf.to_s
      data = buf3.to_s(@size)

      case @type
      when "commit"
        Commit.new data
      else
        raise "Can't deal with #{@type} yet."
      end
    end

    def size
      type
      @size
    end

    def parse
      obj = Git.parse_object @data
      Object.new obj
    end
  end

  class Tree
    def initialize(sha1)
      @tree = TreeStruct.new Git.parse_tree_indirect(sha1.data)
    end

    READ_TREE = DL.callback("IPPIPII") {
      |sha1, base, baselen, pathname, mode, stage|
      s = Git::SHA1.new(sha1.to_s(20))

      if base
        path = base.to_s(baselen)
      else
        path = ""
      end
      path << pathname.to_s

      id = path.slice!(0, 4).unpack("L").first
      block = ObjectSpace._id2ref(id & ~1)

      block.call [s, path, mode, stage]
      id & 1
    }

    def each(recursive=false, &block)
      Git.read_tree_recursive(@tree, [block.object_id | (recursive ? 1 : 0)].
                                      pack("L"), 4, 0, nil, READ_TREE)
    end
  end
  
  def self.sha1(name)
    buffer = DL.malloc(20)
#    p buffer.to_s(20)
    get_sha1(name, buffer)
    SHA1.new buffer.to_s(20)
  end

  def self.parse_tree(sha)
    Tree.new parse_tree_indirect(sha.data)
  end
end

Dir.chdir(File.expand_path("~/src/git")) {
  p Git.setup_git_directory
  p Git.config

  x = Git.sha1("HEAD")
  p x
  p x.type
  commit = x.read

  while commit['parent']
    p commit.title
    commit = commit['parent'].first.read
  end

  Git::Tree.new(x).each { |sh1, file|
    p [sh1, Git::SHA1.new(sh1.parse.sha1.pack("c*"))]
    p file
  }
}

__END__
  x=Time.now
  Dir.chdir("testrep")
  1000.times {
    `git-hash-object Makefile`
  }
  p Time.now-x

__END__
  head = Git.sha1("HEAD")
  p head
  p head.type
  tree = Git.parse_tree(head)
  p tree
  read_tree = DL.callback("IPPIPII") {
    |sha1, base, baselen, pathname, mode, stage|
    s = Git::SHA1.new(sha1.to_s(20))
    p s
    p s.type
    if base
      path = base.to_s(baselen)
    else
      path = ""
    end
    path << pathname.to_s
    p path
    p mode.to_s(8)
    p stage
    1
  }
  p Git.read_tree_recursive(tree, 0, 0, 0, 0, read_tree)
}

-- 
Christian Neukirchen  <chneukirchen / gmail.com>  http://chneukirchen.org