Class: Gitlab::Git::Commit

Inherits:
Object
  • Object
show all
Extended by:
WrapsGitalyErrors
Includes:
EncodingHelper, RuggedImpl::Commit, Utils::StrongMemoize
Defined in:
lib/gitlab/git/commit.rb

Constant Summary collapse

MAX_COMMIT_MESSAGE_DISPLAY_SIZE =
10.megabytes
SHA1_LENGTH =
40
SHA256_LENGTH =
64
MIN_SHA_LENGTH =
7
MAX_SHA_LENGTH =
SHA256_LENGTH
RAW_SHA_PATTERN =
"\\h{#{MIN_SHA_LENGTH},#{MAX_SHA_LENGTH}}".freeze
SHA_PATTERN =
/#{RAW_SHA_PATTERN}/
RAW_FULL_SHA_PATTERN =

Match a full SHA. Note that because this expression is not anchored it will match any SHA that is at least SHA1_LENGTH long.

"\\h{#{SHA1_LENGTH}}(?:\\h{#{SHA256_LENGTH - SHA1_LENGTH}})?".freeze
FULL_SHA_PATTERN =
/#{RAW_FULL_SHA_PATTERN}/
SERIALIZE_KEYS =
[
  :id, :message, :parent_ids,
  :authored_date, :author_name, :author_email,
  :committed_date, :committer_name, :committer_email, :trailers, :referenced_by
].freeze

Constants included from EncodingHelper

EncodingHelper::BOM_UTF8, EncodingHelper::ENCODING_CONFIDENCE_THRESHOLD, EncodingHelper::ESCAPED_CHARS, EncodingHelper::UNICODE_REPLACEMENT_CHARACTER

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from WrapsGitalyErrors

wrapped_gitaly_errors

Methods included from EncodingHelper

#binary_io, #detect_binary?, #detect_encoding, #detect_libgit2_binary?, #encode!, #encode_binary, #encode_utf8, #encode_utf8_no_detect, #encode_utf8_with_escaping!, #encode_utf8_with_replacement_character, #strip_bom, #unquote_path

Methods included from RuggedImpl::Commit

#init_from_rugged, #rugged_commit, #rugged_tree_entry

Methods included from Utils::Override

#extended, extensions, #included, #method_added, #override, #prepended, #queue_verification, verify!

Methods included from RuggedImpl::UseRugged

#execute_rugged_call, #rugged_enabled_through_feature_flag?, #rugged_feature_keys, #running_puma_with_multiple_threads?, #use_rugged?

Constructor Details

#initialize(repository, raw_commit, head = nil, lazy_load_parents: false) ⇒ Commit

Returns a new instance of Commit.



213
214
215
216
217
218
219
220
221
# File 'lib/gitlab/git/commit.rb', line 213

def initialize(repository, raw_commit, head = nil, lazy_load_parents: false)
  raise "Nil as raw commit passed" unless raw_commit

  @repository = repository
  @head = head
  @lazy_load_parents = lazy_load_parents

  init_commit(raw_commit)
end

Instance Attribute Details

#headObject

Returns the value of attribute head.



12
13
14
# File 'lib/gitlab/git/commit.rb', line 12

def head
  @head
end

#raw_commitObject

Returns the value of attribute raw_commit.



12
13
14
# File 'lib/gitlab/git/commit.rb', line 12

def raw_commit
  @raw_commit
end

#repositoryObject (readonly)

Returns the value of attribute repository.



36
37
38
# File 'lib/gitlab/git/commit.rb', line 36

def repository
  @repository
end

Class Method Details

.batch_by_oid(repo, oids) ⇒ Object

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



182
183
184
185
186
# File 'lib/gitlab/git/commit.rb', line 182

def batch_by_oid(repo, oids)
  wrapped_gitaly_errors do
    repo.gitaly_commit_client.list_commits_by_oid(oids)
  end
end

.batch_signature_extraction(repository, commit_ids) ⇒ Object



196
197
198
# File 'lib/gitlab/git/commit.rb', line 196

