Class: Commit

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

Constant Summary collapse

DIFF_SAFE_LINES =
Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines]
DIFF_HARD_LIMIT_FILES =

Commits above this size will not be rendered in HTML

1000
DIFF_HARD_LIMIT_LINES =
50000
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
DRAFT_REGEX =

WIP is deprecated in favor of Draft. Currently both options are supported gitlab.com/gitlab-org/gitlab/-/issues/227426

/\A\s*#{Regexp.union(Gitlab::Regex.merge_request_wip, 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 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, #parent_user, #refresh_markdown_cache, #refresh_markdown_cache!, #rendered_field_content, #skip_project_check?, #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, #store_mentions!

Methods included from Participable

#participants

Methods included from Noteable

#after_note_created, #after_note_destroyed, #base_class_name, #capped_notes_count, #discussion_ids_relation, #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, #preloads_discussion_diff_highlighting?, #resolvable_discussions, #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.


120
121
122
123
124
125
# File 'app/models/commit.rb', line 120

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


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

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


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

def container
  @container
end

#rawObject

Returns the value of attribute raw


118
119
120
# File 'app/models/commit.rb', line 118

def raw
  @raw
end

#redacted_description_htmlObject

Returns the value of attribute redacted_description_html


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

def redacted_description_html
  @redacted_description_html
end

#redacted_full_title_htmlObject

Returns the value of attribute redacted_full_title_html


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

def redacted_full_title_html
  @redacted_full_title_html
end

#redacted_title_htmlObject

Returns the value of attribute redacted_title_html


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

def redacted_title_html
  @redacted_title_html
end

Class Method Details

.decorate(commits, container) ⇒ Object


49
50
51
52
53
54
55
56
57
# File 'app/models/commit.rb', line 49

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


60
61
62
# File 'app/models/commit.rb', line 60

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

.from_hash(hash, container) ⇒ Object


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

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

.lazy(container, oid) ⇒ Object


99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'app/models/commit.rb', line 99

def lazy(container, oid)
  BatchLoader.for({ container: container, oid: oid }).batch(replace_methods: false) 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

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

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

.max_diff_optionsObject


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

def max_diff_options
  {
    max_files: DIFF_HARD_LIMIT_FILES,
    max_lines: DIFF_HARD_LIMIT_LINES
  }
end

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


64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'app/models/commit.rb', line 64

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, attr2 = operands.first.public_send(order_by), operands.second.public_send(order_by) # rubocop:disable PublicSend

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

.parent_classObject


113
114
115
# File 'app/models/commit.rb', line 113

def parent_class
  ::Project
end

.reference_patternObject

Pattern used to extract commit references from text

This pattern supports cross-project references.


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

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

.reference_prefixObject


152
153
154
# File 'app/models/commit.rb', line 152

def self.reference_prefix
  '@'
end

.reference_valid?(reference) ⇒ Boolean

Returns:

  • (Boolean)

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

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

.truncate_sha(sha) ⇒ Object

Truncate sha to 8 characters


79
80
81
# File 'app/models/commit.rb', line 79

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

.valid_hash?(key) ⇒ Boolean

Returns:

  • (Boolean)

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

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

Instance Method Details

#==(other) ⇒ Object


148
149
150
# File 'app/models/commit.rb', line 148

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

#authorObject


260
261
262
263
264
# File 'app/models/commit.rb', line 260

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

#cache_keyObject


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

def cache_key
  "commit:#{sha}"
end

#change_type_title(user) ⇒ Object


415
416
417
# File 'app/models/commit.rb', line 415

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

#cherry_pick_branch_nameObject


349
350
351
# File 'app/models/commit.rb', line 349

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

#cherry_pick_description(user) ⇒ Object


353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
# File 'app/models/commit.rb', line 353

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


371
372
373
# File 'app/models/commit.rb', line 371

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

#committer(confirmed: true) ⇒ Object


267
268
269
# File 'app/models/commit.rb', line 267

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


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

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)

220
221
222
# File 'app/models/commit.rb', line 220

