599 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Ruby
		
	
	
	
			
		
		
	
	
			599 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Ruby
		
	
	
	
| # Gitlab::Git::Commit is a wrapper around native Rugged::Commit object
 | |
| module Gitlab
 | |
|   module Git
 | |
|     class Commit
 | |
|       include Gitlab::EncodingHelper
 | |
| 
 | |
|       attr_accessor :raw_commit, :head
 | |
| 
 | |
|       MIN_SHA_LENGTH = 7
 | |
|       SERIALIZE_KEYS = [
 | |
|         :id, :message, :parent_ids,
 | |
|         :authored_date, :author_name, :author_email,
 | |
|         :committed_date, :committer_name, :committer_email
 | |
|       ].freeze
 | |
| 
 | |
|       attr_accessor *SERIALIZE_KEYS # rubocop:disable Lint/AmbiguousOperator
 | |
| 
 | |
|       def ==(other)
 | |
|         return false unless other.is_a?(Gitlab::Git::Commit)
 | |
| 
 | |
|         id && id == other.id
 | |
|       end
 | |
| 
 | |
|       class << self
 | |
|         # Get commits collection
 | |
|         #
 | |
|         # Ex.
 | |
|         #   Commit.where(
 | |
|         #     repo: repo,
 | |
|         #     ref: 'master',
 | |
|         #     path: 'app/models',
 | |
|         #     limit: 10,
 | |
|         #     offset: 5,
 | |
|         #   )
 | |
|         #
 | |
|         def where(options)
 | |
|           repo = options.delete(:repo)
 | |
|           raise 'Gitlab::Git::Repository is required' unless repo.respond_to?(:log)
 | |
| 
 | |
|           repo.log(options)
 | |
|         end
 | |
| 
 | |
|         # Get single commit
 | |
|         #
 | |
|         # Ex.
 | |
|         #   Commit.find(repo, '29eda46b')
 | |
|         #
 | |
|         #   Commit.find(repo, 'master')
 | |
|         #
 | |
|         # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/321
 | |
|         def find(repo, commit_id = "HEAD")
 | |
|           # Already a commit?
 | |
|           return commit_id if commit_id.is_a?(Gitlab::Git::Commit)
 | |
| 
 | |
|           # A rugged reference?
 | |
|           commit_id = Gitlab::Git::Ref.dereference_object(commit_id)
 | |
|           return decorate(repo, commit_id) if commit_id.is_a?(Rugged::Commit)
 | |
| 
 | |
|           # Some weird thing?
 | |
|           return nil unless commit_id.is_a?(String)
 | |
| 
 | |
|           commit = repo.gitaly_migrate(:find_commit) do |is_enabled|
 | |
|             if is_enabled
 | |
|               repo.gitaly_commit_client.find_commit(commit_id)
 | |
|             else
 | |
|               obj = repo.rev_parse_target(commit_id)
 | |
| 
 | |
|               obj.is_a?(Rugged::Commit) ? obj : nil
 | |
|             end
 | |
|           end
 | |
| 
 | |
|           decorate(repo, commit) if commit
 | |
|         rescue Rugged::ReferenceError, Rugged::InvalidError, Rugged::ObjectError,
 | |
|                Gitlab::Git::CommandError, Gitlab::Git::Repository::NoRepository,
 | |
|                Rugged::OdbError, Rugged::TreeError, ArgumentError
 | |
|           nil
 | |
|         end
 | |
| 
 | |
|         # Get last commit for HEAD
 | |
|         #
 | |
|         # Ex.
 | |
|         #   Commit.last(repo)
 | |
|         #
 | |
|         def last(repo)
 | |
|           find(repo)
 | |
|         end
 | |
| 
 | |
|         # Get last commit for specified path and ref
 | |
|         #
 | |
|         # Ex.
 | |
|         #   Commit.last_for_path(repo, '29eda46b', 'app/models')
 | |
