Class: Commit

Inherits:
Object
  • Object
show all
Extended by:
ActiveModel::Naming, Gitlab::Cache::RequestCache, Gitlab::Utils::Override
Includes:
ActiveModel::Conversion, ActsAsPaginatedDiff, CacheMarkdownField, Gitlab::Utils::StrongMemoize, Mentionable, Noteable, Participable, Presentable, Referable, StaticModel
Defined in:
app/models/commit.rb

Constant Summary collapse

MIN_SHA_LENGTH =
Gitlab::Git::Commit::MIN_SHA_LENGTH
COMMIT_SHA_PATTERN =
/\h{#{MIN_SHA_LENGTH},40}/.freeze
EXACT_COMMIT_SHA_PATTERN =
/\A#{COMMIT_SHA_PATTERN}\z/.freeze
/(patch)/.freeze
DEFAULT_MAX_DIFF_LINES_SETTING =
50_000
DEFAULT_MAX_DIFF_FILES_SETTING =
1_000
MAX_DIFF_LINES_SETTING_UPPER_BOUND =
100_000
MAX_DIFF_FILES_SETTING_UPPER_BOUND =
3_000
DIFF_SAFE_LIMIT_FACTOR =
10
DRAFT_REGEX =
/\A\s*#{Gitlab::Regex.merge_request_draft}|(fixup!|squash!)\s/.freeze

Constants included from CacheMarkdownField

CacheMarkdownField::INVALIDATED_BY

Constants included from Noteable

Noteable::MAX_NOTES_LIMIT

Instance Attribute Summary collapse

Attributes included from Gitlab::Cache::RequestCache

#request_cache_key_block

Attributes included from Noteable

#system_note_timestamp

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Gitlab::Cache::RequestCache

extended, request_cache, request_cache_key

Methods included from Gitlab::Utils::Override

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

Methods included from CacheMarkdownField

#attribute_invalidated?, #banzai_render_context, #cached_html_for, #cached_html_up_to_date?, #can_cache_field?, #invalidated_markdown_cache?, #latest_cached_markdown_version, #local_version, #mentionable_attributes_changed?, #parent_user, #refresh_markdown_cache, #refresh_markdown_cache!, #rendered_field_content, #skip_project_check?, #store_mentions!, #updated_cached_html_for

Methods included from ActsAsPaginatedDiff

#diffs_in_batch

Methods included from Gitlab::Utils::StrongMemoize

#clear_memoization, #strong_memoize, #strong_memoized?

Methods included from Presentable

#present

Methods included from StaticModel

#[], #destroyed?, #new_record?, #to_param

Methods included from Referable

#referable_inspect, #to_reference_base

Methods included from Mentionable

#all_references, #create_cross_references!, #create_new_cross_references!, #directly_addressed_users, #extractors, #gfm_reference, #local_reference, #matches_cross_reference_regex?, #mentioned_users, #referenced_group_users, #referenced_groups, #referenced_mentionables, #referenced_project_users, #referenced_projects, #referenced_users

Methods included from Participable

#participant?, #participants, #visible_participants

Methods included from Noteable

#after_note_created, #after_note_destroyed, #base_class_name, #capped_notes_count, #creatable_note_email_address, #discussion_ids_relation, #discussion_root_note_ids, #discussions, #discussions_can_be_resolved_by?, #discussions_rendered_on_frontend?, #discussions_resolvable?, #discussions_resolved?, #discussions_to_be_resolved, #etag_caching_enabled?, #grouped_diff_discussions, #has_any_diff_note_positions?, #human_class_name, #lockable?, #note_etag_key, #noteable_target_type_name, #preloads_discussion_diff_highlighting?, #resolvable_discussions, #supports_creating_notes_by_email?, #supports_discussions?, #supports_replying_to_individual_notes?, #supports_resolvable_notes?, #supports_suggestion?

Constructor Details

#initialize(raw_commit, container) ⇒ Commit

Returns a new instance of Commit.


140
141
142
143
144
145
# File 'app/models/commit.rb', line 140

def initialize(raw_commit, container)
  raise "Nil as raw commit passed" unless raw_commit

  @raw = raw_commit
  @container = container
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method, *args, &block) ⇒ Object


