Completed
Push — master ( 8c9cef...d36a0f )
by Ricardo
04:29
created

Commit::repository()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
/**
4
 * This file is part of Fabrica.
5
 *
6
 * (c) Alexandre Salomé <[email protected]>
7
 * (c) Julien DIDIER <[email protected]>
8
 *
9
 * This source file is subject to the GPL license that is bundled
10
 * with this source code in the file LICENSE.
11
 */
12
13
namespace Fabrica\Models\Code;
14
15
use Pedreiro\Models\Base;
16
17
class Commit extends Base
18
{
19
20
    public static $apresentationName = 'Commits';
21
22
    protected $organizationPerspective = true;
23
24
    protected $table = 'repository_commits';
25
26
    /**
27
     * The attributes that are mass assignable.
28
     *
29
     * @var array
30
     */
31
    protected $fillable = [
32
        'code',
33
        'date',
34
        'author',
35
        'message',
36
        'reference'
37
    ];
38
39
    public function stayInBranchOfAmbiente(Ambiente $ambiente)
40
    {
41
        // @todo
42
        return true;
43
    }
44
45
    public function getApresentationName()
46
    {
47
        return 'Numero do Commit';
48
    }
49
50
    public function repository()
51
    {
52
        return $this->belongsTo(Repository::class, 'repository_id', 'id');
53
    }
54
55
56
    /*# coding: utf-8
57
    # frozen_string_literal: true
58
59
    class Commit
60
    extend ActiveModel::Naming
61
    extend Gitlab::Cache::RequestCache
62
63
    include ActiveModel::Conversion
64
    include Noteable
65
    include Participable
66
    include Mentionable
67
    include Referable
68
    include StaticModel
69
    include Presentable
70
    include ::Gitlab::Utils::StrongMemoize
71
72
    attr_mentionable :safe_message, pipeline: :single_line
73
74
    participant :author
75
    participant :committer
76
    participant :notes_with_associations
77
78
    attr_accessor :project, :author
79
    attr_accessor :redacted_description_html
80
    attr_accessor :redacted_title_html
81
    attr_accessor :redacted_full_title_html
82
    attr_reader :gpg_commit
83
84
    DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines]
85
86
    # Commits above this size will not be rendered in HTML
87
    DIFF_HARD_LIMIT_FILES = 1000
88
    DIFF_HARD_LIMIT_LINES = 50000
89
90
    MIN_SHA_LENGTH = Gitlab::Git::Commit::MIN_SHA_LENGTH
91
    COMMIT_SHA_PATTERN = /\h{#{MIN_SHA_LENGTH},40}/.freeze
92
    # Used by GFM to match and present link extensions on node texts and hrefs.
93
    LINK_EXTENSION_PATTERN = /(patch)/.freeze
94
95
    def banzai_render_context(field)
96
    pipeline = field == :description ? :commit_description : :single_line
97
    context = { pipeline: pipeline, project: self.project }
98
    context[:author] = self.author if self.author
99
100
    context
101
    end
102
103
    class << self
104
    def decorate(commits, project)
105
      commits.map do |commit|
106
        if commit.is_a?(Commit)
107
          commit
108
        else
109
          self.new(commit, project)
110
        end
111
      end
112
    end
113
114
    # Calculate number of lines to render for diffs
115
    def diff_line_count(diffs)
116
      diffs.reduce(0) { |sum, d| sum + Gitlab::Git::Util.count_lines(d.diff) }
117
    end
118
119
    def order_by(collection:, order_by:, sort:)
120
      return collection unless %w[email name commits].include?(order_by)
121
      return collection unless %w[asc desc].include?(sort)
122
123
      collection.sort do |a, b|
124
        operands = [a, b].tap { |o| o.reverse! if sort == 'desc' }
125
126
        attr1, attr2 = operands.first.public_send(order_by), operands.second.public_send(order_by) # rubocop:disable PublicSend
127
128
        # use case insensitive comparison for string values
129
        order_by.in?(%w[email name]) ? attr1.casecmp(attr2) : attr1 <=> attr2
130
      end
131
    end
132
133
    # Truncate sha to 8 characters
134
    def truncate_sha(sha)
135
      sha[0..MIN_SHA_LENGTH]
136
    end
137
138
    def max_diff_options
139
      {
140
        max_files: DIFF_HARD_LIMIT_FILES,
141
        max_lines: DIFF_HARD_LIMIT_LINES
142
      }
143
    end
144
145
    def from_hash(hash, project)
146
      raw_commit = Gitlab::Git::Commit.new(project.repository.raw, hash)
147
      new(raw_commit, project)
148
    end
149
150
    def valid_hash?(key)
151
      !!(/\A#{COMMIT_SHA_PATTERN}\z/ =~ key)
152
    end
153
154
    def lazy(project, oid)
155
      BatchLoader.for({ project: project, oid: oid }).batch do |items, loader|
156
        items_by_project = items.group_by { |i| i[:project] }
157
158
        items_by_project.each do |project, commit_ids|
159
          oids = commit_ids.map { |i| i[:oid] }
160
161
          project.repository.commits_by(oids: oids).each do |commit|
162
            loader.call({ project: commit.project, oid: commit.id }, commit) if commit
163
          end
164
        end
165
      end
166
    end
167
168
    def parent_class
169
      ::Project
170
    end
171
    end
172
173
    attr_accessor :raw
174
175
    def initialize(raw_commit, project)
176
    raise "Nil as raw commit passed" unless raw_commit
177
178
    @raw = raw_commit
179
    @project = project
180
    @statuses = {}
181
    @gpg_commit = Gitlab::Gpg::Commit.new(self) if project
182
    end
183
184
    def id
185
    raw.id
186
    end
187
188
    def project_id
189
    project.id
190
    end
191
192
    def ==(other)
193
    other.is_a?(self.class) && raw == other.raw
194
    end
195
196
    def self.reference_prefix
197
    '@'
198
    end
199
200
    # Pattern used to extract commit references from text
201
    #
202
    # This pattern supports cross-project references.
203
    def self.reference_pattern
204
    @reference_pattern ||= %r{
205
      (?:#{Project.reference_pattern}#{reference_prefix})?
206
      (?<commit>#{COMMIT_SHA_PATTERN})
207
    }x
208
    end
209
210
    def self.link_reference_pattern
211
    @link_reference_pattern ||=
212
      super("commit", /(?<commit>#{COMMIT_SHA_PATTERN})?(\.(?<extension>#{LINK_EXTENSION_PATTERN}))?/)
213
    end
214
215
    def to_reference(from = nil, full: false)
216
    commit_reference(from, id, full: full)
217
    end
218
219
    def reference_link_text(from = nil, full: false)
220
    commit_reference(from, short_id, full: full)
221
    end
222
223
    def diff_line_count
224
    @diff_line_count ||= Commit.diff_line_count(raw_diffs)
225
    @diff_line_count
226
    end
227
228
    # Returns the commits title.
229
    #
230
    # Usually, the commit title is the first line of the commit message.
231
    # In case this first line is longer than 100 characters, it is cut off
232
    # after 80 characters + `...`
233
    def title
234
    return full_title if full_title.length < 100
235
236
    # Use three dots instead of the ellipsis Unicode character because
237
    # some clients show the raw Unicode value in the merge commit.
238
    full_title.truncate(81, separator: ' ', omission: '...')
239
    end
240
241
    # Returns the full commits title
242
    def full_title
243
    @full_title ||=
244
      if safe_message.blank?
245
        no_commit_message
246
      else
247
        safe_message.split(/[\r\n]/, 2).first
248
      end
249
    end
250
251
    # Returns full commit message if title is truncated (greater than 99 characters)
252
    # otherwise returns commit message without first line
253
    def description
254
    return safe_message if full_title.length >= 100
255
    return no_commit_message if safe_message.blank?
256
257
    safe_message.split("\n", 2)[1].try(:chomp)
258
    end
259
260
    def description?
261
    description.present?
262
    end
263
264
    def hook_attrs(with_changed_files: false)
265
    data = {
266
      id: id,
267
      message: safe_message,
268
      timestamp: committed_date.xmlschema,
269
      url: Gitlab::UrlBuilder.build(self),
270
      author: {
271
        name: author_name,
272
        email: author_email
273
      }
274
    }
275
276
    if with_changed_files
277
      data.merge!(repo_changes)
278
    end
279
280
    data
281
    end
282
283
    # Discover issues should be closed when this commit is pushed to a project's
284
    # default branch.
285
    def closes_issues(current_user = self.committer)
286
    Gitlab::ClosingIssueExtractor.new(project, current_user).closed_by_message(safe_message)
287
    end
288
289
    def lazy_author
290
    BatchLoader.for(author_email.downcase).batch do |emails, loader|
291
      users = User.by_any_email(emails).includes(:emails)
292
293
      emails.each do |email|
294
        user = users.find { |u| u.any_email?(email) }
295
296
        loader.call(email, user)
297
      end
298
    end
299
    end
300
301
    def author
302
    # We use __sync so that we get the actual objects back (including an actual
303
    # nil), instead of a wrapper, as returning a wrapped nil breaks a lot of
304
    # code.
305
    lazy_author.__sync
306
    end
307
    request_cache(:author) { author_email.downcase }
308
309
    def committer
310
    @committer ||= User.find_by_any_email(committer_email)
311
    end
312
313
    def parents
314
    @parents ||= parent_ids.map { |oid| Commit.lazy(project, oid) }
315
    end
316
317
    def parent
318
    strong_memoize(:parent) do
319
      project.commit_by(oid: self.parent_id) if self.parent_id
320
    end
321
    end
322
323
    def notes
324
    project.notes.for_commit_id(self.id)
325
    end
326
327
    def discussion_notes
328
    notes.non_diff_notes
329
    end
330
331
    def notes_with_associations
332
    notes.includes(:author, :award_emoji)
333
    end
334
335
    def merge_requests
336
    @merge_requests ||= project.merge_requests.by_commit_sha(sha)
337
    end
338
339
    def method_missing(method, *args, &block)
340
    @raw.__send__(method, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
341
    end
342
343
    def respond_to_missing?(method, include_private = false)
344
    @raw.respond_to?(method, include_private) || super
345
    end
346
347
    def short_id
348
    @raw.short_id(MIN_SHA_LENGTH)
349
    end
350
351
    def diff_refs
352
    Gitlab::Diff::DiffRefs.new(
353
      base_sha: self.parent_id || Gitlab::Git::BLANK_SHA,
354
      head_sha: self.sha
355
    )
356
    end
357
358
    def pipelines
359
    project.ci_pipelines.where(sha: sha)
360
    end
361
362
    def last_pipeline
363
    strong_memoize(:last_pipeline) do
364
      pipelines.last
365
    end
366
    end
367
368
    def status(ref = nil)
369
    return @statuses[ref] if @statuses.key?(ref)
370
371
    @statuses[ref] = status_for_project(ref, project)
372
    end
373
374
    def status_for_project(ref, pipeline_project)
375
    pipeline_project.ci_pipelines.latest_status_per_commit(id, ref)[id]
376
    end
377
378
    def set_status_for_ref(ref, status)
379
    @statuses[ref] = status
380
    end
381
382
    def signature
383
    return @signature if defined?(@signature)
384
385
    @signature = gpg_commit.signature
386
    end
387
388
    delegate :has_signature?, to: :gpg_commit
389
390
    def revert_branch_name
391
    "revert-#{short_id}"
392
    end
393
394
    def cherry_pick_branch_name
395
    project.repository.next_branch("cherry-pick-#{short_id}", mild: true)
396
    end
397
398
    def cherry_pick_description(user)
399
    message_body = ["(cherry picked from commit #{sha})"]
400
401
    if merged_merge_request?(user)
402
      commits_in_merge_request = merged_merge_request(user).commits
403
404
      if commits_in_merge_request.present?
405
        message_body << ""
406
407
        commits_in_merge_request.reverse.each do |commit_in_merge|
408
          message_body << "#{commit_in_merge.short_id} #{commit_in_merge.title}"
409
        end
410
      end
411
    end
412
413
    message_body.join("\n")
414
    end
415
416
    def cherry_pick_message(user)
417
    %Q{#{message}\n\n#{cherry_pick_description(user)}}
418
    end
419
420
    def revert_description(user)
421
    if merged_merge_request?(user)
422
      "This reverts merge request #{merged_merge_request(user).to_reference}"
423
    else
424
      "This reverts commit #{sha}"
425
    end
426
    end
427
428
    def revert_message(user)
429
    %Q{Revert "#{title.strip}"\n\n#{revert_description(user)}}
430
    end
431
432
    def reverts_commit?(commit, user)
433
    description? && description.include?(commit.revert_description(user))
434
    end
435
436
    def merge_commit?
437
    parent_ids.size > 1
438
    end
439
440
    def merged_merge_request(current_user)
441
    # Memoize with per-user access check
442
    @merged_merge_request_hash ||= Hash.new do |hash, user|
443
      hash[user] = merged_merge_request_no_cache(user)
444
    end
445
446
    @merged_merge_request_hash[current_user]
447
    end
448
449
    def has_been_reverted?(current_user, notes_association = nil)
450
    ext = all_references(current_user)
451
    notes_association ||= notes_with_associations
452
453
    notes_association.system.each do |note|
454
      note.all_references(current_user, extractor: ext)
455
    end
456
457
    ext.commits.any? { |commit_ref| commit_ref.reverts_commit?(self, current_user) }
458
    end
459
460
    def change_type_title(user)
461
    merged_merge_request?(user) ? 'merge request' : 'commit'
462
    end
463
464
    # Get the URI type of the given path
465
    #
466
    # Used to build URLs to files in the repository in GFM.
467
    #
468
    # path - String path to check
469
    #
470
    # Examples:
471
    #
472
    #   uri_type('doc/README.md') # => :blob
473
    #   uri_type('doc/logo.png')  # => :raw
474
    #   uri_type('doc/api')       # => :tree
475
    #   uri_type('not/found')     # => nil
476
    #
477
    # Returns a symbol
478
    def uri_type(path)
479
    entry = @raw.tree_entry(path)
480
    return unless entry
481
482
    if entry[:type] == :blob
483
      blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: entry[:name]), @project)
484
      blob.image? || blob.video? ? :raw : :blob
485
    else
486
      entry[:type]
487
    end
488
    end
489
490
    def raw_diffs(*args)
491
    raw.diffs(*args)
492
    end
493
494
    def raw_deltas
495
    @deltas ||= raw.deltas
496
    end
497
498
    def diffs(diff_options = {})
499
    Gitlab::Diff::FileCollection::Commit.new(self, diff_options: diff_options)
500
    end
501
502
    def persisted?
503
    true
504
    end
505
506
    def to_ability_name
507
    model_name.singular
508
    end
509
510
    def touch
511
    # no-op but needs to be defined since #persisted? is defined
512
    end
513
514
    def touch_later
515
    # No-op.
516
    # This method is called by ActiveRecord.
517
    # We don't want to do anything for `Commit` model, so this is empty.
518
    end
519
520
    WIP_REGEX = /\A\s*(((?i)(\[WIP\]|WIP:|WIP)\s|WIP$))|(fixup!|squash!)\s/.freeze
521
522
    def work_in_progress?
523
    !!(title =~ WIP_REGEX)
524
    end
525
526
    def merged_merge_request?(user)
527
    !!merged_merge_request(user)
528
    end
529
530
    def cache_key
531
    "commit:#{sha}"
532
    end
533
534
    private
535
536
    def commit_reference(from, referable_commit_id, full: false)
537
    reference = project.to_reference(from, full: full)
538
539
    if reference.present?
540
      "#{reference}#{self.class.reference_prefix}#{referable_commit_id}"
541
    else
542
      referable_commit_id
543
    end
544
    end
545
546
    def repo_changes
547
    changes = { added: [], modified: [], removed: [] }
548
549
    raw_deltas.each do |diff|
550
      if diff.deleted_file
551
        changes[:removed] << diff.old_path
552
      elsif diff.renamed_file || diff.new_file
553
        changes[:added] << diff.new_path
554
      else
555
        changes[:modified] << diff.new_path
556
      end
557
    end
558
559
    changes
560
    end
561
562
    def merged_merge_request_no_cache(user)
563
    MergeRequestsFinder.new(user, project_id: project.id).find_by(merge_commit_sha: id) if merge_commit?
564
    end
565
    end
566
    */
567
}
568