|         #
 | |
|         #   Commit.last_for_path(repo, 'master', 'Gemfile')
 | |
|         #
 | |
|         def last_for_path(repo, ref, path = nil)
 | |
|           where(
 | |
|             repo: repo,
 | |
|             ref: ref,
 | |
|             path: path,
 | |
|             limit: 1
 | |
|           ).first
 | |
|         end
 | |
| 
 | |
|         # Get commits between two revspecs
 | |
|         # See also #repository.commits_between
 | |
|         #
 | |
|         # Ex.
 | |
|         #   Commit.between(repo, '29eda46b', 'master')
 | |
|         #
 | |
|         def between(repo, base, head)
 | |
|           Gitlab::GitalyClient.migrate(:commits_between) do |is_enabled|
 | |
|             if is_enabled
 | |
|               repo.gitaly_commit_client.between(base, head)
 | |
|             else
 | |
|               repo.rugged_commits_between(base, head).map { |c| decorate(repo, c) }
 | |
|             end
 | |
|           end
 | |
|         rescue Rugged::ReferenceError
 | |
|           []
 | |
|         end
 | |
| 
 | |
|         # Returns commits collection
 | |
|         #
 | |
|         # Ex.
 | |
|         #   Commit.find_all(
 | |
|         #     repo,
 | |
|         #     ref: 'master',
 | |
|         #     max_count: 10,
 | |
|         #     skip: 5,
 | |
|         #     order: :date
 | |
|         #   )
 | |
|         #
 | |
|         #   +options+ is a Hash of optional arguments to git
 | |
|         #     :ref is the ref from which to begin (SHA1 or name)
 | |
|         #     :max_count is the maximum number of commits to fetch
 | |
|         #     :skip is the number of commits to skip
 | |
|         #     :order is the commits order and allowed value is :none (default), :date,
 | |
|         #        :topo, or any combination of them (in an array). Commit ordering types
 | |
|         #        are documented here:
 | |
|         #        http://www.rubydoc.info/github/libgit2/rugged/Rugged#SORT_NONE-constant)
 | |
|         #
 | |
|         # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/326
 | |
|         def find_all(repo, options = {})
 | |
|           Gitlab::GitalyClient.migrate(:find_all_commits) do |is_enabled|
 | |
|             if is_enabled
 | |
|               find_all_by_gitaly(repo, options)
 | |
|             else
 | |
|               find_all_by_rugged(repo, options)
 | |
|             end
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         def find_all_by_rugged(repo, options = {})
 | |
|           actual_options = options.dup
 | |
| 
 | |
|           allowed_options = [:ref, :max_count, :skip, :order]
 | |
| 
 | |
|           actual_options.keep_if do |key|
 | |
|             allowed_options.include?(key)
 | |
|           end
 | |
| 
 | |
|           default_options = { skip: 0 }
 | |
|           actual_options = default_options.merge(actual_options)
 | |
| 
 | |
|           rugged = repo.rugged
 | |
|           walker = Rugged::Walker.new(rugged)
 | |
| 
 | |
|           if actual_options[:ref]
 | |
|             walker.push(rugged.rev_parse_oid(actual_options[:ref]))
 | |
|           else
 | |
|             rugged.references.each("refs/heads/*") do |ref|
 | |
|               walker.push(ref.target_id)
 | |
|             end
 | |
|           end
 | |
| 
 | |
|           walker.sorting(rugged_sort_type(actual_options[:order]))
 | |
| 
 | |
|           commits = []
 | |
|           offset = actual_options[:skip]
 | |
|           limit = actual_options[:max_count]
 | |
|           walker.each(offset: offset, limit: limit) do |commit|
 | |
|             commits.push(decorate(repo, commit))
 | |
|           end
 | |
| 
 | |
|           walker.reset
 | |
| 
 | |
|           commits
 | |
|         rescue Rugged::OdbError
 | |
|           []
 | |
