71569a9c41
This solves a problem where commits populated with Gitaly were not equal to commits populated with Rugged. This is because Gitaly may not return all fields of a commit for optimizations purposes, which resulted in false negatives when comparing the same commit (commits with the same sha) with different sources.
306 lines
7.6 KiB
Ruby
306 lines
7.6 KiB
Ruby
# Gitlab::Git::Commit is a wrapper around native Rugged::Commit object
|
|
module Gitlab
|
|
module Git
|
|
class Commit
|
|
include Gitlab::Git::EncodingHelper
|
|
|
|
attr_accessor :raw_commit, :head, :refs
|
|
|
|
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
|
|
|
|
delegate :tree, to: :raw_commit
|
|
|
|
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).map { |c| decorate(c) }
|
|
end
|
|
|
|
# Get single commit
|
|
#
|
|
# Ex.
|
|
# Commit.find(repo, '29eda46b')
|
|
#
|
|
# Commit.find(repo, 'master')
|
|
#
|
|
def find(repo, commit_id = "HEAD")
|
|
return decorate(commit_id) if commit_id.is_a?(Rugged::Commit)
|
|
|
|
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)
|
|
|
|
decorate(obj)
|
|
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)
|
|
repo.commits_between(base, head).map do |commit|
|
|
decorate(commit)
|
|
end
|
|
rescue Rugged::ReferenceError
|
|
[]
|
|
end
|
|
|
|
# Delegate Repository#find_commits
|
|
def find_all(repo, options = {})
|
|
repo.find_commits(options)
|
|
end
|
|
|
|
def decorate(commit, ref = nil)
|
|
Gitlab::Git::Commit.new(commit, ref)
|
|
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
|
|
end
|
|
|
|
def initialize(raw_commit, head = nil)
|
|
raise "Nil as raw commit passed" unless raw_commit
|
|
|
|
if raw_commit.is_a?(Hash)
|
|
init_from_hash(raw_commit)
|
|
elsif raw_commit.is_a?(Rugged::Commit)
|
|
init_from_rugged(raw_commit)
|
|
else
|
|
raise "Invalid raw commit type: #{raw_commit.class}"
|
|
end
|
|
|
|
@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.
|
|
def to_diff(options = {})
|
|
diff_from_parent(options).patch
|
|
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 = {})
|
|
Commit.diff_from_parent(raw_commit, options)
|
|
end
|
|
|
|
def deltas
|
|
@deltas ||= diff_from_parent.each_delta.map { |d| Gitlab::Git::Diff.new(d) }
|
|
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
|
|
raw_commit.parents.map { |c| Gitlab::Git::Commit.new(c) }
|
|
end
|
|
|
|
def stats
|
|
Gitlab::Git::CommitStats.new(self)
|
|
end
|
|
|
|
def to_patch(options = {})
|
|
begin
|
|
raw_commit.to_mbox(options)
|
|
rescue Rugged::InvalidError => ex
|
|
if ex.message =~ /Commit \w+ is a merge commit/
|
|
'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
|
|
|
|
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
|
|
|
|
def serialize_keys
|
|
SERIALIZE_KEYS
|
|
end
|
|
end
|
|
end
|
|
end
|