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