def description?
  description.present?
end

#diff_line_countObject


183
184
185
186
# File 'app/models/commit.rb', line 183

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

#diff_refsObject


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

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


453
454
455
# File 'app/models/commit.rb', line 453

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

#discussion_notesObject


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

def discussion_notes
  notes.non_diff_notes
end

#expire_note_etag_cacheObject


491
492
493
494
495
# File 'app/models/commit.rb', line 491

def expire_note_etag_cache
  super

  expire_note_etag_cache_for_related_mrs
end

#full_titleObject

Returns the full commits title


202
203
204
205
206
207
208
209
# File 'app/models/commit.rb', line 202

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

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

Returns:

  • (Boolean)

404
405
406
407
408
409
410
411
412
413
# File 'app/models/commit.rb', line 404

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)

316
317
318
# File 'app/models/commit.rb', line 316

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

#hook_attrs(with_changed_files: false) ⇒ Object


228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'app/models/commit.rb', line 228

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


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

def id
  raw.id
end

#lazy_authorObject


248
249
250
251
252
253
254
255
256
257
258
# File 'app/models/commit.rb', line 248

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)

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

def merge_commit?
  parent_ids.size > 1
end

#merged_merge_request(current_user) ⇒ Object


395
396
397
398
399
400
401
402
# File 'app/models/commit.rb', line 395

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)

483
484
485
# File 'app/models/commit.rb', line 483

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

#notesObject


281
282
283
# File 'app/models/commit.rb', line 281

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

#notes_with_associationsObject


293
294
295
# File 'app/models/commit.rb', line 293

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

#parentObject


275
276
277
278
279
# File 'app/models/commit.rb', line 275

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

#parentsObject


271
272
273
# File 'app/models/commit.rb', line 271

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

#persisted?Boolean

Returns:

  • (Boolean)

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

def persisted?
  true
end

#project_idObject


144
145
146
# File 'app/models/commit.rb', line 144

def project_id
  project&.id
end

#raw_deltasObject


449
450
451
# File 'app/models/commit.rb', line 449

def raw_deltas
  @deltas ||= raw.deltas
end

#raw_diffs(*args) ⇒ Object


445
446
447
# File 'app/models/commit.rb', line 445

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

#raw_signature_typeObject


320
321
322
323
324
325
326
# File 'app/models/commit.rb', line 320

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

    @raw.raw_commit.signature_type if defined? @raw.raw_commit.signature_type
  end
end

179
180
181
# File 'app/models/commit.rb', line 179

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)

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

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

#revert_branch_nameObject


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

def revert_branch_name
  "revert-#{short_id}"
end

#revert_description(user) ⇒ Object


375
376
377
378
379
380
381
# File 'app/models/commit.rb', line 375

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


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

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

#reverts_commit?(commit, user) ⇒ Boolean

Returns:

  • (Boolean)

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

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

#short_idObject


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

def short_id
  @raw.short_id(MIN_SHA_LENGTH)
end

#signatureObject


332
333
334
335
336
337
338
339
340
341
342
343
# File 'app/models/commit.rb', line 332

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

#signature_typeObject


328
329
330
# File 'app/models/commit.rb', line 328

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

#timestampObject


224
225
226
# File 'app/models/commit.rb', line 224

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 + `…`


193
194
195
196
197
198
199
# File 'app/models/commit.rb', line 193

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


461
462
463
# File 'app/models/commit.rb', line 461

def to_ability_name
  model_name.singular
end

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


175
176
177
# File 'app/models/commit.rb', line 175

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

#touchObject


465
466
467
# File 'app/models/commit.rb', line 465

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

#touch_laterObject


469
470
471
472
473
# File 'app/models/commit.rb', line 469

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


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

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_mentionsObject


285
286
287
# File 'app/models/commit.rb', line 285

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

#with_pipelineObject


136
137
138
# File 'app/models/commit.rb', line 136

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

#work_in_progress?Boolean

Returns:

  • (Boolean)

479
480
481
# File 'app/models/commit.rb', line 479

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