def batch_signature_extraction(repository, commit_ids)
  repository.gitaly_commit_client.get_commit_signatures(commit_ids)
end

.between(repo, base, head, limit: nil) ⇒ Object

Get commits between two revspecs See also #repository.commits_between

Ex.

Commit.between(repo, '29eda46b', 'master') # all commits, ordered oldest to newest
Commit.between(repo, '29eda46b', 'master', limit: 100) # 100 newest commits, ordered oldest to newest


127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/gitlab/git/commit.rb', line 127

def between(repo, base, head, limit: nil)
  # In either of these cases, we are guaranteed to return no commits, so
  # shortcut the RPC call
  return [] if Gitlab::Git.blank_ref?(base) || Gitlab::Git.blank_ref?(head)

  wrapped_gitaly_errors do
    revisions = [head, "^#{base}"] # base..head
    client = repo.gitaly_commit_client

    # We must return the commits in chronological order but using both
    # limit and reverse in the Gitaly RPC would return the oldest N,
    # rather than newest N, commits, so reorder in Ruby with limit
    if limit
      client.list_commits(revisions, pagination_params: { limit: limit }).reverse!
    else
      client.list_commits(revisions, reverse: true)
    end
  end
end

.decorate(repository, commit, ref = nil) ⇒ Object



171
172
173
# File 'lib/gitlab/git/commit.rb', line 171

def decorate(repository, commit, ref = nil)
  Gitlab::Git::Commit.new(repository, commit, ref)
end

.extract_signature_lazily(repository, commit_id) ⇒ Object



188
189
190
191
192
193
194
# File 'lib/gitlab/git/commit.rb', line 188

def extract_signature_lazily(repository, commit_id)
  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)
    end
  end
end

.find(repo, commit_id = "HEAD") ⇒ Object

Get single commit

Ex.

Commit.find(repo, '29eda46b')

Commit.find(repo, 'master')

Gitaly migration: gitlab.com/gitlab-org/gitaly/issues/321



71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/gitlab/git/commit.rb', line 71

def find(repo, commit_id = "HEAD")
  # Already a commit?
  return commit_id if commit_id.is_a?(Gitlab::Git::Commit)

  # This saves us an RPC round trip.
  return unless valid?(commit_id)

  commit = find_commit(repo, commit_id)

  decorate(repo, commit) if commit
rescue Gitlab::Git::CommandError, Gitlab::Git::Repository::NoRepository, ArgumentError
  nil
end

.find_all(repo, options = {}) ⇒ Object

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: https://git-scm.com/docs/git-log#_commit_ordering


165
166
167
168
169
# File 'lib/gitlab/git/commit.rb', line 165

def find_all(repo, options = {})
  wrapped_gitaly_errors do
    Gitlab::GitalyClient::CommitService.new(repo).find_all_commits(options)
  end
end

.find_commit(repo, commit_id) ⇒ Object



85
86
87
88
89
# File 'lib/gitlab/git/commit.rb', line 85

def find_commit(repo, commit_id)
  wrapped_gitaly_errors do
    repo.gitaly_commit_client.find_commit(commit_id)
  end
end

.get_message(repository, commit_id) ⇒ Object



200
201
202
203
204
205
206
# File 'lib/gitlab/git/commit.rb', line 200

def get_message(repository, commit_id)
  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)
    end
  end
end

.get_messages(repository, commit_ids) ⇒ Object



208
209
210
# File 'lib/gitlab/git/commit.rb', line 208

def get_messages(repository, commit_ids)
  repository.gitaly_commit_client.get_commit_messages(commit_ids)
end

.last(repo) ⇒ Object

Get last commit for HEAD

Ex.

Commit.last(repo)


96
97
98
# File 'lib/gitlab/git/commit.rb', line 96

def last(repo)
  find(repo)
end

.last_for_path(repo, ref, path = nil, literal_pathspec: false) ⇒ Object

Get last commit for specified path and ref

Ex.

Commit.last_for_path(repo, '29eda46b', 'app/models')

Commit.last_for_path(repo, 'master', 'Gemfile')