|         end
 | |
| 
 | |
|         def find_all_by_gitaly(repo, options = {})
 | |
|           Gitlab::GitalyClient::CommitService.new(repo).find_all_commits(options)
 | |
|         end
 | |
| 
 | |
|         def decorate(repository, commit, ref = nil)
 | |
|           Gitlab::Git::Commit.new(repository, commit, ref)
 | |
|         end
 | |
| 
 | |
|         # Returns the `Rugged` sorting type constant for one or more given
 | |
|         # sort types. Valid keys are `:none`, `:topo`, and `:date`, or an array
 | |
|         # containing more than one of them. `:date` uses a combination of date and
 | |
|         # topological sorting to closer mimic git's native ordering.
 | |
|         def rugged_sort_type(sort_type)
 | |
|           @rugged_sort_types ||= {
 | |
|             none: Rugged::SORT_NONE,
 | |
|             topo: Rugged::SORT_TOPO,
 | |
|             date: Rugged::SORT_DATE | Rugged::SORT_TOPO
 | |
|           }
 | |
| 
 | |
|           @rugged_sort_types.fetch(sort_type, Rugged::SORT_NONE)
 | |
|         end
 | |
| 
 | |
|         def shas_with_signatures(repository, shas)
 | |
|           GitalyClient.migrate(:filter_shas_with_signatures) do |is_enabled|
 | |
|             if is_enabled
 | |
|               Gitlab::GitalyClient::CommitService.new(repository).filter_shas_with_signatures(shas)
 | |
|             else
 | |
|               shas.select do |sha|
 | |
|                 begin
 | |
|                   Rugged::Commit.extract_signature(repository.rugged, sha)
 | |
|                 rescue Rugged::OdbError
 | |
|                   false
 | |
|                 end
 | |
|               end
 | |
|             end
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         # Only to be used when the object ids will not necessarily have a
 | |
|         # relation to each other. The last 10 commits for a branch for example,
 | |
|         # should go through .where
 | |
|         def batch_by_oid(repo, oids)
 | |
