debian-mirror-gitlab/lib/gitlab/git/commit.rb

436 lines
12 KiB
Ruby
Raw Normal View History

2017-08-17 22:00:37 +05:30
# Gitlab::Git::Commit is a wrapper around native Rugged::Commit object
module Gitlab
module Git
class Commit
2017-09-10 17:25:29 +05:30
include Gitlab::EncodingHelper
2017-08-17 22:00:37 +05:30
2017-09-10 17:25:29 +05:30
attr_accessor :raw_commit, :head
2017-08-17 22:00:37 +05:30
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
2017-09-10 17:25:29 +05:30
delegate :tree, to: :rugged_commit
2017-08-17 22:00:37 +05:30
def ==(other)
return false unless other.is_a?(Gitlab::Git::Commit)
2017-09-10 17:25:29 +05:30
id && id == other.id
2017-08-17 22:00:37 +05:30
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)
2017-09-10 17:25:29 +05:30
repo.log(options)
2017-08-17 22:00:37 +05:30
end
# Get single commit
#
# Ex.
# Commit.find(repo, '29eda46b')
#
# Commit.find(repo, 'master')
#
2017-09-10 17:25:29 +05:30
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/321
2017-08-17 22:00:37 +05:30
def find(repo, commit_id = "HEAD")
2017-09-10 17:25:29 +05:30
return commit_id if commit_id.is_a?(Gitlab::Git::Commit)
return decorate(repo, commit_id) if commit_id.is_a?(Rugged::Commit)
2017-08-17 22:00:37 +05:30
obj = if commit_id.is_a?(String)
repo.rev_parse_target(commit_id)
else
Gitlab::Git::Ref.dereference_object(commit_id)
end
return nil unless obj.is_a?(Rugged::Commit)
2017-09-10 17:25:29 +05:30
decorate(repo, obj)
2017-08-17 22:00:37 +05:30
rescue Rugged::ReferenceError, Rugged::InvalidError, Rugged::ObjectError, Gitlab::Git::Repository::NoRepository
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)
2017-09-10 17:25:29 +05:30
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
2017-08-17 22:00:37 +05:30
end
rescue Rugged::ReferenceError
[]
end
2017-09-10 17:25:29 +05:30
# 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
2017-08-17 22:00:37 +05:30
def find_all(repo, options = {})
2017-09-10 17:25:29 +05:30
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
2017-08-17 22:00:37 +05:30
end
2017-09-10 17:25:29 +05:30
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)
2017-08-17 22:00:37 +05:30
end
# Returns a diff object for the changes introduced by +rugged_commit+.
# If +rugged_commit+ doesn't have a parent, then the diff is between
# this commit and an empty repo. See Repository#diff for the keys
# allowed in the +options+ hash.
def diff_from_parent(rugged_commit, 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
2017-09-10 17:25:29 +05:30
# 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)
shas.select do |sha|
begin
Rugged::Commit.extract_signature(repository.rugged, sha)
rescue Rugged::OdbError
false
end
end
end
2017-08-17 22:00:37 +05:30
end
2017-09-10 17:25:29 +05:30
def initialize(repository, raw_commit, head = nil)
2017-08-17 22:00:37 +05:30
raise "Nil as raw commit passed" unless raw_commit
2017-09-10 17:25:29 +05:30
case raw_commit
when Hash
2017-08-17 22:00:37 +05:30
init_from_hash(raw_commit)
2017-09-10 17:25:29 +05:30
when Rugged::Commit
2017-08-17 22:00:37 +05:30
init_from_rugged(raw_commit)
2017-09-10 17:25:29 +05:30
when Gitaly::GitCommit
init_from_gitaly(raw_commit)
2017-08-17 22:00:37 +05:30
else
raise "Invalid raw commit type: #{raw_commit.class}"
end
2017-09-10 17:25:29 +05:30
@repository = repository
2017-08-17 22:00:37 +05:30
@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.
2017-09-10 17:25:29 +05:30
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/324
def to_diff
diff_from_parent.patch
2017-08-17 22:00:37 +05:30
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 = {})
2017-09-10 17:25:29 +05:30
Commit.diff_from_parent(rugged_commit, options)
end
def deltas
@deltas ||= diff_from_parent.each_delta.map { |d| Gitlab::Git::Diff.new(d) }
2017-08-17 22:00:37 +05:30
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)
end
end
def date
committed_date
end
def diffs(options = {})
Gitlab::Git::DiffCollection.new(diff_from_parent(options), options)
end
def parents
2017-09-10 17:25:29 +05:30
parent_ids.map { |oid| self.class.find(@repository, oid) }.compact
2017-08-17 22:00:37 +05:30
end
def stats
Gitlab::Git::CommitStats.new(self)
end
def to_patch(options = {})
begin
2017-09-10 17:25:29 +05:30
rugged_commit.to_mbox(options)
2017-08-17 22:00:37 +05:30
rescue Rugged::InvalidError => ex
2017-09-10 17:25:29 +05:30
if ex.message =~ /commit \w+ is a merge commit/i
2017-08-17 22:00:37 +05:30
'Patch format is not currently supported for merge commits.'
end
end
end
# Get a collection of Rugged::Reference objects for this commit.
#
# Ex.
# commit.ref(repo)
#
def refs(repo)
repo.refs_hash[id]
end
# Get ref names collection
#
# Ex.
# commit.ref_names(repo)
#
def ref_names(repo)
refs(repo).map do |ref|
ref.name.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
2017-09-10 17:25:29 +05:30
def rugged_commit
@rugged_commit ||= if raw_commit.is_a?(Rugged::Commit)
raw_commit
else
@repository.rev_parse_target(id)
end
end
2017-08-17 22:00:37 +05:30
private
def init_from_hash(hash)
raw_commit = hash.symbolize_keys
serialize_keys.each do |key|
send("#{key}=", raw_commit[key])
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
2017-09-10 17:25:29 +05:30
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)
@author_name = commit.author.name.dup
@author_email = commit.author.email.dup
@committed_date = Time.at(commit.committer.date.seconds)
@committer_name = commit.committer.name.dup
@committer_email = commit.committer.email.dup
@parent_ids = commit.parent_ids
end
2017-08-17 22:00:37 +05:30
def serialize_keys
SERIALIZE_KEYS
end
end
end
end