107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/gitlab/git/commit.rb', line 107

def last_for_path(repo, ref, path = nil, literal_pathspec: false)
  # rubocop: disable Rails/FindBy
  # This is not where..first from ActiveRecord
  where(
    repo: repo,
    ref: ref,
    path: path,
    limit: 1,
    literal_pathspec: literal_pathspec
  ).first
  # rubocop: enable Rails/FindBy
end

.shas_with_signatures(repository, shas) ⇒ Object



175
176
177
# File 'lib/gitlab/git/commit.rb', line 175

def shas_with_signatures(repository, shas)
  Gitlab::GitalyClient::CommitService.new(repository).filter_shas_with_signatures(shas)
end

.where(options) ⇒ Object

Get commits collection

Ex.

Commit.where(
  repo: repo,
  ref: 'master',
  path: 'app/models',
  limit: 10,
  offset: 5,
)


56
57
58
59
60
61
# File 'lib/gitlab/git/commit.rb', line 56

def where(options)
  repo = options.delete(:repo)
  raise 'Gitlab::Git::Repository is required' unless repo.respond_to?(:log)

  repo.log(options)
end

Instance Method Details

#==(other) ⇒ Object



38
39
40
41
42
# File 'lib/gitlab/git/commit.rb', line 38

def ==(other)
  return false unless other.is_a?(Gitlab::Git::Commit)

  id && id == other.id
end

#author_emailObject



357
358
359
# File 'lib/gitlab/git/commit.rb', line 357

def author_email
  encode! @author_email
end

#author_nameObject



353
354
355
# File 'lib/gitlab/git/commit.rb', line 353

def author_name
  encode! @author_name
end

#authored_dateObject



277
278
279
280
281
# File 'lib/gitlab/git/commit.rb', line 277

def authored_date
  strong_memoize(:authored_date) do
    init_date_from_gitaly(raw_commit.author) if raw_commit
  end
end

#commit_tree_entry(path) ⇒ Object



383
384
385
386
387
388
389
390
391
392
393
394
395
396
# File 'lib/gitlab/git/commit.rb', line 383

def commit_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

#committed_dateObject



271
272
273
274
275
# File 'lib/gitlab/git/commit.rb', line 271

def committed_date
  strong_memoize(:committed_date) do
    init_date_from_gitaly(raw_commit.committer) if raw_commit
  end
end

#committer_emailObject



365
366
367
# File 'lib/gitlab/git/commit.rb', line 365

def committer_email
  encode! @committer_email
end

#committer_nameObject



361
362
363
# File 'lib/gitlab/git/commit.rb', line 361

def committer_name
  encode! @committer_name
end

#created_atObject



252
253
254
# File 'lib/gitlab/git/commit.rb', line 252

def created_at
  committed_date
end

#dateObject



314
315
316
# File 'lib/gitlab/git/commit.rb', line 314

def date
  committed_date
end

#deltasObject



291
292
293
294
295
296
# File 'lib/gitlab/git/commit.rb', line 291

def deltas
  @deltas ||= begin
    deltas = @repository.gitaly_commit_client.commit_deltas(self)
    deltas.map { |delta| Gitlab::Git::Diff.new(delta) }
  end
end

#diff_from_parent(options = {}) ⇒ Object

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.



287
288
289
# File 'lib/gitlab/git/commit.rb', line 287

def diff_from_parent(options = {})
  @repository.gitaly_commit_client.diff_from_parent(self, options)
end

#different_committer?Boolean

Was this commit committed by a different person than the original author?

Returns:

  • (Boolean)


257
258
259
# File 'lib/gitlab/git/commit.rb', line 257

def different_committer?
  author_name != committer_name || author_email != committer_email
end

#diffs(options = {}) ⇒ Object



318
319
320
# File 'lib/gitlab/git/commit.rb', line 318

def diffs(options = {})
  Gitlab::Git::DiffCollection.new(diff_from_parent(options), options)
end

#first_ref_by_oid(repo) ⇒ Object



341
342
343
344
345
346
347
# File 'lib/gitlab/git/commit.rb', line 341

