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

459 lines
13 KiB
Ruby
Raw Normal View History

2019-02-15 15:39:39 +05:30
# frozen_string_literal: true
2018-11-18 11:00:15 +05:30
# Gitlab::Git::Commit is a wrapper around Gitaly::GitCommit
2017-08-17 22:00:37 +05:30
module Gitlab
module Git
class Commit
2017-09-10 17:25:29 +05:30
include Gitlab::EncodingHelper
2019-05-03 19:53:19 +05:30
prepend Gitlab::Git::RuggedImpl::Commit
2018-12-13 13:39:08 +05:30
extend Gitlab::Git::WrapsGitalyErrors
2020-06-23 00:09:42 +05:30
include Gitlab::Utils::StrongMemoize
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
2018-11-08 19:23:39 +05:30
MAX_COMMIT_MESSAGE_DISPLAY_SIZE = 10.megabytes
2018-03-17 18:26:18 +05:30
MIN_SHA_LENGTH = 7
2017-08-17 22:00:37 +05:30
SERIALIZE_KEYS = [
:id, :message, :parent_ids,
:authored_date, :author_name, :author_email,
2021-03-11 19:13:27 +05:30
:committed_date, :committer_name, :committer_email, :trailers
2017-08-17 22:00:37 +05:30
].freeze
2020-03-13 15:44:24 +05:30
attr_accessor(*SERIALIZE_KEYS)
2021-04-17 20:07:23 +05:30
attr_reader :repository
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")
2018-03-17 18:26:18 +05:30
# Already a commit?
2017-09-10 17:25:29 +05:30
return commit_id if commit_id.is_a?(Gitlab::Git::Commit)
2018-03-17 18:26:18 +05:30
2018-11-08 19:23:39 +05:30
# This saves us an RPC round trip.
2020-05-24 23:13:21 +05:30
return unless valid?(commit_id)
2017-08-17 22:00:37 +05:30
2019-05-03 19:53:19 +05:30
commit = find_commit(repo, commit_id)
2017-08-17 22:00:37 +05:30
2018-03-17 18:26:18 +05:30
decorate(repo, commit) if commit
2018-11-18 11:00:15 +05:30
rescue Gitlab::Git::CommandError, Gitlab::Git::Repository::NoRepository, ArgumentError
2017-08-17 22:00:37 +05:30
nil
end
2019-05-03 19:53:19 +05:30
def find_commit(repo, commit_id)
wrapped_gitaly_errors do
repo.gitaly_commit_client.find_commit(commit_id)
end
end
2017-08-17 22:00:37 +05:30
# 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')
#
2020-07-28 23:09:34 +05:30
def last_for_path(repo, ref, path = nil, literal_pathspec: false)
2020-04-22 19:07:51 +05:30
# rubocop: disable Rails/FindBy
# This is not where..first from ActiveRecord
2017-08-17 22:00:37 +05:30
where(
repo: repo,
ref: ref,
path: path,
2020-07-28 23:09:34 +05:30
limit: 1,
literal_pathspec: literal_pathspec
2017-08-17 22:00:37 +05:30
).first
2020-04-22 19:07:51 +05:30
# rubocop: enable Rails/FindBy
2017-08-17 22:00:37 +05:30
end
# Get commits between two revspecs
# See also #repository.commits_between
#
# Ex.
# Commit.between(repo, '29eda46b', 'master')
#
def between(repo, base, head)
2018-12-13 13:39:08 +05:30
wrapped_gitaly_errors do
2018-11-08 19:23:39 +05:30
repo.gitaly_commit_client.between(base, head)
2017-08-17 22:00:37 +05:30
end
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
2020-03-13 15:44:24 +05:30
# are documented here: https://git-scm.com/docs/git-log#_commit_ordering
2017-08-17 22:00:37 +05:30
def find_all(repo, options = {})
2018-12-13 13:39:08 +05:30
wrapped_gitaly_errors do
2018-11-08 19:23:39 +05:30
Gitlab::GitalyClient::CommitService.new(repo).find_all_commits(options)
2017-09-10 17:25:29 +05:30
end
2017-08-17 22:00:37 +05:30
end
2017-09-10 17:25:29 +05:30
def decorate(repository, commit, ref = nil)
Gitlab::Git::Commit.new(repository, commit, ref)
2017-08-17 22:00:37 +05:30
end
2017-09-10 17:25:29 +05:30
def shas_with_signatures(repository, shas)
2018-11-08 19:23:39 +05:30
Gitlab::GitalyClient::CommitService.new(repository).filter_shas_with_signatures(shas)
2018-03-17 18:26:18 +05:30
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)
2018-12-13 13:39:08 +05:30
wrapped_gitaly_errors do
2018-11-18 11:00:15 +05:30
repo.gitaly_commit_client.list_commits_by_oid(oids)
2017-09-10 17:25:29 +05:30
end
end
2018-03-17 18:26:18 +05:30
2018-03-27 19:54:05 +05:30
def extract_signature_lazily(repository, commit_id)
2019-02-15 15:39:39 +05:30
BatchLoader.for(commit_id).batch(key: repository) do |commit_ids, loader, args|
batch_signature_extraction(args[:key], commit_ids).each do |commit_id, signature_data|
loader.call(commit_id, signature_data)
2018-03-27 19:54:05 +05:30
end
end
end
def batch_signature_extraction(repository, commit_ids)
repository.gitaly_commit_client.get_commit_signatures(commit_ids)
end
2018-11-08 19:23:39 +05:30
def get_message(repository, commit_id)
2019-02-15 15:39:39 +05:30
BatchLoader.for(commit_id).batch(key: repository) do |commit_ids, loader, args|
get_messages(args[:key], commit_ids).each do |commit_id, message|
loader.call(commit_id, message)
2018-11-08 19:23:39 +05:30
end
2018-03-27 19:54:05 +05:30
end
end
2018-11-08 19:23:39 +05:30
def get_messages(repository, commit_ids)
repository.gitaly_commit_client.get_commit_messages(commit_ids)
2018-03-17 18:26:18 +05:30
end
2017-08-17 22:00:37 +05:30
end
2019-07-07 11:18:12 +05:30
def initialize(repository, raw_commit, head = nil, lazy_load_parents: false)
2017-08-17 22:00:37 +05:30
raise "Nil as raw commit passed" unless raw_commit
2018-11-08 19:23:39 +05:30
@repository = repository
@head = head
2019-07-07 11:18:12 +05:30
@lazy_load_parents = lazy_load_parents
2018-11-08 19:23:39 +05:30
2019-05-03 19:53:19 +05:30
init_commit(raw_commit)
end
def init_commit(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 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
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
2019-07-07 11:18:12 +05:30
def parent_ids
return @parent_ids unless @lazy_load_parents
@parent_ids ||= @repository.commit(id).parent_ids
end
2017-08-17 22:00:37 +05:30
def parent_id
parent_ids.first
end
2020-06-23 00:09:42 +05:30
def committed_date
strong_memoize(:committed_date) do
init_date_from_gitaly(raw_commit.committer) if raw_commit
end
end
def authored_date
strong_memoize(:authored_date) do
init_date_from_gitaly(raw_commit.author) if raw_commit
end
end
2017-08-17 22:00:37 +05:30
# 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
2018-03-17 18:26:18 +05:30
# empty repo. See Repository#diff for keys allowed in the +options+
2017-08-17 22:00:37 +05:30
# hash.
def diff_from_parent(options = {})
2018-11-08 19:23:39 +05:30
@repository.gitaly_commit_client.diff_from_parent(self, options)
2018-03-17 18:26:18 +05:30
end
2017-09-10 17:25:29 +05:30
def deltas
2018-03-17 18:26:18 +05:30
@deltas ||= begin
2018-11-18 11:00:15 +05:30
deltas = @repository.gitaly_commit_client.commit_deltas(self)
2018-03-17 18:26:18 +05:30
deltas.map { |delta| Gitlab::Git::Diff.new(delta) }
end
2017-08-17 22:00:37 +05:30
end
def has_zero_stats?
2020-10-24 23:57:45 +05:30
stats.total == 0
2021-06-08 01:23:25 +05:30
rescue StandardError
2017-08-17 22:00:37 +05:30
true
end
def no_commit_message
2020-03-13 15:44:24 +05:30
"No commit message"
2017-08-17 22:00:37 +05:30
end
def to_hash
serialize_keys.map.with_object({}) do |key, hash|
2018-03-17 18:26:18 +05:30
hash[key] = send(key) # rubocop:disable GitlabSecurity/PublicSend
2017-08-17 22:00:37 +05:30
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
2018-03-17 18:26:18 +05:30
Gitlab::Git::CommitStats.new(@repository, self)
2017-08-17 22:00:37 +05:30
end
# Get ref names collection
#
# Ex.
# commit.ref_names(repo)
#
def ref_names(repo)
refs(repo).map do |ref|
2018-03-17 18:26:18 +05:30
ref.sub(%r{^refs/(heads|remotes|tags)/}, "")
2017-08-17 22:00:37 +05:30
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
2018-03-17 18:26:18 +05:30
def merge_commit?
parent_ids.size > 1
end
2019-07-07 11:18:12 +05:30
def gitaly_commit?
raw_commit.is_a?(Gitaly::GitCommit)
end
2018-03-17 18:26:18 +05:30
def tree_entry(path)
2018-10-15 14:42:47 +05:30
return unless path.present?
2019-05-03 19:53:19 +05:30
commit_tree_entry(path)
end
def commit_tree_entry(path)
2018-11-08 19:23:39 +05:30
# 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
2019-05-03 19:53:19 +05:30
# To be compatible with the rugged format
2018-11-08 19:23:39 +05:30
entry = entry.to_h
entry.delete(:data)
entry[:name] = File.basename(path)
entry[:type] = entry[:type].downcase
entry
2018-03-17 18:26:18 +05:30
end
def to_gitaly_commit
2019-07-07 11:18:12 +05:30
return raw_commit if gitaly_commit?
2018-03-17 18:26:18 +05:30
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,
2018-12-05 23:21:45 +05:30
author: gitaly_commit_author_from_raw(raw_commit.author),
committer: gitaly_commit_author_from_raw(raw_commit.committer)
2018-03-17 18:26:18 +05:30
)
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|
2018-03-17 18:26:18 +05:30
send("#{key}=", raw_commit[key]) # rubocop:disable GitlabSecurity/PublicSend
2017-08-17 22:00:37 +05:30
end
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.
2018-11-08 19:23:39 +05:30
@message = message_from_gitaly_body
2017-09-10 17:25:29 +05:30
@author_name = commit.author.name.dup
@author_email = commit.author.email.dup
2020-01-01 13:55:28 +05:30
2017-09-10 17:25:29 +05:30
@committer_name = commit.committer.name.dup
@committer_email = commit.committer.email.dup
2018-03-17 18:26:18 +05:30
@parent_ids = Array(commit.parent_ids)
2021-04-29 21:17:54 +05:30
@trailers = commit.trailers.to_h { |t| [t.key, t.value] }
2017-09-10 17:25:29 +05:30
end
2020-01-01 13:55:28 +05:30
# Gitaly provides a UNIX timestamp in author.date.seconds, and a timezone
# offset in author.timezone. If the latter isn't present, assume UTC.
def init_date_from_gitaly(author)
if author.timezone.present?
Time.strptime("#{author.date.seconds} #{author.timezone}", '%s %z')
else
Time.at(author.date.seconds).utc
end
end
2017-08-17 22:00:37 +05:30
def serialize_keys
SERIALIZE_KEYS
end
2018-03-17 18:26:18 +05:30
2018-12-05 23:21:45 +05:30
def gitaly_commit_author_from_raw(author_or_committer)
2018-03-17 18:26:18 +05:30
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
2018-11-08 19:23:39 +05:30
def message_from_gitaly_body
2020-10-24 23:57:45 +05:30
return @raw_commit.subject.dup if @raw_commit.body_size == 0
2018-11-08 19:23:39 +05:30
return @raw_commit.body.dup if full_body_fetched_from_gitaly?
if @raw_commit.body_size > MAX_COMMIT_MESSAGE_DISPLAY_SIZE
"#{@raw_commit.subject}\n\n--commit message is too big".strip
else
fetch_body_from_gitaly
end
end
def full_body_fetched_from_gitaly?
@raw_commit.body.bytesize == @raw_commit.body_size
end
def fetch_body_from_gitaly
self.class.get_message(@repository, id)
end
2020-05-24 23:13:21 +05:30
def self.valid?(commit_id)
commit_id.is_a?(String) && !(
commit_id.start_with?('-') ||
commit_id.include?(':') ||
commit_id.include?("\x00") ||
commit_id.match?(/\s/)
)
end
2017-08-17 22:00:37 +05:30
end
end
end
2019-05-03 19:53:19 +05:30
Gitlab::Git::Commit.singleton_class.prepend Gitlab::Git::RuggedImpl::Commit::ClassMethods