326
327
328
# File 'app/models/commit.rb', line 326

def method_missing(method, *args, &block)
  @raw.__send__(method, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
end

Instance Attribute Details

#containerObject (readonly)

Returns the value of attribute container.


26
27
28
# File 'app/models/commit.rb', line 26

def container
  @container
end

#rawObject

Returns the value of attribute raw.


138
139
140
# File 'app/models/commit.rb', line 138

def raw
  @raw
end

#redacted_description_htmlObject

Returns the value of attribute redacted_description_html.


23
24
25
# File 'app/models/commit.rb', line 23

def redacted_description_html
  @redacted_description_html
end

#redacted_full_title_htmlObject

Returns the value of attribute redacted_full_title_html.


25
26
27
# File 'app/models/commit.rb', line 25

def redacted_full_title_html
  @redacted_full_title_html
end

#redacted_title_htmlObject

Returns the value of attribute redacted_title_html.


24
25
26
# File 'app/models/commit.rb', line 24

def redacted_title_html
  @redacted_title_html
end

Class Method Details

.decorate(commits, container) ⇒ Object


52
53
54
55
56
57
58
59
60
# File 'app/models/commit.rb', line 52

def decorate(commits, container)
  commits.map do |commit|
    if commit.is_a?(Commit)
      commit
    else
      self.new(commit, container)
    end
  end
end

.diff_line_count(diffs) ⇒ Object

Calculate number of lines to render for diffs


63
64
65
# File 'app/models/commit.rb', line 63

def diff_line_count(diffs)
  diffs.reduce(0) { |sum, d| sum + Gitlab::Git::Util.count_lines(d.diff) }
end

.diff_max_filesObject


87
88
89
# File 'app/models/commit.rb', line 87

def diff_max_files
  Gitlab::CurrentSettings.diff_max_files
end

.diff_max_linesObject


91
92
93
# File 'app/models/commit.rb', line 91

def diff_max_lines
  Gitlab::CurrentSettings.diff_max_lines
end

.diff_safe_max_filesObject


102
103
104
# File 'app/models/commit.rb', line 102

def diff_safe_max_files
  diff_max_files / DIFF_SAFE_LIMIT_FACTOR
end

.diff_safe_max_linesObject


106
107
108
# File 'app/models/commit.rb', line 106

def diff_safe_max_lines
  diff_max_lines / DIFF_SAFE_LIMIT_FACTOR
end

.from_hash(hash, container) ⇒ Object


110
111
112
113
# File 'app/models/commit.rb', line 110

def from_hash(hash, container)
  raw_commit = Gitlab::Git::Commit.new(container.repository.raw, hash)
  new(raw_commit, container)
end

.lazy(container, oid) ⇒ Object


119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'app/models/commit.rb', line 119

def lazy(container, oid)
  BatchLoader.for({ container: container, oid: oid }).batch do |items, loader|
    items_by_container = items.group_by { |i| i[:container] }

    items_by_container.each do |container, commit_ids|
      oids = commit_ids.map { |i| i[:oid] }

      container.repository.commits_by(oids: oids).each do |commit|
        loader.call({ container: commit.container, oid: commit.id }, commit) if commit
      end
    end
  end
end

191
192
193
194
# File 'app/models/commit.rb', line 191

def self.link_reference_pattern
  @link_reference_pattern ||=
    super("commit", /(?<commit>#{COMMIT_SHA_PATTERN})?(\.(?<extension>#{LINK_EXTENSION_PATTERN}))?/)
end

.max_diff_optionsObject


95
96
97
98
99
100
# File 'app/models/commit.rb', line 95

def max_diff_options
  {
    max_files: diff_max_files,
    max_lines: diff_max_lines
  }
end

.order_by(collection:, order_by:, sort:) ⇒ Object


67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'app/models/commit.rb', line 67

def order_by(collection:, order_by:, sort:)
  return collection unless %w[email name commits].include?(order_by)
  return collection unless %w[asc desc].include?(sort)

  collection.sort do |a, b|
    operands = [a, b].tap { |o| o.reverse! if sort == 'desc' }

    attr1 = operands.first.public_send(order_by) # rubocop:disable GitlabSecurity/PublicSend
    attr2 = operands.second.public_send(order_by) # rubocop:disable GitlabSecurity/PublicSend

    # use case insensitive comparison for string values
    order_by.in?(%w[email name]) ? attr1.casecmp(attr2) : attr1 <=> attr2
  end
end

.parent_classObject


133
134
135
# File 'app/models/commit.rb', line 133

def parent_class
  ::Project
end

.reference_patternObject

Pattern used to extract commit references from text

This pattern supports cross-project references.


184
185
186
187
188
189
# File 'app/models/commit.rb', line 184

def self.reference_pattern
  @reference_pattern ||= %r{
    (?:#{Project.reference_pattern}#{reference_prefix})?
    (?<commit>#{COMMIT_SHA_PATTERN})
  }x
end

.reference_prefixObject


173
174
175
# File 'app/models/commit.rb', line 173

def self.reference_prefix
  '@'
end

.reference_valid?(reference) ⇒ Boolean

Returns:

  • (Boolean)

177
178
179
# File 'app/models/commit.rb', line 177

def self.reference_valid?(reference)
  !!(reference =~ EXACT_COMMIT_SHA_PATTERN)
end

.truncate_sha(sha) ⇒ Object

Truncate sha to 8 characters


83
84
85
# File 'app/models/commit.rb', line 83

def truncate_sha(sha)
  sha[0..MIN_SHA_LENGTH]
end

.valid_hash?(key) ⇒ Boolean

Returns:

  • (Boolean)

115
116
117
# File 'app/models/commit.rb', line 115

def valid_hash?(key)
  !!(EXACT_COMMIT_SHA_PATTERN =~ key)
end

Instance Method Details

#==(other) ⇒ Object


169
170
171
# File 'app/models/commit.rb', line 169

def ==(other)
  other.is_a?(self.class) && raw == other.raw
end

#authorObject


289
290
291
292
293
# File 'app/models/commit.rb', line 289

def author
  strong_memoize(:author) do
    lazy_author&.itself
  end
end

#author_full_textObject


232
233
234
235
236
237
238
# File 'app/models/commit.rb', line 232

def author_full_text
  return unless author_name && author_email

  strong_memoize(:author_full_text) do
    "#{author_name} <#{author_email}>"
  end
end

#cache_keyObject


526
527
528
# File 'app/models/commit.rb', line 526

def cache_key
  "commit:#{sha}"
end

#change_type_title(user) ⇒ Object


456
457
458
# File 'app/models/commit.rb', line 456

def change_type_title(user)
  merged_merge_request?(user) ? 'merge request' : 'commit'
end

#cherry_pick_branch_nameObject


390
391
392
# File 'app/models/commit.rb', line 390

def cherry_pick_branch_name
  repository.next_branch("cherry-pick-#{short_id}", mild: true)
end

#cherry_pick_description(user) ⇒ Object


394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
# File 'app/models/commit.rb', line 394

def cherry_pick_description(user)
  message_body = ["(cherry picked from commit #{sha})"]

  if merged_merge_request?(user)
    commits_in_merge_request = merged_merge_request(user).commits

    if commits_in_merge_request.present?
      message_body << ""

      commits_in_merge_request.reverse_each do |commit_in_merge|
        message_body << "#{commit_in_merge.short_id} #{commit_in_merge.title}"
      end
    end
  end

  message_body.join("\n")
end

#cherry_pick_message(user) ⇒ Object


412
413
414
# File 'app/models/commit.rb', line 412

def cherry_pick_message(user)
  %Q{#{message}\n\n#{cherry_pick_description(user)}}
end

#committer(confirmed: true) ⇒ Object


296
297
298
# File 'app/models/commit.rb', line 296

def committer(confirmed: true)
  @committer ||= User.find_by_any_email(committer_email, confirmed: confirmed)
end

#descriptionObject

Returns full commit message if title is truncated (greater than 99 characters) otherwise returns commit message without first line


242
243
244
245
246
247
# File 'app/models/commit.rb', line 242

def description
  return safe_message if full_title.length >= 100
  return no_commit_message if safe_message.blank?

  safe_message.split("\n", 2)[1].try(:chomp)
end

#description?Boolean

Returns:

  • (Boolean)

249
250
251
# File 'app/models/commit.rb', line 249

def description?
  description.present?
end

#diff_line_countObject


204
205
206
207
# File 'app/models/commit.rb', line 204

def diff_line_count
  @diff_line_count ||= Commit.diff_line_count(raw_diffs)
  @diff_line_count
end

#diff_refsObject


338
339
340
341
342
343
# File 'app/models/commit.rb', line 338

def diff_refs
  Gitlab::Diff::DiffRefs.new(
    base_sha: self.parent_id || Gitlab::Git::BLANK_SHA,
    head_sha: self.sha
  )
end

#diffs(diff_options = {}) ⇒ Object


494
495
496
# File 'app/models/commit.rb', line 494

def diffs(diff_options = {})
  Gitlab::Diff::FileCollection::Commit.new(self, diff_options: diff_options)
end

#discussion_notesObject


318
319
320
# File 'app/models/commit.rb', line 318

def discussion_notes
  notes.non_diff_notes
end

#expire_note_etag_cacheObject


530
531
532
533
534
# File 'app/models/commit.rb', line 530

def expire_note_etag_cache
  super

  expire_note_etag_cache_for_related_mrs
end

#full_titleObject

Returns the full commits title


223
224
225
226
227
228
229
230
# File 'app/models/commit.rb', line 223

def full_title
  @full_title ||=
    if safe_message.blank?
      no_commit_message
    else
      safe_message.split(/[\r\n]/, 2).first
    end
end

#gpg_commitObject


382
383
384
# File 'app/models/commit.rb', line 382

def gpg_commit
  @gpg_commit ||= Gitlab::Gpg::Commit.new(self)
end

#has_been_reverted?(current_user, notes_association = nil) ⇒ Boolean

Returns:

  • (Boolean)

445
446
447
448
449
450
451
452
453
454
# File 'app/models/commit.rb', line 445

def has_been_reverted?(current_user, notes_association = nil)
  ext = Gitlab::ReferenceExtractor.new(project, current_user)
  notes_association ||= notes_with_associations

  notes_association.system.each do |note|
    note.all_references(current_user, extractor: ext)
  end

  ext.commits.any? { |commit_ref| commit_ref.reverts_commit?(self, current_user) }
end

#has_signature?Boolean

Returns:

  • (Boolean)

345
346
347
# File 'app/models/commit.rb', line 345

def has_signature?
  signature_type && signature_type != :NONE
end

#hook_attrs(with_changed_files: false) ⇒ Object


257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
# File 'app/models/commit.rb', line 257

def hook_attrs(with_changed_files: false)
  data = {
    id: id,
    message: safe_message,
    title: title,
    timestamp: timestamp,
    url: Gitlab::UrlBuilder.build(self),
    author: {
      name: author_name,
      email: author_email
    }
  }

  if with_changed_files
    data.merge!(repo_changes)
  end

  data
end

#idObject


161
162
163
# File 'app/models/commit.rb', line 161

def id
  raw.id
end

#lazy_authorObject


277
278
279
280
281
282
283
284
285
286
287
# File 'app/models/commit.rb', line 277

def lazy_author
  BatchLoader.for(author_email.downcase).batch do |emails, loader|
    users = User.by_any_email(emails, confirmed: true).includes(:emails)

    emails.each do |email|
      user = users.find { |u| u.any_email?(email) }

      loader.call(email, user)
    end
  end
end

#merge_commit?Boolean

Returns:

  • (Boolean)

432
433
434
# File 'app/models/commit.rb', line 432

def merge_commit?
  parent_ids.size > 1
end

#merged_merge_request(current_user) ⇒ Object


436
437
438
439
440
441
442
443
# File 'app/models/commit.rb', line 436

def merged_merge_request(current_user)
  # Memoize with per-user access check
  @merged_merge_request_hash ||= Hash.new do |hash, user|
    hash[user] = merged_merge_request_no_cache(user)
  end

  @merged_merge_request_hash[current_user]
end

#merged_merge_request?(user) ⇒ Boolean

Returns:

  • (Boolean)

522
523
524
# File 'app/models/commit.rb', line 522

def merged_merge_request?(user)
  !!merged_merge_request(user)
end

#notesObject


310
311
312
# File 'app/models/commit.rb', line 310

def notes
  container.notes.for_commit_id(self.id)
end

#notes_with_associationsObject


322
323
324
# File 'app/models/commit.rb', line 322

def notes_with_associations
  notes.includes(:author, :award_emoji)
end

#parentObject


304
305
306
307
308
# File 'app/models/commit.rb', line 304

def parent
  strong_memoize(:parent) do
    container.commit_by(oid: self.parent_id) if self.parent_id
  end
end

#parentsObject


300
301
302
# File 'app/models/commit.rb', line 300

def parents
  @parents ||= parent_ids.map { |oid| Commit.lazy(container, oid) }
end

#persisted?Boolean

Returns:

  • (Boolean)

498
499
500
# File 'app/models/commit.rb', line 498

def persisted?
  true
end

#project_idObject


165
166
167
# File 'app/models/commit.rb', line 165

def project_id
  project&.id
end

#raw_commit_from_rugged?Boolean

Returns:

  • (Boolean)

378
379
380
# File 'app/models/commit.rb', line 378

def raw_commit_from_rugged?
  @raw.raw_commit.is_a?(Rugged::Commit)
end

#raw_deltasObject


490
491
492
# File 'app/models/commit.rb', line 490

def raw_deltas
  @deltas ||= raw.deltas
end

#raw_diffs(*args) ⇒ Object


486
487
488
# File 'app/models/commit.rb', line 486

def raw_diffs(*args)
  raw.diffs(*args)
end

#raw_signature_typeObject


349
350
351
352
353
354
355
356
357
358
359
# File 'app/models/commit.rb', line 349

def raw_signature_type
  strong_memoize(:raw_signature_type) do
    next unless @raw.instance_of?(Gitlab::Git::Commit)

    if raw_commit_from_rugged? && gpg_commit.signature_text.present?
      :PGP
    elsif defined? @raw.raw_commit.signature_type
      @raw.raw_commit.signature_type
    end
  end
end

#readable_by?(user) ⇒ Boolean

Returns:

  • (Boolean)

536
537
538
# File 'app/models/commit.rb', line 536

def readable_by?(user)
  Ability.allowed?(user, :read_commit, self)
end

200
201
202
# File 'app/models/commit.rb', line 200

def reference_link_text(from = nil, full: false)
  commit_reference(from, short_id, full: full)
end

#respond_to_missing?(method, include_private = false) ⇒ Boolean

Returns:

  • (Boolean)

330
331
332
# File 'app/models/commit.rb', line 330

def respond_to_missing?(method, include_private = false)
  @raw.respond_to?(method, include_private) || super
end

#revert_branch_nameObject


386
387
388
# File 'app/models/commit.rb', line 386

def revert_branch_name
  "revert-#{short_id}"
end

#revert_description(user) ⇒ Object


416
417
418
419
420
421
422
# File 'app/models/commit.rb', line 416

def revert_description(user)
  if merged_merge_request?(user)
    "This reverts merge request #{merged_merge_request(user).to_reference}"
  else
    "This reverts commit #{sha}"
  end
end

#revert_message(user) ⇒ Object


424
425
426
# File 'app/models/commit.rb', line 424

def revert_message(user)
  %Q{Revert "#{title.strip}"\n\n#{revert_description(user)}}
end

#reverts_commit?(commit, user) ⇒ Boolean

Returns:

  • (Boolean)

428
429
430
# File 'app/models/commit.rb', line 428

def reverts_commit?(commit, user)
  description? && description.include?(commit.revert_description(user))
end

#short_idObject


334
335
336
# File 'app/models/commit.rb', line 334

def short_id
  @raw.short_id(MIN_SHA_LENGTH)
end

#signatureObject


365
366
367
368
369
370
371
372
373
374
375
376
# File 'app/models/commit.rb', line 365

def signature
  strong_memoize(:signature) do
    case signature_type
    when :PGP
      gpg_commit.signature
    when :X509
      Gitlab::X509::Commit.new(self).signature
    else
      nil
    end
  end
end

#signature_typeObject


361
362
363
# File 'app/models/commit.rb', line 361

def signature_type
  @signature_type ||= raw_signature_type || :NONE
end

#timestampObject


253
254
255
# File 'app/models/commit.rb', line 253

def timestamp
  committed_date.xmlschema
end

#titleObject

Returns the commits title.

Usually, the commit title is the first line of the commit message. In case this first line is longer than 100 characters, it is cut off after 80 characters + `…`


214
215
216
217
218
219
220
# File 'app/models/commit.rb', line 214

def title
  return full_title if full_title.length < 100

  # Use three dots instead of the ellipsis Unicode character because
  # some clients show the raw Unicode value in the merge commit.
  full_title.truncate(81, separator: ' ', omission: '...')
end

#to_ability_nameObject


502
503
504
# File 'app/models/commit.rb', line 502

def to_ability_name
  model_name.singular
end

#to_reference(from = nil, full: false) ⇒ Object


196
197
198
# File 'app/models/commit.rb', line 196

def to_reference(from = nil, full: false)
  commit_reference(from, id, full: full)
end

#touchObject


506
507
508
# File 'app/models/commit.rb', line 506

def touch
  # no-op but needs to be defined since #persisted? is defined
end

#touch_laterObject


510
511
512
513
514
# File 'app/models/commit.rb', line 510

def touch_later
  # No-op.
  # This method is called by ActiveRecord.
  # We don't want to do anything for `Commit` model, so this is empty.
end

#uri_type(path) ⇒ Object

Get the URI type of the given path

Used to build URLs to files in the repository in GFM.

path - String path to check

Examples:

uri_type('doc/README.md') # => :blob
uri_type('doc/logo.png')  # => :raw
uri_type('doc/api')       # => :tree
uri_type('not/found')     # => nil

Returns a symbol


474
475
476
477
478
479
480
481
482
483
484
# File 'app/models/commit.rb', line 474

def uri_type(path)
  entry = @raw.tree_entry(path)
  return unless entry

  if entry[:type] == :blob
    blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: entry[:name]), container)
    blob.image? || blob.video? || blob.audio? ? :raw : :blob
  else
    entry[:type]
  end
end

#user_mention_classObject


541
542
543
# File 'app/models/commit.rb', line 541

def user_mention_class
  CommitUserMention
end

#user_mention_identifierObject


546
547
548
549
550
551
# File 'app/models/commit.rb', line 546

def user_mention_identifier
  {
    commit_id: id,
    note_id: nil
  }
end

#user_mentionsObject


314
315
316
# File 'app/models/commit.rb', line 314

def user_mentions
  user_mention_class.where(commit_id: self.id)
end

#with_pipelineObject


157
158
159
# File 'app/models/commit.rb', line 157

def with_pipeline
  @with_pipeline ||= Ci::CommitWithPipeline.new(self)
end

#work_in_progress?Boolean

Returns:

  • (Boolean)

518
519
520
# File 'app/models/commit.rb', line 518

def work_in_progress?
  !!(title =~ DRAFT_REGEX)
end