gitlabhq/grit

View on GitHub
lib/grit/git-ruby.rb

Summary

Maintainability
D
1 day
Test Coverage
require 'grit/git-ruby/repository'

module Grit

  # the functions in this module intercept the calls to git binary
  # made by the grit objects and attempts to run them in pure ruby
  # if it will be faster, or if the git binary is not available (!!TODO!!)
  module GitRuby

    attr_accessor :ruby_git_repo, :git_file_index

    def init(options, *args)
      if options.size == 0
        Grit::GitRuby::Repository.init(@git_dir)
      else
        method_missing('init', options, *args)
      end
    end

    def cat_file(options, sha)
      if options[:t]
        file_type(sha)
      elsif options[:s]
        file_size(sha)
      elsif options[:p]
        try_run { ruby_git.cat_file(sha) }
      end
    rescue Grit::GitRuby::Repository::NoSuchShaFound
      ''
    end

    def cat_ref(options, ref)
      sha = rev_parse({}, ref)
      cat_file(options, sha)
    end

    # lib/grit/tree.rb:16:      output = repo.git.ls_tree({}, treeish, *paths)
    def ls_tree(options, treeish, *paths)
      sha = rev_parse({}, treeish)
      ruby_git.ls_tree(sha, paths.flatten, options.delete(:r))
    rescue Grit::GitRuby::Repository::NoSuchShaFound
      ''
    end

    # git diff --full-index 'ec037431382e83c3e95d4f2b3d145afbac8ea55d' 'f1ec1aea10986159456846b8a05615b87828d6c6'
    def diff(options, sha1, sha2 = nil)
      try_run { ruby_git.diff(sha1, sha2, options) }
    end

    def rev_list(options, *refs)
      refs = ['master'] if refs.empty?
      options.delete(:skip) if options[:skip].to_i == 0
      allowed_options = [:max_count, :since, :until, :pretty]  # this is all I can do right now
      if ((options.keys - allowed_options).size > 0) || refs.size > 1
        method_missing('rev-list', options, *refs)
      elsif (options.size == 0)
        # pure rev-list
        ref = refs.first
        begin
          file_index.commits_from(rev_parse({}, ref)).join("\n") + "\n"
        rescue
          method_missing('rev-list', options, *refs)
        end
      else
        ref = refs.first
        aref = rev_parse({:verify => true}, ref)
        if aref.is_a? Array
          method_missing('rev-list', options, *refs)
        else
          try_run { ruby_git.rev_list(aref, options) }
        end
      end
    end

    def rev_parse(options, string)
      raise RuntimeError, "invalid string: #{string.inspect}" unless string.is_a?(String)

      # Split ranges, but don't split when specifying a ref:path.
      # Don't split HEAD:some/path/in/repo..txt
      # Do split sha1..sha2
      if string !~ /:/ && string =~ /\.\./
        (sha1, sha2) = string.split('..')
        return [rev_parse({}, sha1), rev_parse({}, sha2)]
      end

      if /^[0-9a-f]{40}$/.match(string)  # passing in a sha - just no-op it
        return string.chomp
      end

      head = File.join(@git_dir, 'refs', 'heads', string)
      return File.read(head).chomp if File.file?(head)

      head = File.join(@git_dir, 'refs', 'remotes', string)
      return File.read(head).chomp if File.file?(head)

      head = File.join(@git_dir, 'refs', 'tags', string)
      return File.read(head).chomp if File.file?(head)

      ## check packed-refs file, too
      packref = File.join(@git_dir, 'packed-refs')
      if File.file?(packref)
        File.readlines(packref).each do |line|
          if m = /^(\w{40}) refs\/.+?\/(.*?)$/.match(line)
            next if !Regexp.new(Regexp.escape(string) + '$').match(m[3])
            return m[1].chomp
          end
        end
      end

      ## !! more - partials and such !!

      # revert to calling git - grr
      return method_missing('rev-parse', options, string).chomp
    end

    def refs(options, prefix)
      refs = []
      already = {}
      orig_prefix = prefix
      prefix = File.join @git_dir, prefix

      files = Dir.glob(prefix + '/**/*')
      files.each do |ref|
        next if !File.file?(ref)
        id = File.read(ref).chomp
        name = ref.sub("#{prefix}/", '')
        if !already[name]
          refs << "#{name} #{id}"
          already[name] = true
        end
      end

      packed = File.join(@git_dir, 'packed-refs')
      if File.file?(packed)
        File.readlines(packed).each do |line|
          if m = /^(\w{40}) (.*?)$/.match(line)
            next if !Regexp.new('^' + orig_prefix).match(m[2])
            name = m[2].sub("#{orig_prefix}/", '')
            if !already[name]
              refs << "#{name} #{m[1]}"
              already[name] = true
            end
          end
        end
      end

      refs.join("\n")
    end

    def tags(options, prefix)
      refs = []
      already = {}

      orig_prefix = prefix
      prefix = File.join @git_dir, prefix

      files = Dir.glob(prefix + '/**/*')

      files.each do |ref|
        next if !File.file?(ref)

        id = File.read(ref).chomp
        name = ref.sub("#{prefix}/", '')

        if !already[name]
          refs << "#{name} #{id}"
          already[name] = true
        end
      end

      packed = File.join(@git_dir, 'packed-refs')
      if File.file?(packed)
        lines = File.readlines('packed-refs')
        lines.each_with_index do |line, i|
          if m = /^(\w{40}) (.*?)$/.match(line)
            next if !Regexp.new('^' + orig_prefix).match(m[2])
            name = m[2].sub("#{orig_prefix}/", '')

            # Annotated tags in packed-refs include a reference
            # to the commit object on the following line.
            next_line = lines[i + 1]

            id =
              if next_line && next_line[0] == ?^
                next_line[1..-1].chomp
              else
                m[1]
              end

            if !already[name]
              refs << "#{name} #{id}"
              already[name] = true
            end
          end
        end
      end

      refs.join("\n")
    end

    def file_size(ref)
      try_run { ruby_git.cat_file_size(ref).to_s }
    end

    def file_type(ref)
      try_run { ruby_git.cat_file_type(ref).to_s }
    end

    def ruby_git
      @ruby_git_repo ||= Repository.new(@git_dir)
    end

    private

      def try_run
        ret = ''
        Timeout.timeout(self.class.git_timeout) do
          ret = yield
        end
        @bytes_read += ret.size

        #if @bytes_read > 5242880 # 5.megabytes
        #  bytes = @bytes_read
        #  @bytes_read = 0
        #  raise Grit::Git::GitTimeout.new(command, bytes)
        #end

        ret
      rescue Timeout::Error => e
        bytes = @bytes_read
        @bytes_read = 0
        raise Grit::Git::GitTimeout.new(command, bytes)
      end

      def looking_for(commit, path = nil)
        tree_sha = ruby_git.get_subtree(rev_parse({}, commit), path)

        looking_for = []
        ruby_git.get_object_by_sha1(tree_sha).entry.each do |e|
          if path && !(path == '' || path == '.' || path == './')
            file = File.join(path, e.name)
          else
            file = e.name
          end
          file += '/' if e.type == :directory
          looking_for << file
        end
        looking_for
      end

      def clean_paths(commit_array)
        new_commits = {}
        commit_array.each do |file, sha|
          file = file.chop if file[file.size - 1 , 1] == '/'
          new_commits[file] = sha
        end
        new_commits
      end

    # TODO
    # git grep -n 'foo' 'master'
    # git log --pretty='raw' --max-count='1' 'master' -- 'LICENSE'
    # git log --pretty='raw' --max-count='1' 'master' -- 'test'

  end
end