├── VERSION ├── .rspec ├── .gitignore ├── lib ├── gitlab_git │ ├── tag.rb │ ├── branch.rb │ ├── blame.rb │ ├── blob_snippet.rb │ ├── popen.rb │ ├── ref.rb │ ├── git_stats.rb │ ├── log_parser.rb │ ├── encoding_herlper.rb │ ├── compare.rb │ ├── stats.rb │ ├── diff.rb │ ├── blob.rb │ ├── tree.rb │ ├── commit.rb │ └── repository.rb └── gitlab_git.rb ├── spec ├── support │ ├── repo.rb │ ├── seed_helper.rb │ ├── ruby_blob.rb │ ├── big_commit.rb │ ├── first_commit.rb │ ├── last_commit.rb │ └── commit.rb ├── tag_spec.rb ├── branch_spec.rb ├── stats_spec.rb ├── git_stats_spec.rb ├── compare_spec.rb ├── spec_helper.rb ├── diff_spec.rb ├── log_parser_spec.rb ├── blob_spec.rb ├── tree_spec.rb ├── repository_spec.rb └── commit_spec.rb ├── .travis.yml ├── Gemfile ├── Guardfile ├── gitlab_git.gemspec ├── LICENSE ├── CHANGELOG ├── Gemfile.lock └── README.md /VERSION: -------------------------------------------------------------------------------- 1 | 6.1.0 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format progress 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | support/testme.git* 3 | tmp/* 4 | coverage/* 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /lib/gitlab_git/tag.rb: -------------------------------------------------------------------------------- 1 | module Gitlab 2 | module Git 3 | class Tag < Ref 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/gitlab_git/branch.rb: -------------------------------------------------------------------------------- 1 | module Gitlab 2 | module Git 3 | class Branch < Ref 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/support/repo.rb: -------------------------------------------------------------------------------- 1 | module SeedRepo 2 | module Repo 3 | HEAD = "master" 4 | BRANCHES = ["feature", "fix", "master"] 5 | TAGS = ["v1.0.0", 'v1.1.0'] 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | env: 3 | - TRAVIS=true 4 | before_install: 5 | - sudo apt-get install libicu-dev -y 6 | branches: 7 | only: 8 | - 'master' 9 | rvm: 10 | - 1.9.3-p327 11 | - 2.0.0 12 | before_script: 13 | - "bundle install" 14 | script: "bundle exec rspec spec" 15 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec 4 | 5 | group :development do 6 | gem 'coveralls', require: false 7 | gem 'rspec' 8 | gem 'webmock' 9 | gem 'guard' 10 | gem 'guard-rspec' 11 | gem 'pry' 12 | end 13 | 14 | group :test do 15 | gem 'simplecov', require: false 16 | end 17 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | guard 'rspec' do 5 | watch(%r{^spec/.+_spec\.rb$}) 6 | watch('spec/spec_helper.rb') { "spec" } 7 | watch(%r{^lib/gitlab_git/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } 8 | watch(%r{^spec/support/(.+)\.rb$}) { "spec" } 9 | end 10 | 11 | -------------------------------------------------------------------------------- /spec/support/seed_helper.rb: -------------------------------------------------------------------------------- 1 | module SeedHelper 2 | def ensure_seeds 3 | unless File.exists?(TEST_REPO_PATH) 4 | create_seeds 5 | end 6 | end 7 | 8 | def create_seeds 9 | puts 'Prepare seeds' 10 | FileUtils.mkdir_p(SUPPORT_PATH) 11 | system(*%W(git clone --bare https://github.com/gitlabhq/testme.git), chdir: SUPPORT_PATH) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/gitlab_git/blame.rb: -------------------------------------------------------------------------------- 1 | module Gitlab 2 | module Git 3 | class Blame 4 | attr_accessor :repository, :sha, :path 5 | 6 | def initialize(repository, sha, path) 7 | @repository, @sha, @path = repository, sha, path 8 | end 9 | 10 | def each 11 | raw_blame = Grit::Blob.blame(repository.raw, sha, path) 12 | 13 | raw_blame.each do |commit, lines| 14 | next unless commit 15 | 16 | commit = Gitlab::Git::Commit.new(commit) 17 | yield(commit, lines) 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/tag_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Gitlab::Git::Tag do 4 | let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } 5 | 6 | describe 'first tag' do 7 | let(:tag) { repository.tags.first } 8 | 9 | it { tag.name.should == "v1.0.0" } 10 | it { tag.target.should == "f4e6814c3e4e7a0de82a9e7cd20c626cc963a2f8" } 11 | end 12 | 13 | describe 'last tag' do 14 | let(:tag) { repository.tags.last } 15 | 16 | it { tag.name.should == "v1.1.0" } 17 | it { tag.target.should == "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" } 18 | end 19 | 20 | it { repository.tags.size.should == 2 } 21 | end 22 | -------------------------------------------------------------------------------- /lib/gitlab_git/blob_snippet.rb: -------------------------------------------------------------------------------- 1 | module Gitlab 2 | module Git 3 | class BlobSnippet 4 | include Linguist::BlobHelper 5 | 6 | attr_accessor :ref 7 | attr_accessor :lines 8 | attr_accessor :filename 9 | attr_accessor :startline 10 | 11 | def initialize(ref, lines, startline, filename) 12 | @ref, @lines, @startline, @filename = ref, lines, startline, filename 13 | end 14 | 15 | def data 16 | lines.join("\n") 17 | end 18 | 19 | def name 20 | filename 21 | end 22 | 23 | def size 24 | data.length 25 | end 26 | 27 | def mode 28 | nil 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/gitlab_git/popen.rb: -------------------------------------------------------------------------------- 1 | require 'open3' 2 | 3 | module Gitlab 4 | module Git 5 | module Popen 6 | def popen(cmd, path) 7 | unless cmd.is_a?(Array) 8 | raise "System commands must be given as an array of strings" 9 | end 10 | 11 | vars = { "PWD" => path } 12 | options = { :chdir => path } 13 | 14 | @cmd_output = "" 15 | @cmd_status = 0 16 | Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| 17 | @cmd_output << stdout.read 18 | @cmd_output << stderr.read 19 | @cmd_status = wait_thr.value.exitstatus 20 | end 21 | 22 | return @cmd_output, @cmd_status 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/gitlab_git/ref.rb: -------------------------------------------------------------------------------- 1 | module Gitlab 2 | module Git 3 | class Ref 4 | # Branch or tag name 5 | # without "refs/tags|heads" prefix 6 | attr_reader :name 7 | 8 | # Target sha. 9 | # Usually it is commit sha but in case 10 | # when tag reference on other tag it can be tag sha 11 | attr_reader :target 12 | 13 | # Extract branch name from full ref path 14 | # 15 | # Ex. 16 | # Ref.extract_branch_name('refs/heads/master') #=> 'master' 17 | def self.extract_branch_name(str) 18 | str.gsub(/\Arefs\/heads\//, '') 19 | end 20 | 21 | def initialize(name, target) 22 | @name, @target = name.gsub(/\Arefs\/(tags|heads)\//, ''), target 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /gitlab_git.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'gitlab_git' 3 | s.version = `cat VERSION` 4 | s.date = Time.now.strftime("%Y-%m-%d") 5 | s.summary = "Gitlab::Git library" 6 | s.description = "GitLab wrapper around git objects" 7 | s.authors = ["Dmitriy Zaporozhets"] 8 | s.email = 'dmitriy.zaporozhets@gmail.com' 9 | s.license = 'MIT' 10 | s.files = `git ls-files lib/`.split("\n") << 'VERSION' 11 | s.homepage = 12 | 'http://rubygems.org/gems/gitlab_git' 13 | 14 | s.add_dependency("gitlab-linguist", "~> 3.0") 15 | s.add_dependency("gitlab-grit", "~> 2.6") 16 | s.add_dependency("activesupport", "~> 4.0") 17 | s.add_dependency("rugged", "~> 0.19.0") 18 | s.add_dependency("charlock_holmes", "~> 0.6") 19 | end 20 | -------------------------------------------------------------------------------- /spec/branch_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Gitlab::Git::Branch do 4 | let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } 5 | 6 | subject { repository.branches } 7 | 8 | it { should be_kind_of Array } 9 | its(:size) { should eq(3) } 10 | 11 | describe 'first branch' do 12 | let(:branch) { repository.branches.first } 13 | 14 | it { branch.name.should == SeedRepo::Repo::BRANCHES.first } 15 | it { branch.target.should == "0b4bc9a49b562e85de7cc9e834518ea6828729b9" } 16 | end 17 | 18 | describe 'last branch' do 19 | let(:branch) { repository.branches.last } 20 | 21 | it { branch.name.should == SeedRepo::Repo::BRANCHES.last } 22 | it { branch.target.should == SeedRepo::LastCommit::ID } 23 | end 24 | 25 | it { repository.branches.size.should == SeedRepo::Repo::BRANCHES.size } 26 | end 27 | -------------------------------------------------------------------------------- /lib/gitlab_git/git_stats.rb: -------------------------------------------------------------------------------- 1 | require_relative 'log_parser' 2 | 3 | module Gitlab 4 | module Git 5 | class GitStats 6 | attr_accessor :repo, :ref, :timeout 7 | 8 | def initialize(repo, ref, timeout = 30) 9 | @repo, @ref, @timeout = repo, ref, timeout 10 | end 11 | 12 | def log 13 | log = nil 14 | Grit::Git.with_timeout(timeout) do 15 | # Limit log to 6k commits to avoid timeout for huge projects 16 | args = [ref, '-6000', '--format=%aN%x0a%aE%x0a%cd', '--date=short', '--shortstat', '--no-merges', '--diff-filter=ACDM'] 17 | log = repo.git.native(:log, {}, args) 18 | end 19 | 20 | log 21 | rescue Grit::Git::GitTimeout 22 | nil 23 | end 24 | 25 | def parsed_log 26 | LogParser.parse_log(log) 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/stats_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Gitlab::Git::Stats do 4 | let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } 5 | 6 | before do 7 | @stats = Gitlab::Git::Stats.new(repository.raw, 'master') 8 | end 9 | 10 | describe :authors do 11 | let(:author) { @stats.authors.first } 12 | 13 | it { author.name.should == 'Dmitriy Zaporozhets' } 14 | it { author.email.should == 'dmitriy.zaporozhets@gmail.com' } 15 | it { author.commits.should == 13 } 16 | end 17 | 18 | describe :graph do 19 | let(:graph) { @stats.graph } 20 | 21 | it { graph.labels.should include Date.today.strftime('%b %d') } 22 | it { graph.commits.should be_kind_of(Array) } 23 | it { graph.weeks.should == 4 } 24 | end 25 | 26 | it { @stats.commits_count.should == 13 } 27 | it { @stats.files_count.should == 31 } 28 | end 29 | -------------------------------------------------------------------------------- /spec/git_stats_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Gitlab::Git::GitStats do 4 | describe "#parsed_log" do 5 | let(:stats) { Gitlab::Git::GitStats.new(nil, nil) } 6 | 7 | before(:each) do 8 | stats.stub(:log).and_return("anything") 9 | end 10 | 11 | context "LogParser#parse_log returns 'test'" do 12 | it "returns 'test'" do 13 | Gitlab::Git::LogParser.stub(:parse_log).and_return("test") 14 | stats.parsed_log.should eq("test") 15 | end 16 | end 17 | end 18 | 19 | describe "#log" do 20 | let(:repo) { double(Gitlab::Git::Repository).as_null_object } 21 | let(:gs) { Gitlab::Git::GitStats.new(repo.raw, repo.root_ref) } 22 | 23 | context "repo.git.native returns 'test'" do 24 | it "returns 'test'" do 25 | repo.raw.git.stub(:native).and_return("test") 26 | gs.log.should eq("test") 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/gitlab_git/log_parser.rb: -------------------------------------------------------------------------------- 1 | module Gitlab 2 | module Git 3 | class LogParser 4 | # Parses the log file into a collection of commits 5 | # Data model: 6 | # {author_name, author_email, date, additions, deletions} 7 | def self.parse_log(log_from_git) 8 | log = log_from_git.split("\n") 9 | collection = [] 10 | 11 | log.each_slice(5) do |slice| 12 | entry = {} 13 | entry[:author_name] = slice[0].force_encoding('UTF-8') 14 | entry[:author_email] = slice[1].force_encoding('UTF-8') 15 | entry[:date] = slice[2] 16 | 17 | if slice[4] 18 | changes = slice[4] 19 | 20 | entry[:additions] = $1.to_i if changes =~ /(\d+) insertion/ 21 | entry[:deletions] = $1.to_i if changes =~ /(\d+) deletion/ 22 | end 23 | 24 | collection.push(entry) 25 | end 26 | 27 | collection 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/gitlab_git.rb: -------------------------------------------------------------------------------- 1 | # Libraries 2 | require 'ostruct' 3 | require 'fileutils' 4 | require 'grit' 5 | require 'linguist' 6 | require 'active_support/core_ext/hash/keys' 7 | require 'active_support/core_ext/object/try' 8 | require 'grit' 9 | require 'grit_ext' 10 | require 'rugged' 11 | require "charlock_holmes" 12 | 13 | Grit::Blob.class_eval do 14 | include Linguist::BlobHelper 15 | end 16 | 17 | # Gitlab::Git 18 | require_relative "gitlab_git/popen" 19 | require_relative "gitlab_git/encoding_herlper" 20 | require_relative "gitlab_git/blame" 21 | require_relative "gitlab_git/blob" 22 | require_relative "gitlab_git/commit" 23 | require_relative "gitlab_git/compare" 24 | require_relative "gitlab_git/diff" 25 | require_relative "gitlab_git/repository" 26 | require_relative "gitlab_git/stats" 27 | require_relative "gitlab_git/tree" 28 | require_relative "gitlab_git/blob_snippet" 29 | require_relative "gitlab_git/git_stats" 30 | require_relative "gitlab_git/log_parser" 31 | require_relative "gitlab_git/ref" 32 | require_relative "gitlab_git/branch" 33 | require_relative "gitlab_git/tag" 34 | -------------------------------------------------------------------------------- /spec/support/ruby_blob.rb: -------------------------------------------------------------------------------- 1 | # 2 | # From SeedRepo::Commit::ID 3 | # 4 | module SeedRepo 5 | module RubyBlob 6 | ID = "7e3e39ebb9b2bf433b4ad17313770fbe4051649c" 7 | NAME = "popen.rb" 8 | CONTENT = <<-eos 9 | require 'fileutils' 10 | require 'open3' 11 | 12 | module Popen 13 | extend self 14 | 15 | def popen(cmd, path=nil) 16 | unless cmd.is_a?(Array) 17 | raise RuntimeError, "System commands must be given as an array of strings" 18 | end 19 | 20 | path ||= Dir.pwd 21 | 22 | vars = { 23 | "PWD" => path 24 | } 25 | 26 | options = { 27 | chdir: path 28 | } 29 | 30 | unless File.directory?(path) 31 | FileUtils.mkdir_p(path) 32 | end 33 | 34 | @cmd_output = "" 35 | @cmd_status = 0 36 | 37 | Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| 38 | @cmd_output << stdout.read 39 | @cmd_output << stderr.read 40 | @cmd_status = wait_thr.value.exitstatus 41 | end 42 | 43 | return @cmd_output, @cmd_status 44 | end 45 | end 46 | eos 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Dmitriy Zaporozhets 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /lib/gitlab_git/encoding_herlper.rb: -------------------------------------------------------------------------------- 1 | module EncodingHelper 2 | extend self 3 | 4 | def encode!(message) 5 | return nil unless message.respond_to? :force_encoding 6 | 7 | # if message is utf-8 encoding, just return it 8 | message.force_encoding("UTF-8") 9 | return message if message.valid_encoding? 10 | 11 | # return message if message type is binary 12 | detect = CharlockHolmes::EncodingDetector.detect(message) 13 | return message.force_encoding("BINARY") if detect && detect[:type] == :binary 14 | 15 | # encoding message to detect encoding 16 | if detect && detect[:encoding] 17 | message.force_encoding(detect[:encoding]) 18 | end 19 | 20 | # encode and clean the bad chars 21 | message.replace clean(message) 22 | rescue 23 | encoding = detect ? detect[:encoding] : "unknown" 24 | "--broken encoding: #{encoding}" 25 | end 26 | 27 | private 28 | 29 | def clean(message) 30 | message.encode("UTF-16BE", :undef => :replace, :invalid => :replace, :replace => "") 31 | .encode("UTF-8") 32 | .gsub("\0".encode("UTF-8"), "") 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/compare_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Gitlab::Git::Compare do 4 | let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } 5 | let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID) } 6 | 7 | describe :commits do 8 | subject do 9 | compare.commits.map(&:id) 10 | end 11 | 12 | it { should have(8).elements } 13 | it { should include(SeedRepo::Commit::PARENT_ID) } 14 | it { should_not include(SeedRepo::BigCommit::PARENT_ID) } 15 | end 16 | 17 | describe :diffs do 18 | subject do 19 | compare.diffs.map(&:new_path) 20 | end 21 | 22 | it { should have(10).elements } 23 | it { should include('files/ruby/popen.rb') } 24 | it { should_not include('LICENSE') } 25 | it { compare.timeout.should be_false } 26 | it { compare.empty_diff?.should be_false } 27 | end 28 | 29 | describe 'non-existing refs' do 30 | let(:compare) { Gitlab::Git::Compare.new(repository, 'no-such-branch', '1234567890') } 31 | 32 | it { compare.commits.should be_empty } 33 | it { compare.diffs.should be_empty } 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/support/big_commit.rb: -------------------------------------------------------------------------------- 1 | # Seed repo: 2 | # 5937ac0a7beb003549fc5fd26fc247adbce4a52e Add submodule from gitlab.com 3 | # 570e7b2abdd848b95f2f578043fc23bd6f6fd24d Change some files 4 | # 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 More submodules 5 | # d14d6c0abdd253381df51a723d58691b2ee1ab08 Remove ds_store files 6 | # c1acaa58bbcbc3eafe538cb8274ba387047b69f8 Ignore DS files 7 | # ae73cb07c9eeaf35924a10f713b364d32b2dd34f Binary file added 8 | # 874797c3a73b60d2187ed6e2fcabd289ff75171e Ruby files modified 9 | # 2f63565e7aac07bcdadb654e253078b727143ec4 Modified image 10 | # 33f3729a45c02fc67d00adb1b8bca394b0e761d9 Image added 11 | # 913c66a37b4a45b9769037c55c2d238bd0942d2e Files, encoding and much more 12 | # cfe32cf61b73a0d5e9f13e774abde7ff789b1660 Add submodule 13 | # 6d394385cf567f80a8fd85055db1ab4c5295806f Added contributing guide 14 | # 1a0b36b3cdad1d2ee32457c102a8c0b7056fa863 Initial commit 15 | # 16 | module SeedRepo 17 | module BigCommit 18 | ID = "913c66a37b4a45b9769037c55c2d238bd0942d2e" 19 | PARENT_ID = "cfe32cf61b73a0d5e9f13e774abde7ff789b1660" 20 | MESSAGE = "Files, encoding and much more" 21 | AUTHOR_FULL_NAME = "Dmitriy Zaporozhets" 22 | FILES_COUNT = 2 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/support/first_commit.rb: -------------------------------------------------------------------------------- 1 | # Seed repo: 2 | # 5937ac0a7beb003549fc5fd26fc247adbce4a52e Add submodule from gitlab.com 3 | # 570e7b2abdd848b95f2f578043fc23bd6f6fd24d Change some files 4 | # 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 More submodules 5 | # d14d6c0abdd253381df51a723d58691b2ee1ab08 Remove ds_store files 6 | # c1acaa58bbcbc3eafe538cb8274ba387047b69f8 Ignore DS files 7 | # ae73cb07c9eeaf35924a10f713b364d32b2dd34f Binary file added 8 | # 874797c3a73b60d2187ed6e2fcabd289ff75171e Ruby files modified 9 | # 2f63565e7aac07bcdadb654e253078b727143ec4 Modified image 10 | # 33f3729a45c02fc67d00adb1b8bca394b0e761d9 Image added 11 | # 913c66a37b4a45b9769037c55c2d238bd0942d2e Files, encoding and much more 12 | # cfe32cf61b73a0d5e9f13e774abde7ff789b1660 Add submodule 13 | # 6d394385cf567f80a8fd85055db1ab4c5295806f Added contributing guide 14 | # 1a0b36b3cdad1d2ee32457c102a8c0b7056fa863 Initial commit 15 | # 16 | module SeedRepo 17 | module FirstCommit 18 | ID = "1a0b36b3cdad1d2ee32457c102a8c0b7056fa863" 19 | PARENT_ID = nil 20 | MESSAGE = "Initial commit" 21 | AUTHOR_FULL_NAME = "Dmitriy Zaporozhets" 22 | FILES = ["LICENSE", ".gitignore", "README.md"] 23 | FILES_COUNT = 3 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | if ENV['TRAVIS'] 2 | require 'coveralls' 3 | Coveralls.wear! 4 | else 5 | require 'simplecov' 6 | SimpleCov.start 7 | end 8 | 9 | require 'gitlab_git' 10 | require 'pry' 11 | 12 | require_relative 'support/seed_helper' 13 | require_relative 'support/commit' 14 | require_relative 'support/first_commit' 15 | require_relative 'support/last_commit' 16 | require_relative 'support/big_commit' 17 | require_relative 'support/ruby_blob' 18 | require_relative 'support/repo' 19 | 20 | RSpec::Matchers.define :be_valid_commit do 21 | match do |actual| 22 | actual != nil 23 | actual.id == SeedRepo::Commit::ID 24 | actual.message == SeedRepo::Commit::MESSAGE 25 | actual.author_name == SeedRepo::Commit::AUTHOR_FULL_NAME 26 | end 27 | end 28 | 29 | SUPPORT_PATH = File.join(File.expand_path(File.dirname(__FILE__)), '../support') 30 | TEST_REPO_PATH = File.join(SUPPORT_PATH, 'testme.git') 31 | 32 | RSpec.configure do |config| 33 | config.treat_symbols_as_metadata_keys_with_true_values = true 34 | config.run_all_when_everything_filtered = true 35 | config.filter_run :focus 36 | config.order = 'random' 37 | config.include SeedHelper 38 | config.before(:all) { ensure_seeds } 39 | end 40 | -------------------------------------------------------------------------------- /spec/support/last_commit.rb: -------------------------------------------------------------------------------- 1 | # Seed repo: 2 | # 5937ac0a7beb003549fc5fd26fc247adbce4a52e Add submodule from gitlab.com 3 | # 570e7b2abdd848b95f2f578043fc23bd6f6fd24d Change some files 4 | # 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 More submodules 5 | # d14d6c0abdd253381df51a723d58691b2ee1ab08 Remove ds_store files 6 | # c1acaa58bbcbc3eafe538cb8274ba387047b69f8 Ignore DS files 7 | # ae73cb07c9eeaf35924a10f713b364d32b2dd34f Binary file added 8 | # 874797c3a73b60d2187ed6e2fcabd289ff75171e Ruby files modified 9 | # 2f63565e7aac07bcdadb654e253078b727143ec4 Modified image 10 | # 33f3729a45c02fc67d00adb1b8bca394b0e761d9 Image added 11 | # 913c66a37b4a45b9769037c55c2d238bd0942d2e Files, encoding and much more 12 | # cfe32cf61b73a0d5e9f13e774abde7ff789b1660 Add submodule 13 | # 6d394385cf567f80a8fd85055db1ab4c5295806f Added contributing guide 14 | # 1a0b36b3cdad1d2ee32457c102a8c0b7056fa863 Initial commit 15 | # 16 | module SeedRepo 17 | module LastCommit 18 | ID = "5937ac0a7beb003549fc5fd26fc247adbce4a52e" 19 | PARENT_ID = "570e7b2abdd848b95f2f578043fc23bd6f6fd24d" 20 | MESSAGE = "Add submodule from gitlab.com" 21 | AUTHOR_FULL_NAME = "Dmitriy Zaporozhets" 22 | FILES = [".gitmodules", "gitlab-grack"] 23 | FILES_COUNT = 2 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/diff_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Gitlab::Git::Diff do 4 | let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } 5 | 6 | before do 7 | @raw_diff_hash = { 8 | diff: 'Hello world', 9 | new_path: 'temp.rb', 10 | old_path: 'test.rb', 11 | a_mode: '100644', 12 | b_mode: '100644', 13 | new_file: false, 14 | renamed_file: true, 15 | deleted_file: false, 16 | } 17 | 18 | @grit_diff = double('Grit::Diff', @raw_diff_hash) 19 | end 20 | 21 | describe :new do 22 | context 'init from grit' do 23 | before do 24 | @diff = Gitlab::Git::Diff.new(@raw_diff_hash) 25 | end 26 | 27 | it { @diff.to_hash.should == @raw_diff_hash } 28 | end 29 | 30 | context 'init from hash' do 31 | before do 32 | @diff = Gitlab::Git::Diff.new(@grit_diff) 33 | end 34 | 35 | it { @diff.to_hash.should == @raw_diff_hash } 36 | end 37 | end 38 | 39 | describe :between do 40 | let(:diffs) { Gitlab::Git::Diff.between(repository, 'feature', 'master') } 41 | subject { diffs } 42 | 43 | it { should be_kind_of Array } 44 | its(:size) { should eq(1) } 45 | 46 | context :diff do 47 | subject { diffs.first } 48 | 49 | it { should be_kind_of Gitlab::Git::Diff } 50 | its(:new_path) { should == 'files/ruby/feature.rb' } 51 | its(:diff) { should include '+class Feature' } 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/log_parser_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Gitlab::Git::LogParser do 4 | 5 | describe "#self.parse_log" do 6 | context "log_from_git is a valid log" do 7 | it "returns the correct log" do 8 | fake_log = "Karlo Soriano 9 | m@example.com 10 | 2013-05-09 11 | 12 | 14 files changed, 471 insertions(+) 13 | Dmitriy Zaporozhets 14 | m@example.com 15 | 2013-05-08 16 | 17 | 1 file changed, 6 insertions(+), 1 deletion(-) 18 | Dmitriy Zaporozhets 19 | m@example.com 20 | 2013-05-08 21 | 22 | 6 files changed, 19 insertions(+), 3 deletions(-) 23 | Dmitriy Zaporozhets 24 | m@example.com 25 | 2013-05-08 26 | 27 | 3 files changed, 29 insertions(+), 3 deletions(-) 28 | Dmitriy Zaporozhets 29 | m@example.com 30 | 2013-05-08 31 | 32 | 3 files changed, 3 deletions(-)"; 33 | 34 | lp = Gitlab::Git::LogParser.parse_log(fake_log) 35 | lp.should eq([ 36 | {author_email: 'm@example.com', author_name: "Karlo Soriano", date: "2013-05-09", additions: 471}, 37 | {author_email: 'm@example.com', author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 6, deletions: 1}, 38 | {author_email: 'm@example.com', author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 19, deletions: 3}, 39 | {author_email: 'm@example.com', author_name: "Dmitriy Zaporozhets", date: "2013-05-08", additions: 29, deletions: 3}, 40 | {author_email: 'm@example.com', author_name: "Dmitriy Zaporozhets", date: "2013-05-08", deletions: 3}]) 41 | end 42 | end 43 | end 44 | 45 | end 46 | -------------------------------------------------------------------------------- /lib/gitlab_git/compare.rb: -------------------------------------------------------------------------------- 1 | module Gitlab 2 | module Git 3 | class Compare 4 | attr_reader :commits, :diffs, :same, :timeout, :head, :base 5 | 6 | def initialize(repository, base, head) 7 | @commits, @diffs = [], [] 8 | @same = false 9 | @repository = repository 10 | @timeout = false 11 | 12 | return unless base && head 13 | 14 | @base = Gitlab::Git::Commit.find(repository, base.try(:strip)) 15 | @head = Gitlab::Git::Commit.find(repository, head.try(:strip)) 16 | 17 | return unless @base && @head 18 | 19 | if @base.id == @head.id 20 | @same = true 21 | return 22 | end 23 | 24 | @commits = Gitlab::Git::Commit.between(repository, @base.id, @head.id) 25 | end 26 | 27 | def diffs(paths = nil) 28 | unless @head && @base 29 | return [] 30 | end 31 | 32 | # Try to collect diff only if diffs is empty 33 | # Otherwise return cached version 34 | if @diffs.empty? && @timeout == false 35 | begin 36 | @diffs = Gitlab::Git::Diff.between(@repository, @head.id, @base.id, *paths) 37 | rescue Gitlab::Git::Diff::TimeoutError => ex 38 | @diffs = [] 39 | @timeout = true 40 | end 41 | end 42 | 43 | @diffs 44 | end 45 | 46 | # Check if diff is empty because it is actually empty 47 | # and not because its impossible to get it 48 | def empty_diff? 49 | diffs.empty? && timeout == false 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/support/commit.rb: -------------------------------------------------------------------------------- 1 | # Seed repo: 2 | # 5937ac0a7beb003549fc5fd26fc247adbce4a52e Add submodule from gitlab.com 3 | # 570e7b2abdd848b95f2f578043fc23bd6f6fd24d Change some files 4 | # 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 More submodules 5 | # d14d6c0abdd253381df51a723d58691b2ee1ab08 Remove ds_store files 6 | # c1acaa58bbcbc3eafe538cb8274ba387047b69f8 Ignore DS files 7 | # ae73cb07c9eeaf35924a10f713b364d32b2dd34f Binary file added 8 | # 874797c3a73b60d2187ed6e2fcabd289ff75171e Ruby files modified 9 | # 2f63565e7aac07bcdadb654e253078b727143ec4 Modified image 10 | # 33f3729a45c02fc67d00adb1b8bca394b0e761d9 Image added 11 | # 913c66a37b4a45b9769037c55c2d238bd0942d2e Files, encoding and much more 12 | # cfe32cf61b73a0d5e9f13e774abde7ff789b1660 Add submodule 13 | # 6d394385cf567f80a8fd85055db1ab4c5295806f Added contributing guide 14 | # 1a0b36b3cdad1d2ee32457c102a8c0b7056fa863 Initial commit 15 | # 16 | module SeedRepo 17 | module Commit 18 | ID = "570e7b2abdd848b95f2f578043fc23bd6f6fd24d" 19 | PARENT_ID = "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9" 20 | MESSAGE = "Change some files" 21 | AUTHOR_FULL_NAME = "Dmitriy Zaporozhets" 22 | 23 | FILES = ["files/ruby/popen.rb", "files/ruby/regex.rb"] 24 | FILES_COUNT = 2 25 | 26 | C_FILE_PATH = "files/ruby" 27 | C_FILES = ["popen.rb", "regex.rb", "version_info.rb"] 28 | 29 | BLOB_FILE = %{%h3= @key.title\n%hr\n%pre= @key.key\n.actions\n = link_to 'Remove', @key, :confirm => 'Are you sure?', :method => :delete, :class => \"btn danger delete-key\"\n\n\n} 30 | BLOB_FILE_PATH = "app/views/keys/show.html.haml" 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/gitlab_git/stats.rb: -------------------------------------------------------------------------------- 1 | module Gitlab 2 | module Git 3 | class Stats 4 | attr_accessor :repo, :ref 5 | 6 | def initialize repo, ref 7 | @repo, @ref = repo, ref 8 | end 9 | 10 | def authors 11 | @authors ||= collect_authors 12 | end 13 | 14 | def commits_count 15 | @commits_count ||= repo.commit_count(ref) 16 | end 17 | 18 | def files_count 19 | args = [ref, '-r', '--name-only' ] 20 | repo.git.native(:ls_tree, {}, args).split("\n").count 21 | end 22 | 23 | def authors_count 24 | authors.size 25 | end 26 | 27 | def graph 28 | @graph ||= build_graph 29 | end 30 | 31 | protected 32 | 33 | def collect_authors 34 | shortlog = repo.git.shortlog({e: true, s: true }, ref) 35 | 36 | authors = [] 37 | 38 | lines = shortlog.split("\n") 39 | 40 | lines.each do |line| 41 | data = line.split("\t") 42 | commits = data.first 43 | author = Grit::Actor.from_string(data.last) 44 | 45 | authors << OpenStruct.new( 46 | name: author.name, 47 | email: author.email, 48 | commits: commits.to_i 49 | ) 50 | end 51 | 52 | authors.sort_by(&:commits).reverse 53 | end 54 | 55 | def build_graph(n = 4) 56 | from, to = (Date.today.prev_day(n*7)), Date.today 57 | args = ['--all', "--since=#{from.to_s}", '--format=%ad' ] 58 | rev_list = repo.git.native(:rev_list, {}, args).split("\n") 59 | 60 | commits_dates = rev_list.values_at(* rev_list.each_index.select {|i| i.odd?}) 61 | commits_dates = commits_dates.map { |date_str| Time.parse(date_str).to_date.to_s } 62 | 63 | commits_per_day = from.upto(to).map do |day| 64 | commits_dates.count(day.to_date.to_s) 65 | end 66 | 67 | OpenStruct.new( 68 | labels: from.upto(to).map { |day| day.strftime('%b %d') }, 69 | commits: commits_per_day, 70 | weeks: n 71 | ) 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /spec/blob_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | require "spec_helper" 4 | 5 | describe Gitlab::Git::Blob do 6 | let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } 7 | 8 | describe :find do 9 | context 'file in subdir' do 10 | let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb") } 11 | 12 | it { blob.id.should == SeedRepo::RubyBlob::ID } 13 | it { blob.name.should == SeedRepo::RubyBlob::NAME } 14 | it { blob.path.should == "files/ruby/popen.rb" } 15 | it { blob.commit_id.should == SeedRepo::Commit::ID } 16 | it { blob.data[0..10].should == SeedRepo::RubyBlob::CONTENT[0..10] } 17 | it { blob.size.should == 669 } 18 | end 19 | 20 | context 'file in root' do 21 | let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, ".gitignore") } 22 | 23 | it { blob.id.should == "dfaa3f97ca337e20154a98ac9d0be76ddd1fcc82" } 24 | it { blob.name.should == ".gitignore" } 25 | it { blob.path.should == ".gitignore" } 26 | it { blob.commit_id.should == SeedRepo::Commit::ID } 27 | it { blob.data[0..10].should == "*.rbc\n*.sas" } 28 | it { blob.size.should == 241 } 29 | end 30 | 31 | context 'non-exist file' do 32 | let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "missing.rb") } 33 | 34 | it { blob.should be_nil } 35 | end 36 | end 37 | 38 | describe :raw do 39 | let(:raw_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::RubyBlob::ID) } 40 | it { raw_blob.id.should == SeedRepo::RubyBlob::ID } 41 | it { raw_blob.data[0..10].should == "require \'fi" } 42 | it { raw_blob.size.should == 669 } 43 | end 44 | 45 | describe 'encoding' do 46 | context 'file with russian text' do 47 | let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "encoding/russian.rb") } 48 | 49 | it { blob.name.should == "russian.rb" } 50 | it { blob.data.lines.first.should == "Хороший файл" } 51 | it { blob.size.should == 23 } 52 | end 53 | 54 | context 'file with Chinese text' do 55 | let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "encoding/テスト.txt") } 56 | 57 | it { blob.name.should == "テスト.txt" } 58 | it { blob.data.should include("これはテスト") } 59 | it { blob.size.should == 340 } 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/gitlab_git/diff.rb: -------------------------------------------------------------------------------- 1 | # Gitlab::Git::Diff is a wrapper around native Grit::Diff object 2 | # We dont want to use grit objects inside app/ 3 | # It helps us easily migrate to rugged in future 4 | module Gitlab 5 | module Git 6 | class Diff 7 | class TimeoutError < StandardError; end 8 | 9 | attr_accessor :raw_diff 10 | 11 | # Diff properties 12 | attr_accessor :old_path, :new_path, :a_mode, :b_mode, :diff 13 | 14 | # Stats properties 15 | attr_accessor :new_file, :renamed_file, :deleted_file 16 | 17 | class << self 18 | def between(repo, head, base, *paths) 19 | # Only show what is new in the source branch compared to the target branch, not the other way around. 20 | # The linex below with merge_base is equivalent to diff with three dots (git diff branch1...branch2) 21 | # From the git documentation: "git diff A...B" is equivalent to "git diff $(git-merge-base A B) B" 22 | common_commit = repo.merge_base_commit(head, base) 23 | 24 | repo.diff(common_commit, head, *paths).map do |diff| 25 | Gitlab::Git::Diff.new(diff) 26 | end 27 | rescue Grit::Git::GitTimeout 28 | raise TimeoutError.new("Diff.between exited with timeout") 29 | end 30 | end 31 | 32 | def initialize(raw_diff) 33 | raise "Nil as raw diff passed" unless raw_diff 34 | 35 | if raw_diff.is_a?(Hash) 36 | init_from_hash(raw_diff) 37 | else 38 | init_from_grit(raw_diff) 39 | end 40 | end 41 | 42 | def serialize_keys 43 | @serialize_keys ||= %w(diff new_path old_path a_mode b_mode new_file renamed_file deleted_file).map(&:to_sym) 44 | end 45 | 46 | def to_hash 47 | hash = {} 48 | 49 | keys = serialize_keys 50 | 51 | keys.each do |key| 52 | hash[key] = send(key) 53 | end 54 | 55 | hash 56 | end 57 | 58 | private 59 | 60 | def init_from_grit(grit) 61 | @raw_diff = grit 62 | 63 | serialize_keys.each do |key| 64 | send(:"#{key}=", grit.send(key)) 65 | end 66 | end 67 | 68 | def init_from_hash(hash) 69 | raw_diff = hash.symbolize_keys 70 | 71 | serialize_keys.each do |key| 72 | send(:"#{key}=", raw_diff[key.to_sym]) 73 | end 74 | end 75 | end 76 | end 77 | end 78 | 79 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | v 6.1.0 2 | - Remove limit parameter from Compare.new method 3 | - Prevent 'undefined method' exception when pass wrong ref to Compare 4 | 5 | v 6.0.1 6 | - Use tmp file for archive repository unless file is ready 7 | 8 | v 6.0.0 9 | - Add Repository#commit_count method 10 | 11 | v 5.9.0 12 | - Performance improvements for Commit#find method 13 | 14 | v 5.8.0 15 | - Less strict dependency rules 16 | - Updated gems 17 | - Use the new shell command style 18 | 19 | v 5.7.1 20 | - Fixed branch discover if master does not exist 21 | 22 | v 5.7.0 23 | - Fixed issue with bz2 archive not being created (Jason Hollingsworth) 24 | - Fixed branch discover if HEAD has not been updated to one of the existing branches (Jason Hollingsworth) 25 | 26 | v 5.6.0 27 | - Rename Tree#contribution? to Tree#contributing? 28 | - Use new light seed repo 29 | 30 | v 5.5.0 31 | - Add Tree#contribution? method 32 | 33 | v 5.4.0 34 | - Get rid of BROKEN_DIFF constant 35 | - Diff.between raises exception in case of timeout now 36 | - Add tests to Compare 37 | - Cache diff result for Compare 38 | 39 | v 5.3.0 40 | - Add Repository#submodules method 41 | 42 | v 5.2.0 43 | - Add EncodingHelper. Encode blob data to utf8 44 | 45 | v 5.1.0 46 | - Added rugged dependency 47 | - Git::Tag and Git::Branch objects 48 | - Branch, Tag, Tree uses rugged 49 | - Blob use rugged 50 | 51 | v 5.0.0 52 | - Use new version of linguist 53 | 54 | v 4.1.0 55 | - Allow to specify diff paths 56 | - Remove pygments dependency 57 | 58 | v 4.0.0 59 | - use activesupport v4 60 | - Add support for raw blob search 61 | - Add support for zip archives 62 | 63 | v 3.1.0 64 | - use gitlab dependencies (linguist, pygments) 65 | 66 | v 3.0.0 67 | - Refactor Git::Tree 68 | - Refactor Git::Blob 69 | - Refactored Git::Repository 70 | 71 | v 2.3.0 72 | - Better ref detection 73 | - Fixed empty repo bug 74 | 75 | v 2.2.0 76 | - Fixed Compare showing wrong diff 77 | 78 | v 1.4.0 79 | - Update gitlab_grit dependency to 2.6.0 80 | 81 | v 1.3.0 82 | - Added GitStats and LogParser classes 83 | 84 | v 1.2.1 85 | - fixed commit stats 86 | 87 | v 1.2.0 88 | - Use gitlab-grit instead of grit 89 | - removed grit_ext dependency also 90 | 91 | v 1.1.0 92 | - new method Repository#search_files added 93 | - new class BlobSnippet added (for search results) 94 | 95 | v 1.0.6 96 | - new method Tree#submodules added 97 | -------------------------------------------------------------------------------- /lib/gitlab_git/blob.rb: -------------------------------------------------------------------------------- 1 | module Gitlab 2 | module Git 3 | class Blob 4 | include Linguist::BlobHelper 5 | include EncodingHelper 6 | 7 | attr_accessor :name, :path, :size, :data, :mode, :id, :commit_id 8 | 9 | class << self 10 | def find(repository, sha, path) 11 | commit = repository.lookup(sha) 12 | root_tree = commit.tree 13 | 14 | blob_entry = find_entry_by_path(repository, root_tree.oid, path) 15 | 16 | return nil unless blob_entry 17 | 18 | blob = repository.lookup(blob_entry[:oid]) 19 | 20 | if blob 21 | Blob.new( 22 | id: blob.oid, 23 | name: blob_entry[:name], 24 | size: blob.size, 25 | data: blob.content, 26 | mode: blob_entry[:mode], 27 | path: path, 28 | commit_id: sha, 29 | ) 30 | end 31 | end 32 | 33 | def raw(repository, sha) 34 | blob = repository.lookup(sha) 35 | 36 | Blob.new( 37 | id: blob.oid, 38 | size: blob.size, 39 | data: blob.content, 40 | ) 41 | end 42 | 43 | # Recursive search of blob id by path 44 | # 45 | # Ex. 46 | # blog/ # oid: 1a 47 | # app/ # oid: 2a 48 | # models/ # oid: 3a 49 | # file.rb # oid: 4a 50 | # 51 | # 52 | # Blob.find_entry_by_path(repo, '1a', 'app/file.rb') # => '4a' 53 | # 54 | def find_entry_by_path(repository, root_id, path) 55 | root_tree = repository.lookup(root_id) 56 | path_arr = path.split('/') 57 | 58 | entry = root_tree.find do |entry| 59 | entry[:name] == path_arr[0] 60 | end 61 | 62 | return nil unless entry 63 | 64 | if path_arr.size > 1 65 | return nil unless entry[:type] == :tree 66 | else 67 | return nil unless entry[:type] == :blob 68 | end 69 | 70 | if path_arr.size > 1 71 | path_arr.shift 72 | find_entry_by_path(repository, entry[:oid], path_arr.join('/')) 73 | else 74 | entry 75 | end 76 | end 77 | end 78 | 79 | def initialize(options) 80 | %w(id name path size data mode commit_id).each do |key| 81 | self.send("#{key}=", options[key.to_sym]) 82 | end 83 | end 84 | 85 | def empty? 86 | !data || data == '' 87 | end 88 | 89 | def data 90 | encode! @data 91 | end 92 | 93 | def name 94 | encode! @name 95 | end 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /lib/gitlab_git/tree.rb: -------------------------------------------------------------------------------- 1 | module Gitlab 2 | module Git 3 | class Tree 4 | attr_accessor :id, :root_id, :name, :path, :type, 5 | :mode, :commit_id, :submodule_url 6 | 7 | class << self 8 | # Get list of tree objects 9 | # for repository based on commit sha and path 10 | # Uses rugged for raw objects 11 | def where(repository, sha, path = nil) 12 | path = nil if path == '' || path == '/' 13 | 14 | commit = repository.lookup(sha) 15 | root_tree = commit.tree 16 | 17 | tree = if path 18 | id = Tree.find_id_by_path(repository, root_tree.oid, path) 19 | if id 20 | repository.lookup(id) 21 | else 22 | [] 23 | end 24 | else 25 | root_tree 26 | end 27 | 28 | tree.map do |entry| 29 | Tree.new( 30 | id: entry[:oid], 31 | root_id: root_tree.oid, 32 | name: entry[:name], 33 | type: entry[:type] || :submodule, 34 | mode: entry[:filemode], 35 | path: path ? File.join(path, entry[:name]) : entry[:name], 36 | commit_id: sha, 37 | ) 38 | end 39 | end 40 | 41 | # Recursive search of tree id for path 42 | # 43 | # Ex. 44 | # blog/ # oid: 1a 45 | # app/ # oid: 2a 46 | # models/ # oid: 3a 47 | # views/ # oid: 4a 48 | # 49 | # 50 | # Tree.find_id_by_path(repo, '1a', 'app/models') # => '3a' 51 | # 52 | def find_id_by_path(repository, root_id, path) 53 | root_tree = repository.lookup(root_id) 54 | path_arr = path.split('/') 55 | 56 | entry = root_tree.find do |entry| 57 | entry[:name] == path_arr[0] && entry[:type] == :tree 58 | end 59 | 60 | return nil unless entry 61 | 62 | if path_arr.size > 1 63 | path_arr.shift 64 | find_id_by_path(repository, entry[:oid], path_arr.join('/')) 65 | else 66 | entry[:oid] 67 | end 68 | end 69 | end 70 | 71 | def initialize(options) 72 | %w(id root_id name path type mode commit_id).each do |key| 73 | self.send("#{key}=", options[key.to_sym]) 74 | end 75 | end 76 | 77 | def dir? 78 | type == :tree 79 | end 80 | 81 | def file? 82 | type == :blob 83 | end 84 | 85 | def submodule? 86 | type == :submodule 87 | end 88 | 89 | def readme? 90 | name =~ /^readme/i 91 | end 92 | 93 | def contributing? 94 | name =~ /^contributing/i 95 | end 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | gitlab_git (6.1.0) 5 | activesupport (~> 4.0) 6 | charlock_holmes (~> 0.6) 7 | gitlab-grit (~> 2.6) 8 | gitlab-linguist (~> 3.0) 9 | rugged (~> 0.19.0) 10 | 11 | GEM 12 | remote: http://rubygems.org/ 13 | specs: 14 | activesupport (4.1.1) 15 | i18n (~> 0.6, >= 0.6.9) 16 | json (~> 1.7, >= 1.7.7) 17 | minitest (~> 5.1) 18 | thread_safe (~> 0.1) 19 | tzinfo (~> 1.1) 20 | addressable (2.3.4) 21 | charlock_holmes (0.6.9.4) 22 | coderay (1.0.9) 23 | colorize (0.5.8) 24 | coveralls (0.6.7) 25 | colorize 26 | multi_json (~> 1.3) 27 | rest-client 28 | simplecov (>= 0.7) 29 | thor 30 | crack (0.3.2) 31 | diff-lcs (1.2.5) 32 | escape_utils (0.2.4) 33 | ffi (1.8.1) 34 | formatador (0.2.4) 35 | gitlab-grit (2.7.0) 36 | charlock_holmes (~> 0.6) 37 | diff-lcs (~> 1.1) 38 | mime-types (~> 1.15) 39 | posix-spawn (~> 0.3) 40 | gitlab-linguist (3.0.0) 41 | charlock_holmes (~> 0.6.6) 42 | escape_utils (~> 0.2.4) 43 | mime-types (~> 1.19) 44 | guard (1.8.0) 45 | formatador (>= 0.2.4) 46 | listen (>= 1.0.0) 47 | lumberjack (>= 1.0.2) 48 | pry (>= 0.9.10) 49 | thor (>= 0.14.6) 50 | guard-rspec (2.5.4) 51 | guard (>= 1.1) 52 | rspec (~> 2.11) 53 | i18n (0.6.11) 54 | json (1.8.1) 55 | listen (1.0.2) 56 | rb-fsevent (>= 0.9.3) 57 | rb-inotify (>= 0.9) 58 | rb-kqueue (>= 0.2) 59 | lumberjack (1.0.3) 60 | method_source (0.8.1) 61 | mime-types (1.25.1) 62 | minitest (5.3.5) 63 | multi_json (1.7.2) 64 | posix-spawn (0.3.8) 65 | pry (0.9.12.1) 66 | coderay (~> 1.0.5) 67 | method_source (~> 0.8) 68 | slop (~> 3.4) 69 | rb-fsevent (0.9.3) 70 | rb-inotify (0.9.0) 71 | ffi (>= 0.5.0) 72 | rb-kqueue (0.2.0) 73 | ffi (>= 0.5.0) 74 | rest-client (1.6.7) 75 | mime-types (>= 1.16) 76 | rspec (2.13.0) 77 | rspec-core (~> 2.13.0) 78 | rspec-expectations (~> 2.13.0) 79 | rspec-mocks (~> 2.13.0) 80 | rspec-core (2.13.1) 81 | rspec-expectations (2.13.0) 82 | diff-lcs (>= 1.1.3, < 2.0) 83 | rspec-mocks (2.13.1) 84 | rugged (0.19.0) 85 | simplecov (0.7.1) 86 | multi_json (~> 1.0) 87 | simplecov-html (~> 0.7.1) 88 | simplecov-html (0.7.1) 89 | slop (3.4.4) 90 | thor (0.18.1) 91 | thread_safe (0.3.4) 92 | tzinfo (1.2.1) 93 | thread_safe (~> 0.1) 94 | webmock (1.11.0) 95 | addressable (>= 2.2.7) 96 | crack (>= 0.3.2) 97 | 98 | PLATFORMS 99 | ruby 100 | 101 | DEPENDENCIES 102 | coveralls 103 | gitlab_git! 104 | guard 105 | guard-rspec 106 | pry 107 | rspec 108 | simplecov 109 | webmock 110 | -------------------------------------------------------------------------------- /spec/tree_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Gitlab::Git::Tree do 4 | context :repo do 5 | let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } 6 | let(:tree) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID) } 7 | 8 | it { tree.should be_kind_of Array } 9 | it { tree.empty?.should be_false } 10 | it { tree.select(&:dir?).size.should == 2 } 11 | it { tree.select(&:file?).size.should == 10 } 12 | it { tree.select(&:submodule?).size.should == 2 } 13 | 14 | describe :dir do 15 | let(:dir) { tree.select(&:dir?).first } 16 | 17 | it { dir.should be_kind_of Gitlab::Git::Tree } 18 | it { dir.id.should == '3c122d2b7830eca25235131070602575cf8b41a1' } 19 | it { dir.commit_id.should == SeedRepo::Commit::ID } 20 | it { dir.name.should == 'encoding' } 21 | it { dir.path.should == 'encoding' } 22 | 23 | context :subdir do 24 | let(:subdir) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID, 'files').first } 25 | 26 | it { subdir.should be_kind_of Gitlab::Git::Tree } 27 | it { subdir.id.should == 'a1e8f8d745cc87e3a9248358d9352bb7f9a0aeba' } 28 | it { subdir.commit_id.should == SeedRepo::Commit::ID } 29 | it { subdir.name.should == 'html' } 30 | it { subdir.path.should == 'files/html' } 31 | end 32 | 33 | context :subdir_file do 34 | let(:subdir_file) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID, 'files/ruby').first } 35 | 36 | it { subdir_file.should be_kind_of Gitlab::Git::Tree } 37 | it { subdir_file.id.should == '7e3e39ebb9b2bf433b4ad17313770fbe4051649c' } 38 | it { subdir_file.commit_id.should == SeedRepo::Commit::ID } 39 | it { subdir_file.name.should == 'popen.rb' } 40 | it { subdir_file.path.should == 'files/ruby/popen.rb' } 41 | end 42 | end 43 | 44 | describe :file do 45 | let(:file) { tree.select(&:file?).first } 46 | 47 | it { file.should be_kind_of Gitlab::Git::Tree } 48 | it { file.id.should == 'dfaa3f97ca337e20154a98ac9d0be76ddd1fcc82' } 49 | it { file.commit_id.should == SeedRepo::Commit::ID } 50 | it { file.name.should == '.gitignore' } 51 | end 52 | 53 | describe :readme do 54 | let(:file) { tree.select(&:readme?).first } 55 | 56 | it { file.should be_kind_of Gitlab::Git::Tree } 57 | it { file.name.should == 'README.md' } 58 | end 59 | 60 | describe :contributing do 61 | let(:file) { tree.select(&:contributing?).first } 62 | 63 | it { file.should be_kind_of Gitlab::Git::Tree } 64 | it { file.name.should == 'CONTRIBUTING.md' } 65 | end 66 | 67 | describe :submodule do 68 | let(:submodule) { tree.select(&:submodule?).first } 69 | 70 | it { submodule.should be_kind_of Gitlab::Git::Tree } 71 | it { submodule.id.should == '79bceae69cb5750d6567b223597999bfa91cb3b9' } 72 | it { submodule.commit_id.should == '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' } 73 | it { submodule.name.should == 'gitlab-shell' } 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /spec/repository_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Gitlab::Git::Repository do 4 | let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } 5 | 6 | describe "Respond to" do 7 | subject { repository } 8 | 9 | it { should respond_to(:raw) } 10 | it { should respond_to(:grit) } 11 | it { should respond_to(:root_ref) } 12 | it { should respond_to(:tags) } 13 | end 14 | 15 | 16 | describe "#discover_default_branch" do 17 | let(:master) { 'master' } 18 | let(:feature) { 'feature' } 19 | let(:feature2) { 'feature2' } 20 | 21 | it "returns 'master' when master exists" do 22 | repository.should_receive(:branch_names).at_least(:once).and_return([feature, master]) 23 | repository.discover_default_branch.should == 'master' 24 | end 25 | 26 | it "returns non-master when master exists but default branch is set to something else" do 27 | File.write(File.join(repository.path, 'HEAD'), 'ref: refs/heads/feature') 28 | repository.should_receive(:branch_names).at_least(:once).and_return([feature, master]) 29 | repository.discover_default_branch.should == 'feature' 30 | File.write(File.join(repository.path, 'HEAD'), 'ref: refs/heads/master') 31 | end 32 | 33 | it "returns a non-master branch when only one exists" do 34 | repository.should_receive(:branch_names).at_least(:once).and_return([feature]) 35 | repository.discover_default_branch.should == 'feature' 36 | end 37 | 38 | it "returns a non-master branch when more than one exists and master does not" do 39 | repository.should_receive(:branch_names).at_least(:once).and_return([feature, feature2]) 40 | repository.discover_default_branch.should == 'feature' 41 | end 42 | 43 | it "returns nil when no branch exists" do 44 | repository.should_receive(:branch_names).at_least(:once).and_return([]) 45 | repository.discover_default_branch.should be_nil 46 | end 47 | end 48 | 49 | describe :branch_names do 50 | subject { repository.branch_names } 51 | 52 | it { should have(SeedRepo::Repo::BRANCHES.size).elements } 53 | it { should include("master") } 54 | it { should_not include("branch-from-space") } 55 | end 56 | 57 | describe :tag_names do 58 | subject { repository.tag_names } 59 | 60 | it { should be_kind_of Array } 61 | it { should have(SeedRepo::Repo::TAGS.size).elements } 62 | its(:last) { should == "v1.1.0" } 63 | it { should include("v1.0.0") } 64 | it { should_not include("v5.0.0") } 65 | end 66 | 67 | shared_examples 'archive check' do |extenstion| 68 | it { archive.should match(/tmp\/testme.git\/testme-5937ac0a/) } 69 | it { archive.should end_with extenstion } 70 | it { File.exists?(archive).should be_true } 71 | it { File.size?(archive).should_not be_nil } 72 | end 73 | 74 | describe :archive do 75 | let(:archive) { repository.archive_repo('master', '/tmp') } 76 | after { FileUtils.rm_r(archive) } 77 | 78 | it_should_behave_like 'archive check', '.tar.gz' 79 | end 80 | 81 | describe :archive_zip do 82 | let(:archive) { repository.archive_repo('master', '/tmp', 'zip') } 83 | after { FileUtils.rm_r(archive) } 84 | 85 | it_should_behave_like 'archive check', '.zip' 86 | end 87 | 88 | describe :archive_bz2 do 89 | let(:archive) { repository.archive_repo('master', '/tmp', 'tbz2') } 90 | after { FileUtils.rm_r(archive) } 91 | 92 | it_should_behave_like 'archive check', '.tar.bz2' 93 | end 94 | 95 | describe :archive_fallback do 96 | let(:archive) { repository.archive_repo('master', '/tmp', 'madeup') } 97 | after { FileUtils.rm_r(archive) } 98 | 99 | it_should_behave_like 'archive check', '.tar.gz' 100 | end 101 | 102 | describe :size do 103 | subject { repository.size } 104 | 105 | it { should < 2 } 106 | end 107 | 108 | describe :has_commits? do 109 | it { repository.has_commits?.should be_true } 110 | end 111 | 112 | describe :empty? do 113 | it { repository.empty?.should be_false } 114 | end 115 | 116 | describe :heads do 117 | let(:heads) { repository.heads } 118 | subject { heads } 119 | 120 | it { should be_kind_of Array } 121 | its(:size) { should eq(3) } 122 | 123 | context :head do 124 | subject { heads.first } 125 | 126 | its(:name) { should == 'feature' } 127 | 128 | context :commit do 129 | subject { heads.first.commit } 130 | 131 | its(:id) { should == '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } 132 | end 133 | end 134 | end 135 | 136 | describe :ref_names do 137 | let(:ref_names) { repository.ref_names } 138 | subject { ref_names } 139 | 140 | it { should be_kind_of Array } 141 | its(:first) { should == 'feature' } 142 | its(:last) { should == 'v1.1.0' } 143 | end 144 | 145 | describe :search_files do 146 | let(:results) { repository.search_files('rails', 'master') } 147 | subject { results } 148 | 149 | it { should be_kind_of Array } 150 | its(:first) { should be_kind_of Gitlab::Git::BlobSnippet } 151 | 152 | context 'blob result' do 153 | subject { results.first } 154 | 155 | its(:ref) { should == 'master' } 156 | its(:filename) { should == 'CHANGELOG' } 157 | its(:startline) { should == 35 } 158 | its(:data) { should include "Ability to filter by multiple labels" } 159 | end 160 | end 161 | 162 | context :submodules do 163 | let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } 164 | let(:submodules) { repository.submodules(SeedRepo::Commit::ID) } 165 | 166 | it { submodules.should be_kind_of Hash } 167 | it { submodules.empty?.should be_false } 168 | 169 | describe :submodule do 170 | let(:submodule) { submodules.first } 171 | 172 | it 'should have valid data' do 173 | submodule.should == [ 174 | "six", { 175 | "id"=>"409f37c4f05865e4fb208c771485f211a22c4c2d", 176 | "path"=>"six", 177 | "url"=>"git://github.com/randx/six.git" 178 | } 179 | ] 180 | end 181 | end 182 | end 183 | 184 | describe :commit_count do 185 | it { repository.commit_count("master").should == 13 } 186 | it { repository.commit_count("feature").should == 9 } 187 | end 188 | end 189 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## GitLab Git 2 | 3 | GitLab wrapper around git objects. 4 | 5 | - - - 6 | 7 | [![build status](https://ci.gitlab.org/projects/6/status.png?ref=master)](https://ci.gitlab.org/projects/6?ref=master) 8 | [![Build Status](https://travis-ci.org/gitlabhq/gitlab_git.svg?branch=master)](https://travis-ci.org/gitlabhq/gitlab_git) 9 | [![Gem Version](https://badge.fury.io/rb/gitlab_git.svg)](http://badge.fury.io/rb/gitlab_git) 10 | [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlab_git.png)](https://codeclimate.com/github/gitlabhq/gitlab_git) 11 | [![Coverage Status](https://coveralls.io/repos/gitlabhq/gitlab_git/badge.png?branch=master)](https://coveralls.io/r/gitlabhq/gitlab_git) 12 | 13 | ### Move from Grit to Rugged 14 | 15 | GitLab Git used grit as main library in past. 16 | Right now GitLab Git uses 2 libraries: rugged and grit. 17 | We want to increase usage of rugged and reduce usage of grit in this library. 18 | If you can help us with that - please send Merge Request. 19 | 20 | 21 | ### How to use: 22 | 23 | #### Repository 24 | 25 | # Init repo with full path 26 | repo = Gitlab::Git::Repository.new('/home/git/repositories/gitlab/gitlab-ci.git') 27 | 28 | repo.path 29 | # "/home/git/repositories/gitlab/gitlab-ci.git" 30 | 31 | repo.name 32 | # "gitlab-ci.git" 33 | 34 | # Get branches and tags 35 | repo.branches 36 | repo.tags 37 | 38 | # Get branch or tag names 39 | repo.branch_names 40 | repo.tag_names 41 | 42 | # Archive repo to `/tmp` dir 43 | repo.archive_repo('master', '/tmp') 44 | 45 | # Bare repo size in MB. 46 | repo.size 47 | # 10.43 48 | 49 | # Search for code 50 | repo.search_files('rspec', 'master') 51 | # [ , ] 52 | 53 | # Access to grit repo object 54 | repo.grit 55 | 56 | #### Tree 57 | 58 | # Tree objects for root dir 59 | tree = Gitlab::Git::Tree.where(repo, '893ade32') 60 | 61 | # Tree objects for sub dir 62 | tree = Gitlab::Git::Tree.where(repo, '893ade32', 'app/models/') 63 | 64 | # [ 65 | # # 66 | # # 67 | # ] 68 | 69 | dir = tree.first 70 | dir.name # lib 71 | dir.type # :tree 72 | dir.dir? # true 73 | dir.file? # false 74 | 75 | file = tree.last 76 | file.name # sample.rb 77 | file.type # :blob 78 | file.dir? # false 79 | file.file? # true 80 | 81 | # Select only files for tree 82 | tree.select(&:file?) 83 | 84 | # Find readme 85 | tree.find(&:readme?) 86 | 87 | #### Blob 88 | 89 | # Blob object for Commit sha 893ade32 90 | blob = Gitlab::Git::Blob.find(repo, '893ade32', 'Gemfile') 91 | 92 | # Attributes 93 | blob.id 94 | blob.name 95 | blob.size 96 | blob.data 97 | blob.mode 98 | blob.path 99 | blob.commit_id 100 | 101 | # Blob object with sha 8a3f8ddcf3536628c9670d41e67a785383eded1d 102 | raw_blob = Gitlab::Git::Blob.raw(repo, '8a3f8ddcf3536628c9670d41e67a785383eded1d') 103 | 104 | # Attributes for raw blobs are more limited 105 | raw_blob.id 106 | raw_blob.size 107 | raw_blob.data 108 | 109 | 110 | #### Commit 111 | 112 | ##### Picking 113 | 114 | # Get commits collection with pagination 115 | Gitlab::Git::Commit.where( 116 | repo: repo, 117 | ref: 'master', 118 | path: 'app/models', 119 | limit: 10, 120 | offset: 5, 121 | ) 122 | 123 | # Find single commit 124 | Gitlab::Git::Commit.find(repo, '29eda46b') 125 | Gitlab::Git::Commit.find(repo, 'v2.4.6') 126 | 127 | # Get last commit for HEAD 128 | commit = Gitlab::Git::Commit.last(repo) 129 | 130 | # Get last commit for specified file/directory 131 | Gitlab::Git::Commit.find_for_path(repo, '29eda46b', 'app/models') 132 | 133 | # Commits between branches 134 | Gitlab::Git::Commit.between(repo, 'dev', 'master') 135 | # [ , ] 136 | 137 | 138 | ##### Commit object 139 | 140 | # Commit id 141 | commit.id 142 | commit.sha 143 | # ba8812a2de5e5ea191da6930a8ee1965873286e3 144 | 145 | commit.short_id 146 | # ba8812a2de 147 | 148 | commit.message 149 | commit.safe_message 150 | # Fix bug 891 151 | 152 | commit.parent_id 153 | # ba8812a2de5e5ea191da6930a8ee1965873286e3 154 | 155 | commit.diffs 156 | # [ , ] 157 | 158 | commit.created_at 159 | commit.authored_date 160 | commit.committed_date 161 | # 2013-07-03 22:11:26 +0300 162 | 163 | commit.committer_name 164 | commit.author_name 165 | # John Smith 166 | 167 | commit.committer_email 168 | commit.author_email 169 | # jsmith@sample.com 170 | 171 | 172 | #### Diff object 173 | 174 | # From commit 175 | commit.diffs 176 | # [ , ] 177 | 178 | # Diff between several commits 179 | Gitlab::Git::Diff.between(repo, 'dev', 'master') 180 | # [ , ] 181 | 182 | #### Git blame 183 | 184 | # Git blame for file 185 | blame = Gitlab::Git::Blame.new(repo, 'master, 'app/models/project.rb') 186 | blame.each do |commit, lines| 187 | commit # 188 | lines # ['class Project', 'def initialize'] 189 | end 190 | 191 | 192 | #### Compare 193 | 194 | Allows you to get difference(commits, diffs) between two sha/branch/tag 195 | 196 | 197 | compare = Gitlab::Git::Compare.new(repo, 'v4.3.2', 'master') 198 | 199 | compare.commits 200 | # [ , ] 201 | 202 | compare.diffs 203 | # [ , ] 204 | -------------------------------------------------------------------------------- /lib/gitlab_git/commit.rb: -------------------------------------------------------------------------------- 1 | # Gitlab::Git::Commit is a wrapper around native Grit::Commit object 2 | # We dont want to use grit objects inside app/ 3 | # It helps us easily migrate to rugged in future 4 | module Gitlab 5 | module Git 6 | class Commit 7 | attr_accessor :raw_commit, :head, :refs 8 | 9 | SERIALIZE_KEYS = [ 10 | :id, :message, :parent_ids, 11 | :authored_date, :author_name, :author_email, 12 | :committed_date, :committer_name, :committer_email 13 | ] 14 | attr_accessor *SERIALIZE_KEYS 15 | 16 | class << self 17 | # Get commits collection 18 | # 19 | # Ex. 20 | # Commit.where( 21 | # repo: repo, 22 | # ref: 'master', 23 | # path: 'app/models', 24 | # limit: 10, 25 | # offset: 5, 26 | # ) 27 | # 28 | def where(options) 29 | repo = options.delete(:repo) 30 | raise 'Gitlab::Git::Repository is required' unless repo.respond_to?(:log) 31 | 32 | repo.log(options).map { |c| decorate(c) } 33 | end 34 | 35 | # Get single commit 36 | # 37 | # Ex. 38 | # Commit.find(repo, '29eda46b') 39 | # 40 | # Commit.find(repo, 'master') 41 | # 42 | def find(repo, commit_id = nil) 43 | commit = repo.log(ref: commit_id, limit: 1).first 44 | decorate(commit) if commit 45 | end 46 | 47 | # Get last commit for HEAD 48 | # 49 | # Ex. 50 | # Commit.last(repo) 51 | # 52 | def last(repo) 53 | find(repo, nil) 54 | end 55 | 56 | # Get last commit for specified path and ref 57 | # 58 | # Ex. 59 | # Commit.last_for_path(repo, '29eda46b', 'app/models') 60 | # 61 | # Commit.last_for_path(repo, 'master', 'Gemfile') 62 | # 63 | def last_for_path(repo, ref, path = nil) 64 | where( 65 | repo: repo, 66 | ref: ref, 67 | path: path, 68 | limit: 1 69 | ).first 70 | end 71 | 72 | # Get commits between two refs 73 | # 74 | # Ex. 75 | # Commit.between('29eda46b', 'master') 76 | # 77 | def between(repo, base, head) 78 | repo.commits_between(base, head).map do |commit| 79 | decorate(commit) 80 | end 81 | end 82 | 83 | # Delegate Repository#find_commits 84 | def find_all(repo, options = {}) 85 | repo.find_commits(options) 86 | end 87 | 88 | def decorate(commit, ref = nil) 89 | Gitlab::Git::Commit.new(commit, ref) 90 | end 91 | end 92 | 93 | def initialize(raw_commit, head = nil) 94 | raise "Nil as raw commit passed" unless raw_commit 95 | 96 | if raw_commit.is_a?(Hash) 97 | init_from_hash(raw_commit) 98 | else 99 | init_from_grit(raw_commit) 100 | end 101 | 102 | @head = head 103 | end 104 | 105 | def sha 106 | id 107 | end 108 | 109 | def short_id(length = 10) 110 | id.to_s[0..length] 111 | end 112 | 113 | def safe_message 114 | @safe_message ||= message 115 | end 116 | 117 | def created_at 118 | committed_date 119 | end 120 | 121 | # Was this commit committed by a different person than the original author? 122 | def different_committer? 123 | author_name != committer_name || author_email != committer_email 124 | end 125 | 126 | def parent_id 127 | parent_ids.first 128 | end 129 | 130 | # Shows the diff between the commit's parent and the commit. 131 | # 132 | # Cuts out the header and stats from #to_patch and returns only the diff. 133 | def to_diff 134 | # see Grit::Commit#show 135 | patch = to_patch 136 | 137 | # discard lines before the diff 138 | lines = patch.split("\n") 139 | while !lines.first.start_with?("diff --git") do 140 | lines.shift 141 | end 142 | lines.pop if lines.last =~ /^[\d.]+$/ # Git version 143 | lines.pop if lines.last == "-- " # end of diff 144 | lines.join("\n") 145 | end 146 | 147 | def has_zero_stats? 148 | stats.total.zero? 149 | rescue 150 | true 151 | end 152 | 153 | def no_commit_message 154 | "--no commit message" 155 | end 156 | 157 | def to_hash 158 | serialize_keys.map.with_object({}) do |key, hash| 159 | hash[key] = send(key) 160 | end 161 | end 162 | 163 | def date 164 | committed_date 165 | end 166 | 167 | def diffs 168 | raw_commit.diffs.map { |diff| Gitlab::Git::Diff.new(diff) } 169 | end 170 | 171 | def parents 172 | raw_commit.parents 173 | end 174 | 175 | def tree 176 | raw_commit.tree 177 | end 178 | 179 | def stats 180 | raw_commit.stats 181 | end 182 | 183 | def to_patch 184 | raw_commit.to_patch 185 | end 186 | 187 | # Get refs collection(Grit::Head or Grit::Remote or Grit::Tag) 188 | # 189 | # Ex. 190 | # commit.ref(repo) 191 | # 192 | def refs(repo) 193 | repo.refs_hash[id] 194 | end 195 | 196 | # Get ref names collection 197 | # 198 | # Ex. 199 | # commit.ref_names(repo) 200 | # 201 | def ref_names(repo) 202 | refs(repo).map(&:name) 203 | end 204 | 205 | private 206 | 207 | def init_from_grit(grit) 208 | @raw_commit = grit 209 | @id = grit.id 210 | @message = grit.message 211 | @authored_date = grit.authored_date 212 | @committed_date = grit.committed_date 213 | @author_name = grit.author.name 214 | @author_email = grit.author.email 215 | @committer_name = grit.committer.name 216 | @committer_email = grit.committer.email 217 | @parent_ids = grit.parents.map(&:id) 218 | end 219 | 220 | def init_from_hash(hash) 221 | raw_commit = hash.symbolize_keys 222 | 223 | serialize_keys.each do |key| 224 | send("#{key}=", raw_commit[key]) 225 | end 226 | end 227 | 228 | def serialize_keys 229 | SERIALIZE_KEYS 230 | end 231 | end 232 | end 233 | end 234 | -------------------------------------------------------------------------------- /spec/commit_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Gitlab::Git::Commit do 4 | let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) } 5 | let(:commit) { Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID) } 6 | 7 | describe "Commit info" do 8 | before do 9 | @committer = double( 10 | email: 'mike@smith.com', 11 | name: 'Mike Smith' 12 | ) 13 | 14 | @author = double( 15 | email: 'john@smith.com', 16 | name: 'John Smith' 17 | ) 18 | 19 | @tree = double 20 | 21 | @parents = [ double(id: "874797c3a73b60d2187ed6e2fcabd289ff75171e") ] 22 | 23 | @raw_commit = double( 24 | id: "bcf03b5de6abcf03b5de6c", 25 | author: @author, 26 | committer: @committer, 27 | committed_date: Date.today.prev_day, 28 | authored_date: Date.today.prev_day, 29 | tree: @tree, 30 | parents: @parents, 31 | message: 'Refactoring specs' 32 | ) 33 | 34 | @commit = Gitlab::Git::Commit.new(@raw_commit) 35 | end 36 | 37 | it { @commit.short_id.should == "bcf03b5de6a" } 38 | it { @commit.id.should == @raw_commit.id } 39 | it { @commit.sha.should == @raw_commit.id } 40 | it { @commit.safe_message.should == @raw_commit.message } 41 | it { @commit.created_at.should == @raw_commit.committed_date } 42 | it { @commit.date.should == @raw_commit.committed_date } 43 | it { @commit.author_email.should == @author.email } 44 | it { @commit.author_name.should == @author.name } 45 | it { @commit.committer_name.should == @committer.name } 46 | it { @commit.committer_email.should == @committer.email } 47 | it { @commit.different_committer?.should be_true } 48 | it { @commit.parents.should == @parents } 49 | it { @commit.parent_id.should == @parents.first.id } 50 | it { @commit.no_commit_message.should == "--no commit message" } 51 | it { @commit.tree.should == @tree } 52 | end 53 | 54 | context 'Class methods' do 55 | describe :find do 56 | it "should return first head commit if without params" do 57 | Gitlab::Git::Commit.last(repository).id.should == repository.raw.commits.first.id 58 | end 59 | 60 | it "should return valid commit" do 61 | Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID).should be_valid_commit 62 | end 63 | 64 | it "should return valid commit for tag" do 65 | Gitlab::Git::Commit.find(repository, 'v1.0.0').id.should == '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' 66 | end 67 | 68 | it "should return nil" do 69 | Gitlab::Git::Commit.find(repository, "+123_4532530XYZ").should be_nil 70 | end 71 | end 72 | 73 | describe :last_for_path do 74 | context 'no path' do 75 | subject { Gitlab::Git::Commit.last_for_path(repository, 'master') } 76 | 77 | its(:id) { should == SeedRepo::LastCommit::ID } 78 | end 79 | 80 | context 'path' do 81 | subject { Gitlab::Git::Commit.last_for_path(repository, 'master', 'files') } 82 | 83 | its(:id) { should == SeedRepo::Commit::ID } 84 | end 85 | 86 | context 'ref + path' do 87 | subject { Gitlab::Git::Commit.last_for_path(repository, SeedRepo::Commit::ID, 'encoding') } 88 | 89 | its(:id) { should == SeedRepo::BigCommit::ID } 90 | end 91 | end 92 | 93 | 94 | describe "where" do 95 | subject do 96 | commits = Gitlab::Git::Commit.where( 97 | repo: repository, 98 | ref: 'master', 99 | path: 'files', 100 | limit: 3, 101 | offset: 1 102 | ) 103 | 104 | commits.map { |c| c.id } 105 | end 106 | 107 | it { should have(3).elements } 108 | it { should include("874797c3a73b60d2187ed6e2fcabd289ff75171e") } 109 | it { should_not include(SeedRepo::Commit::ID) } 110 | end 111 | 112 | describe :between do 113 | subject do 114 | commits = Gitlab::Git::Commit.between(repository, SeedRepo::Commit::PARENT_ID, SeedRepo::Commit::ID) 115 | commits.map { |c| c.id } 116 | end 117 | 118 | it { should have(1).elements } 119 | it { should include(SeedRepo::Commit::ID) } 120 | it { should_not include(SeedRepo::FirstCommit::ID) } 121 | end 122 | 123 | describe :find_all do 124 | context 'max_count' do 125 | subject do 126 | commits = Gitlab::Git::Commit.find_all( 127 | repository, 128 | max_count: 50 129 | ) 130 | 131 | commits.map { |c| c.id } 132 | end 133 | 134 | it { should have(15).elements } 135 | it { should include(SeedRepo::Commit::ID) } 136 | it { should include(SeedRepo::Commit::PARENT_ID) } 137 | it { should include(SeedRepo::FirstCommit::ID) } 138 | end 139 | 140 | context 'ref + max_count + skip' do 141 | subject do 142 | commits = Gitlab::Git::Commit.find_all( 143 | repository, 144 | ref: 'master', 145 | max_count: 50, 146 | skip: 1 147 | ) 148 | 149 | commits.map { |c| c.id } 150 | end 151 | 152 | it { should have(12).elements } 153 | it { should include(SeedRepo::Commit::ID) } 154 | it { should include(SeedRepo::FirstCommit::ID) } 155 | it { should_not include(SeedRepo::LastCommit::ID) } 156 | end 157 | 158 | context 'contains feature + max_count' do 159 | subject do 160 | commits = Gitlab::Git::Commit.find_all( 161 | repository, 162 | contains: 'feature', 163 | max_count: 7 164 | ) 165 | 166 | commits.map { |c| c.id } 167 | end 168 | 169 | it { should have(7).elements } 170 | 171 | it { should_not include(SeedRepo::Commit::PARENT_ID) } 172 | it { should_not include(SeedRepo::Commit::ID) } 173 | it { should include(SeedRepo::BigCommit::ID) } 174 | end 175 | end 176 | end 177 | 178 | describe :init_from_hash do 179 | let(:commit) { Gitlab::Git::Commit.new(sample_commit_hash) } 180 | subject { commit } 181 | 182 | its(:id) { should == sample_commit_hash[:id]} 183 | its(:message) { should == sample_commit_hash[:message]} 184 | end 185 | 186 | describe :stats do 187 | subject { commit.stats } 188 | 189 | its(:additions) { should eq(11) } 190 | its(:deletions) { should eq(6) } 191 | end 192 | 193 | describe :to_diff do 194 | subject { commit.to_diff } 195 | 196 | it { should_not include "From #{SeedRepo::Commit::ID}" } 197 | it { should include 'diff --git a/files/ruby/popen.rb b/files/ruby/popen.rb'} 198 | end 199 | 200 | describe :has_zero_stats? do 201 | it { commit.has_zero_stats?.should == false } 202 | end 203 | 204 | describe :to_patch do 205 | subject { commit.to_patch } 206 | 207 | it { should include "From #{SeedRepo::Commit::ID}" } 208 | it { should include 'diff --git a/files/ruby/popen.rb b/files/ruby/popen.rb'} 209 | end 210 | 211 | describe :to_hash do 212 | let(:hash) { commit.to_hash } 213 | subject { hash } 214 | 215 | it { should be_kind_of Hash } 216 | its(:keys) { should =~ sample_commit_hash.keys } 217 | end 218 | 219 | describe :diffs do 220 | subject { commit.diffs } 221 | 222 | it { should be_kind_of Array } 223 | its(:size) { should eq(2) } 224 | its(:first) { should be_kind_of Gitlab::Git::Diff } 225 | end 226 | 227 | describe :ref_names do 228 | let(:commit) { Gitlab::Git::Commit.find(repository, 'master') } 229 | subject { commit.ref_names(repository) } 230 | 231 | it { should have(2).elements } 232 | it { should include("master") } 233 | it { should_not include("feature") } 234 | end 235 | 236 | def sample_commit_hash 237 | { 238 | author_email: "dmitriy.zaporozhets@gmail.com", 239 | author_name: "Dmitriy Zaporozhets", 240 | authored_date: "2012-02-27 20:51:12 +0200", 241 | committed_date: "2012-02-27 20:51:12 +0200", 242 | committer_email: "dmitriy.zaporozhets@gmail.com", 243 | committer_name: "Dmitriy Zaporozhets", 244 | id: SeedRepo::Commit::ID, 245 | message: "tree css fixes", 246 | parent_ids: ["874797c3a73b60d2187ed6e2fcabd289ff75171e"] 247 | } 248 | end 249 | end 250 | -------------------------------------------------------------------------------- /lib/gitlab_git/repository.rb: -------------------------------------------------------------------------------- 1 | # Gitlab::Git::Commit is a wrapper around native Grit::Repository object 2 | # We dont want to use grit objects inside app/ 3 | # It helps us easily migrate to rugged in future 4 | require_relative 'encoding_herlper' 5 | require 'tempfile' 6 | 7 | module Gitlab 8 | module Git 9 | class Repository 10 | include Gitlab::Git::Popen 11 | 12 | class NoRepository < StandardError; end 13 | 14 | # Default branch in the repository 15 | attr_accessor :root_ref 16 | 17 | # Full path to repo 18 | attr_reader :path 19 | 20 | # Directory name of repo 21 | attr_reader :name 22 | 23 | # Grit repo object 24 | attr_reader :grit 25 | 26 | # Rugged repo object 27 | attr_reader :rugged 28 | 29 | def initialize(path) 30 | @path = path 31 | @name = path.split("/").last 32 | @root_ref = discover_default_branch 33 | end 34 | 35 | def grit 36 | @grit ||= Grit::Repo.new(path) 37 | rescue Grit::NoSuchPathError 38 | raise NoRepository.new('no repository for such path') 39 | end 40 | 41 | # Alias to old method for compatibility 42 | def raw 43 | grit 44 | end 45 | 46 | def rugged 47 | @rugged ||= Rugged::Repository.new(path) 48 | rescue Rugged::RepositoryError, Rugged::OSError 49 | raise NoRepository.new('no repository for such path') 50 | end 51 | 52 | # Returns an Array of branch names 53 | # sorted by name ASC 54 | def branch_names 55 | branches.map(&:name) 56 | end 57 | 58 | # Returns an Array of Branches 59 | def branches 60 | rugged.branches.map do |rugged_ref| 61 | Branch.new(rugged_ref.name, rugged_ref.target) 62 | end.sort_by(&:name) 63 | end 64 | 65 | # Returns an Array of tag names 66 | def tag_names 67 | # rugged.tags returns array of names 68 | rugged.tags.to_a 69 | end 70 | 71 | # Returns an Array of Tags 72 | def tags 73 | rugged.refs.select do |ref| 74 | ref.name =~ /\Arefs\/tags/ 75 | end.map do |rugged_ref| 76 | Tag.new(rugged_ref.name, rugged_ref.target) 77 | end.sort_by(&:name) 78 | end 79 | 80 | # Returns an Array of branch and tag names 81 | def ref_names 82 | branch_names + tag_names 83 | end 84 | 85 | # Deprecated. Will be removed in 5.2 86 | def heads 87 | @heads ||= grit.heads.sort_by(&:name) 88 | end 89 | 90 | def has_commits? 91 | !empty? 92 | end 93 | 94 | def empty? 95 | rugged.empty? 96 | end 97 | 98 | def repo_exists? 99 | !!rugged 100 | end 101 | 102 | # Discovers the default branch based on the repository's available branches 103 | # 104 | # - If no branches are present, returns nil 105 | # - If one branch is present, returns its name 106 | # - If two or more branches are present, returns current HEAD or master or first branch 107 | def discover_default_branch 108 | if branch_names.length == 0 109 | nil 110 | elsif branch_names.length == 1 111 | branch_names.first 112 | elsif rugged_head && branch_names.include?(Ref.extract_branch_name(rugged_head.name)) 113 | Ref.extract_branch_name(rugged_head.name) 114 | elsif branch_names.include?("master") 115 | "master" 116 | else 117 | branch_names.first 118 | end 119 | end 120 | 121 | def rugged_head 122 | rugged.head 123 | rescue Rugged::ReferenceError 124 | nil 125 | end 126 | 127 | # Archive Project to .tar.gz 128 | # 129 | # Already packed repo archives stored at 130 | # app_root/tmp/repositories/project_name/project_name-commit-id.tag.gz 131 | # 132 | def archive_repo(ref, storage_path, format = "tar.gz") 133 | ref = ref || self.root_ref 134 | commit = Gitlab::Git::Commit.find(self, ref) 135 | return nil unless commit 136 | 137 | extension = nil 138 | git_archive_format = nil 139 | pipe_cmd = nil 140 | 141 | case format 142 | when "tar.bz2", "tbz", "tbz2", "tb2", "bz2" 143 | extension = ".tar.bz2" 144 | pipe_cmd = %W(bzip2) 145 | when "tar" 146 | extension = ".tar" 147 | pipe_cmd = %W(cat) 148 | when "zip" 149 | extension = ".zip" 150 | git_archive_format = "zip" 151 | pipe_cmd = %W(cat) 152 | else 153 | # everything else should fall back to tar.gz 154 | extension = ".tar.gz" 155 | git_archive_format = nil 156 | pipe_cmd = %W(gzip -n) 157 | end 158 | 159 | # Build file path 160 | file_name = self.name.gsub("\.git", "") + "-" + commit.id.to_s + extension 161 | file_path = File.join(storage_path, self.name, file_name) 162 | 163 | # Put files into a directory before archiving 164 | prefix = File.basename(self.name) + "/" 165 | 166 | # Create file if not exists 167 | unless File.exists?(file_path) 168 | # create archive in temp file 169 | tmp_file = Tempfile.new('gitlab-archive-repo') 170 | self.grit.archive_to_file(ref, prefix, tmp_file.path, git_archive_format, pipe_cmd) 171 | 172 | # move temp file to persisted location 173 | FileUtils.mkdir_p File.dirname(file_path) 174 | FileUtils.move(tmp_file.path, file_path) 175 | 176 | # delte temp file 177 | tmp_file.close 178 | tmp_file.unlink 179 | end 180 | 181 | file_path 182 | end 183 | 184 | # Return repo size in megabytes 185 | def size 186 | size = popen(%W(du -s), path).first.strip.to_i 187 | (size.to_f / 1024).round(2) 188 | end 189 | 190 | def search_files(query, ref = nil) 191 | if ref.nil? || ref == "" 192 | ref = root_ref 193 | end 194 | 195 | greps = grit.grep(query, 3, ref) 196 | 197 | greps.map do |grep| 198 | Gitlab::Git::BlobSnippet.new(ref, grep.content, grep.startline, grep.filename) 199 | end 200 | end 201 | 202 | # Delegate log to Grit method 203 | # 204 | # Usage. 205 | # repo.log( 206 | # ref: 'master', 207 | # path: 'app/models', 208 | # limit: 10, 209 | # offset: 5, 210 | # ) 211 | # 212 | def log(options) 213 | default_options = { 214 | limit: 10, 215 | offset: 0, 216 | path: nil, 217 | ref: root_ref, 218 | follow: false 219 | } 220 | 221 | options = default_options.merge(options) 222 | 223 | grit.log( 224 | options[:ref] || root_ref, 225 | options[:path], 226 | max_count: options[:limit].to_i, 227 | skip: options[:offset].to_i, 228 | follow: options[:follow] 229 | ) 230 | end 231 | 232 | # Delegate commits_between to Grit method 233 | # 234 | def commits_between(from, to) 235 | grit.commits_between(from, to) 236 | end 237 | 238 | def merge_base_commit(from, to) 239 | grit.git.native(:merge_base, {}, [to, from]).strip 240 | end 241 | 242 | def diff(from, to, *paths) 243 | grit.diff(from, to, *paths) 244 | end 245 | 246 | # Returns commits collection 247 | # 248 | # Ex. 249 | # repo.find_commits( 250 | # ref: 'master', 251 | # max_count: 10, 252 | # skip: 5, 253 | # order: :date 254 | # ) 255 | # 256 | # +options+ is a Hash of optional arguments to git 257 | # :ref is the ref from which to begin (SHA1 or name) 258 | # :contains is the commit contained by the refs from which to begin (SHA1 or name) 259 | # :max_count is the maximum number of commits to fetch 260 | # :skip is the number of commits to skip 261 | # :order is the commits order and allowed value is :date(default) or :topo 262 | # 263 | def find_commits(options = {}) 264 | actual_options = options.dup 265 | 266 | allowed_options = [:ref, :max_count, :skip, :contains, :order] 267 | 268 | actual_options.keep_if do |key, value| 269 | allowed_options.include?(key) 270 | end 271 | 272 | default_options = {pretty: 'raw', order: :date} 273 | 274 | actual_options = default_options.merge(actual_options) 275 | 276 | order = actual_options.delete(:order) 277 | 278 | case order 279 | when :date 280 | actual_options[:date_order] = true 281 | when :topo 282 | actual_options[:topo_order] = true 283 | end 284 | 285 | ref = actual_options.delete(:ref) 286 | 287 | containing_commit = actual_options.delete(:contains) 288 | 289 | args = [] 290 | 291 | if ref 292 | args.push(ref) 293 | elsif containing_commit 294 | args.push(*branch_names_contains(containing_commit)) 295 | else 296 | actual_options[:all] = true 297 | end 298 | 299 | output = grit.git.native(:rev_list, actual_options, *args) 300 | 301 | Grit::Commit.list_from_string(grit, output).map do |commit| 302 | Gitlab::Git::Commit.decorate(commit) 303 | end 304 | rescue Grit::GitRuby::Repository::NoSuchShaFound 305 | [] 306 | end 307 | 308 | # Returns branch names collection that contains the special commit(SHA1 or name) 309 | # 310 | # Ex. 311 | # repo.branch_names_contains('master') 312 | # 313 | def branch_names_contains(commit) 314 | output = grit.git.native(:branch, {contains: true}, commit) 315 | 316 | # Fix encoding issue 317 | output = EncodingHelper::encode!(output) 318 | 319 | # The output is expected as follow 320 | # fix-aaa 321 | # fix-bbb 322 | # * master 323 | output.scan(/[^* \n]+/) 324 | end 325 | 326 | # Get refs hash which key is SHA1 327 | # and value is ref object(Grit::Head or Grit::Remote or Grit::Tag) 328 | def refs_hash 329 | # Initialize only when first call 330 | if @refs_hash.nil? 331 | @refs_hash = Hash.new { |h, k| h[k] = [] } 332 | 333 | grit.refs.each do |r| 334 | @refs_hash[r.commit.id] << r 335 | end 336 | end 337 | @refs_hash 338 | end 339 | 340 | # Lookup for rugged object by oid 341 | def lookup(oid) 342 | rugged.lookup(oid) 343 | end 344 | 345 | # Return hash with submodules info for this repository 346 | # 347 | # Ex. 348 | # { 349 | # "rack" => { 350 | # "id" => "c67be4624545b4263184c4a0e8f887efd0a66320", 351 | # "path" => "rack", 352 | # "url" => "git://github.com/chneukirchen/rack.git" 353 | # }, 354 | # "encoding" => { 355 | # "id" => .... 356 | # } 357 | # } 358 | # 359 | def submodules(ref) 360 | Grit::Submodule.config(grit, ref) 361 | end 362 | 363 | # Return total commits count accessible from passed ref 364 | def commit_count(ref) 365 | walker = Rugged::Walker.new(rugged) 366 | walker.sorting(Rugged::SORT_TOPO | Rugged::SORT_REVERSE) 367 | walker.push(ref) 368 | walker.count 369 | end 370 | end 371 | end 372 | end 373 | --------------------------------------------------------------------------------