def first_ref_by_oid(repo)
  ref = repo.refs_by_oid(oid: id, limit: 1).first

  return unless ref

  strip_ref_prefix(ref)
end

#gitaly_commit?Boolean

Returns:

  • (Boolean)


373
374
375
# File 'lib/gitlab/git/commit.rb', line 373

def gitaly_commit?
  raw_commit.is_a?(Gitaly::GitCommit)
end

#has_zero_stats?Boolean

Returns:

  • (Boolean)


298
299
300
301
302
# File 'lib/gitlab/git/commit.rb', line 298

def has_zero_stats?
  stats.total == 0
rescue StandardError
  true
end

#init_commit(raw_commit) ⇒ Object



223
224
225
226
227
228
229
230
231
232
# File 'lib/gitlab/git/commit.rb', line 223

def init_commit(raw_commit)
  case raw_commit
  when Hash
    init_from_hash(raw_commit)
  when Gitaly::GitCommit
    init_from_gitaly(raw_commit)
  else
    raise "Invalid raw commit type: #{raw_commit.class}"
  end
end

#merge_commit?Boolean

Returns:

  • (Boolean)


369
370
371
# File 'lib/gitlab/git/commit.rb', line 369

def merge_commit?
  parent_ids.size > 1
end

#messageObject



349
350
351
# File 'lib/gitlab/git/commit.rb', line 349

def message
  encode! @message
end

#no_commit_messageObject



304
305
306
# File 'lib/gitlab/git/commit.rb', line 304

def no_commit_message
  "No commit message"
end

#parent_idObject



267
268
269
# File 'lib/gitlab/git/commit.rb', line 267

def parent_id
  parent_ids.first
end

#parent_idsObject



261
262
263
264
265
# File 'lib/gitlab/git/commit.rb', line 261

def parent_ids
  return @parent_ids unless @lazy_load_parents

  @parent_ids ||= @repository.commit(id).parent_ids
end

#parentsObject



322
323
324
# File 'lib/gitlab/git/commit.rb', line 322

def parents
  parent_ids.map { |oid| self.class.find(@repository, oid) }.compact
end

#ref_names(repo) ⇒ Object

Get ref names collection

Ex.

commit.ref_names(repo)


335
336
337
338
339
# File 'lib/gitlab/git/commit.rb', line 335

def ref_names(repo)
  refs(repo).map do |ref|
    strip_ref_prefix(ref)
  end
end

#safe_messageObject



248
249
250
# File 'lib/gitlab/git/commit.rb', line 248

def safe_message
  @safe_message ||= message
end

#shaObject



234
235
236
# File 'lib/gitlab/git/commit.rb', line 234

def sha
  id
end

#short_id(length = 10) ⇒ Object



238
239
240
# File 'lib/gitlab/git/commit.rb', line 238

def short_id(length = 10)
  id.to_s[0..length]
end

#statsObject



326
327
328
# File 'lib/gitlab/git/commit.rb', line 326

def stats
  Gitlab::Git::CommitStats.new(@repository, self)
end

#to_gitaly_commitObject



398
399
400
401
402
403
404
405
406
407
408
409
410
# File 'lib/gitlab/git/commit.rb', line 398

def to_gitaly_commit
  return raw_commit if gitaly_commit?

  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_raw(raw_commit.author),
    committer: gitaly_commit_author_from_raw(raw_commit.committer)
  )
end

#to_hashObject



308
309
310
311
312
# File 'lib/gitlab/git/commit.rb', line 308

def to_hash
  serialize_keys.map.with_object({}) do |key, hash|
    hash[key] = send(key) # rubocop:disable GitlabSecurity/PublicSend
  end
end

#tree_entry(path) ⇒ Object



377
378
379
380
381
# File 'lib/gitlab/git/commit.rb', line 377

def tree_entry(path)
  return unless path.present?

  commit_tree_entry(path)
end

#tree_idObject



242
243
244
245
246
# File 'lib/gitlab/git/commit.rb', line 242

def tree_id
  return unless raw_commit

  raw_commit.tree_id
end