|           repo.gitaly_migrate(:list_commits_by_oid,
 | |
|                               status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
 | |
|             if is_enabled
 | |
|               repo.gitaly_commit_client.list_commits_by_oid(oids)
 | |
|             else
 | |
|               oids.map { |oid| find(repo, oid) }.compact
 | |
|             end
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         def extract_signature(repository, commit_id)
 | |
|           repository.gitaly_migrate(:extract_commit_signature) do |is_enabled|
 | |
|             if is_enabled
 | |
|               repository.gitaly_commit_client.extract_signature(commit_id)
 | |
|             else
 | |
|               rugged_extract_signature(repository, commit_id)
 | |
|             end
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         def extract_signature_lazily(repository, commit_id)
 | |
|           BatchLoader.for({ repository: repository, commit_id: commit_id }).batch do |items, loader|
 | |
|             items_by_repo = items.group_by { |i| i[:repository] }
 | |
| 
 | |
|             items_by_repo.each do |repo, items|
 | |
|               commit_ids = items.map { |i| i[:commit_id] }
 | |
| 
 | |
|               signatures = batch_signature_extraction(repository, commit_ids)
 | |
| 
 | |
|               signatures.each do |commit_sha, signature_data|
 | |
|                 loader.call({ repository: repository, commit_id: commit_sha }, signature_data)
 | |
|               end
 | |
|             end
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         def batch_signature_extraction(repository, commit_ids)
 | |
|           repository.gitaly_migrate(:extract_commit_signature_in_batch) do |is_enabled|
 | |
|             if is_enabled
 | |
|               gitaly_batch_signature_extraction(repository, commit_ids)
 | |
|             else
 | |
|               rugged_batch_signature_extraction(repository, commit_ids)
 | |
|             end
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         def gitaly_batch_signature_extraction(repository, commit_ids)
 | |
|           repository.gitaly_commit_client.get_commit_signatures(commit_ids)
 | |
|         end
 | |
| 
 | |
|         def rugged_batch_signature_extraction(repository, commit_ids)
 | |
|           commit_ids.each_with_object({}) do |commit_id, signatures|
 | |
|             signature_data = rugged_extract_signature(repository, commit_id)
 | |
|             next unless signature_data
 | |
| 
 | |
|             signatures[commit_id] = signature_data
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         def rugged_extract_signature(repository, commit_id)
 | |
|           begin
 | |
|             Rugged::Commit.extract_signature(repository.rugged, commit_id)
 | |
|           rescue Rugged::OdbError
 | |
|             nil
 | |
|           end
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       def initialize(repository, raw_commit, head = nil)
 | |
|         raise "Nil as raw commit passed" unless raw_commit
 | |
| 
 | |
|         case raw_commit
 | |
|         when Hash
 | |
|           init_from_hash(raw_commit)
 | |
|         when Rugged::Commit
 | |
|           init_from_rugged(raw_commit)
 | |
|         when Gitaly::GitCommit
 | |
|           init_from_gitaly(raw_commit)
 | |
|         else
 | |
|           raise "Invalid raw commit type: #{raw_commit.class}"
 | |
|         end
 | |
| 
 | |
|         @repository = repository
 | |
|         @head = head
 | |
|       end
 | |
| 
 | |
|       def sha
 | |
|         id
 | |
|       end
 | |
| 
 | |
|       def short_id(length = 10)
 | |
|         id.to_s[0..length]
 | |
|       end
 | |
| 
 | |
|       def safe_message
 | |
|         @safe_message ||= message
 | |
|       end
 | |
| 
 | |
|       def created_at
 | |
|         committed_date
 | |
|       end
 | |
| 
 | |
|       # Was this commit committed by a different person than the original author?
 | |
|       def different_committer?
 | |
|         author_name != committer_name || author_email != committer_email
 | |
|       end
 | |
| 
 | |
|       def parent_id
 | |
|         parent_ids.first
 | |
|       end
 | |
| 
 | |
|       # Shows the diff between the commit's parent and the commit.
 | |
|       #
 | |
|       # Cuts out the header and stats from #to_patch and returns only the diff.
 | |
|       #
 | |
|       # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/324
 | |
|       def to_diff
 | |
|         Gitlab::GitalyClient.migrate(:commit_patch, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
 | |
|           if is_enabled
 | |
|             @repository.gitaly_commit_client.patch(id)
 | |
|           else
 | |
|             rugged_diff_from_parent.patch
 | |
|           end
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       # Returns a diff object for the changes from this commit's first parent.
 | |
|       # If there is no parent, then the diff is between this commit and an
 | |
|       # empty repo. See Repository#diff for keys allowed in the +options+
 | |
|       # hash.
 | |
|       def diff_from_parent(options = {})
 | |
|         Gitlab::GitalyClient.migrate(:commit_raw_diffs) do |is_enabled|
 | |
|           if is_enabled
 | |
|             @repository.gitaly_commit_client.diff_from_parent(self, options)
 | |
|           else
 | |
|             rugged_diff_from_parent(options)
 | |
|           end
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       def rugged_diff_from_parent(options = {})
 | |
|         options ||= {}
 | |
|         break_rewrites = options[:break_rewrites]
 | |
|         actual_options = Gitlab::Git::Diff.filter_diff_options(options)
 | |
| 
 | |
|         diff = if rugged_commit.parents.empty?
 | |
|                  rugged_commit.diff(actual_options.merge(reverse: true))
 | |
|                else
 | |
|                  rugged_commit.parents[0].diff(rugged_commit, actual_options)
 | |
|                end
 | |
| 
 | |
|         diff.find_similar!(break_rewrites: break_rewrites)
 | |
|         diff
 | |
|       end
 | |
| 
 | |
|       def deltas
 | |
|         @deltas ||= begin
 | |
|           deltas = Gitlab::GitalyClient.migrate(:commit_deltas) do |is_enabled|
 | |
|             if is_enabled
 | |
|               @repository.gitaly_commit_client.commit_deltas(self)
 | |
|             else
 | |
|               rugged_diff_from_parent.each_delta
 | |
|             end
 | |
|           end
 | |
| 
 | |
|           deltas.map { |delta| Gitlab::Git::Diff.new(delta) }
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       def has_zero_stats?
 | |
|         stats.total.zero?
 | |
|       rescue
 | |
|         true
 | |
|       end
 | |
| 
 | |
|       def no_commit_message
 | |
|         "--no commit message"
 | |
|       end
 | |
| 
 | |
|       def to_hash
 | |
|         serialize_keys.map.with_object({}) do |key, hash|
 | |
|           hash[key] = send(key) # rubocop:disable GitlabSecurity/PublicSend
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       def date
 | |
|         committed_date
 | |
|       end
 | |
| 
 | |
|       def diffs(options = {})
 | |
|         Gitlab::Git::DiffCollection.new(diff_from_parent(options), options)
 | |
|       end
 | |
| 
 | |
|       def parents
 | |
|         parent_ids.map { |oid| self.class.find(@repository, oid) }.compact
 | |
|       end
 | |
| 
 | |
|       def stats
 | |
|         Gitlab::Git::CommitStats.new(@repository, self)
 | |
|       end
 | |
| 
 | |
|       def to_patch(options = {})
 | |
|         begin
 | |
|           rugged_commit.to_mbox(options)
 | |
|         rescue Rugged::InvalidError => ex
 | |
|           if ex.message =~ /commit \w+ is a merge commit/i
 | |
|             'Patch format is not currently supported for merge commits.'
 | |
|           end
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       # Get ref names collection
 | |
|       #
 | |
|       # Ex.
 | |
|       #   commit.ref_names(repo)
 | |
|       #
 | |
|       def ref_names(repo)
 | |
|         refs(repo).map do |ref|
 | |
|           ref.sub(%r{^refs/(heads|remotes|tags)/}, "")
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       def message
 | |
|         encode! @message
 | |
|       end
 | |
| 
 | |
|       def author_name
 | |
|         encode! @author_name
 | |
|       end
 | |
| 
 | |
|       def author_email
 | |
|         encode! @author_email
 | |
|       end
 | |
| 
 | |
|       def committer_name
 | |
|         encode! @committer_name
 | |
|       end
 | |
| 
 | |
|       def committer_email
 | |
|         encode! @committer_email
 | |
|       end
 | |
| 
 | |
|       def rugged_commit
 | |
|         @rugged_commit ||= if raw_commit.is_a?(Rugged::Commit)
 | |
|                              raw_commit
 | |
|                            else
 | |
|                              @repository.rev_parse_target(id)
 | |
|                            end
 | |
|       end
 | |
| 
 | |
|       def merge_commit?
 | |
|         parent_ids.size > 1
 | |
|       end
 | |
| 
 | |
|       def tree_entry(path)
 | |
|         @repository.gitaly_migrate(:commit_tree_entry) do |is_migrated|
 | |
|           if is_migrated
 | |
|             gitaly_tree_entry(path)
 | |
|           else
 | |
|             rugged_tree_entry(path)
 | |
|           end
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       def to_gitaly_commit
 | |
|         return raw_commit if raw_commit.is_a?(Gitaly::GitCommit)
 | |
| 
 | |
|         message_split = raw_commit.message.split("\n", 2)
 | |
|         Gitaly::GitCommit.new(
 | |
|           id: raw_commit.oid,
 | |
|           subject: message_split[0] ? message_split[0].chomp.b : "",
 | |
|           body: raw_commit.message.b,
 | |
|           parent_ids: raw_commit.parent_ids,
 | |
|           author: gitaly_commit_author_from_rugged(raw_commit.author),
 | |
|           committer: gitaly_commit_author_from_rugged(raw_commit.committer)
 | |
|         )
 | |
|       end
 | |
| 
 | |
|       private
 | |
| 
 | |
|       def init_from_hash(hash)
 | |
|         raw_commit = hash.symbolize_keys
 | |
| 
 | |
|         serialize_keys.each do |key|
 | |
|           send("#{key}=", raw_commit[key]) # rubocop:disable GitlabSecurity/PublicSend
 | |
|         end
 | |
|       end
 | |
| 
 | |
|       def init_from_rugged(commit)
 | |
|         author = commit.author
 | |
|         committer = commit.committer
 | |
| 
 | |
|         @raw_commit = commit
 | |
|         @id = commit.oid
 | |
|         @message = commit.message
 | |
|         @authored_date = author[:time]
 | |
|         @committed_date = committer[:time]
 | |
|         @author_name = author[:name]
 | |
|         @author_email = author[:email]
 | |
|         @committer_name = committer[:name]
 | |
|         @committer_email = committer[:email]
 | |
|         @parent_ids = commit.parents.map(&:oid)
 | |
|       end
 | |
| 
 | |
|       def init_from_gitaly(commit)
 | |
|         @raw_commit = commit
 | |
|         @id = commit.id
 | |
|         # TODO: Once gitaly "takes over" Rugged consider separating the
 | |
|         # subject from the message to make it clearer when there's one
 | |
|         # available but not the other.
 | |
|         @message = (commit.body.presence || commit.subject).dup
 | |
|         @authored_date = Time.at(commit.author.date.seconds).utc
 | |
|         @author_name = commit.author.name.dup
 | |
|         @author_email = commit.author.email.dup
 | |
|         @committed_date = Time.at(commit.committer.date.seconds).utc
 | |
|         @committer_name = commit.committer.name.dup
 | |
|         @committer_email = commit.committer.email.dup
 | |
|         @parent_ids = Array(commit.parent_ids)
 | |
|       end
 | |
| 
 | |
|       def serialize_keys
 | |
|         SERIALIZE_KEYS
 | |
|       end
 | |
| 
 | |
|       def gitaly_tree_entry(path)
 | |
|         # We're only interested in metadata, so limit actual data to 1 byte
 | |
|         # since Gitaly doesn't support "send no data" option.
 | |
|         entry = @repository.gitaly_commit_client.tree_entry(id, path, 1)
 | |
|         return unless entry
 | |
| 
 | |
|         # To be compatible with the rugged format
 | |
|         entry = entry.to_h
 | |
|         entry.delete(:data)
 | |
|         entry[:name] = File.basename(path)
 | |
|         entry[:type] = entry[:type].downcase
 | |
| 
 | |
|         entry
 | |
|       end
 | |
| 
 | |
|       # Is this the same as Blob.find_entry_by_path ?
 | |
|       def rugged_tree_entry(path)
 | |
|         rugged_commit.tree.path(path)
 | |
|       rescue Rugged::TreeError
 | |
|         nil
 | |
|       end
 | |
| 
 | |
|       def gitaly_commit_author_from_rugged(author_or_committer)
 | |
|         Gitaly::CommitAuthor.new(
 | |
|           name: author_or_committer[:name].b,
 | |
|           email: author_or_committer[:email].b,
 | |
|           date: Google::Protobuf::Timestamp.new(seconds: author_or_committer[:time].to_i)
 | |
|         )
 | |
|       end
 | |
| 
 | |
|       # Get a collection of Gitlab::Git::Ref objects for this commit.
 | |
|       #
 | |
|       # Ex.
 | |
|       #   commit.ref(repo)
 | |
|       #
 | |
|       def refs(repo)
 | |
|         repo.refs_hash[id]
 | |
|       end
 | |
|     end
 | |
|   end
 | |
| end
 |