commit.rb 8.65 KB
Newer Older
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
1
class Commit
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
2
  extend ActiveModel::Naming
3
4

  include ActiveModel::Conversion
5
  include Participable
6
  include Mentionable
7
8
  include Referable
  include StaticModel
9

10
  attr_mentionable :safe_message, pipeline: :single_line
Yorick Peterse's avatar
Yorick Peterse committed
11
12
13
14

  participant :author
  participant :committer
  participant :notes_with_associations
Saito's avatar
Saito committed
15

16
17
  attr_accessor :project

18
  DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines]
19

20
  # Commits above this size will not be rendered in HTML
21
22
  DIFF_HARD_LIMIT_FILES = 1000
  DIFF_HARD_LIMIT_LINES = 50000
23

24
  # The SHA can be between 7 and 40 hex characters.
Douwe Maan's avatar
Douwe Maan committed
25
  COMMIT_SHA_PATTERN = '\h{7,40}'.freeze
26

27
  class << self
28
    def decorate(commits, project)
29
30
31
32
      commits.map do |commit|
        if commit.kind_of?(Commit)
          commit
        else
33
          self.new(commit, project)
34
35
        end
      end
36
    end
37

38
39
    # Calculate number of lines to render for diffs
    def diff_line_count(diffs)
40
      diffs.reduce(0) { |sum, d| sum + Gitlab::Git::Util.count_lines(d.diff) }
41
    end
42

43
    # Truncate sha to 8 characters
44
    def truncate_sha(sha)
45
      sha[0..7]
46
    end
47
48
49
50
51
52
53

    def max_diff_options
      {
        max_files: DIFF_HARD_LIMIT_FILES,
        max_lines: DIFF_HARD_LIMIT_LINES,
      }
    end
54
55
56
57

    def from_hash(hash, project)
      new(Gitlab::Git::Commit.new(hash), project)
    end
58
59
60
61

    def valid_hash?(key)
      !!(/\A#{COMMIT_SHA_PATTERN}\z/ =~ key)
    end
62
63
  end

64
  attr_accessor :raw
Nihad Abbasov's avatar
Nihad Abbasov committed
65

66
  def initialize(raw_commit, project)
67
68
    raise "Nil as raw commit passed" unless raw_commit

69
    @raw = raw_commit
70
    @project = project
71
  end
72

73
74
75
76
  def id
    @raw.id
  end

Robert Speicher's avatar
Robert Speicher committed
77
78
79
80
  def ==(other)
    (self.class === other) && (raw == other.raw)
  end

81
82
83
84
85
86
87
88
  def self.reference_prefix
    '@'
  end

  # Pattern used to extract commit references from text
  #
  # This pattern supports cross-project references.
  def self.reference_pattern
89
    @reference_pattern ||= %r{
90
      (?:#{Project.reference_pattern}#{reference_prefix})?
91
      (?<commit>\h{7,40})
92
    }x
93
94
  end

95
  def self.link_reference_pattern
96
    @link_reference_pattern ||= super("commit", /(?<commit>#{COMMIT_SHA_PATTERN})/)
97
98
  end

99
100
  def to_reference(from_project = nil, full: false)
    commit_reference(from_project, id, full: full)
101
102
  end

103
104
  def reference_link_text(from_project = nil, full: false)
    commit_reference(from_project, short_id, full: full)
105
106
  end

107
  def diff_line_count
108
    @diff_line_count ||= Commit::diff_line_count(raw_diffs)
109
110
111
    @diff_line_count
  end

112
113
114
  # Returns the commits title.
  #
  # Usually, the commit title is the first line of the commit message.
115
116
  # In case this first line is longer than 100 characters, it is cut off
  # after 80 characters and ellipses (`&hellp;`) are appended.
117
  def title
118
119
    full_title.length > 100 ? full_title[0..79] << "…" : full_title
  end
120

121
122
123
  # Returns the full commits title
  def full_title
    return @full_title if @full_title
124

125
126
    if safe_message.blank?
      @full_title = no_commit_message
127
    else
128
      @full_title = safe_message.split("\n", 2).first
129
130
131
132
133
134
135
    end
  end

  # Returns the commits description
  #
  # cut off, ellipses (`&hellp;`) are prepended to the commit message.
  def description
136
    title_end = safe_message.index("\n")
137
138
    @description ||=
      if (!title_end && safe_message.length > 100) || (title_end && title_end > 100)
139
        "…" << safe_message[80..-1]
140
141
142
      else
        safe_message.split("\n", 2)[1].try(:chomp)
      end
143
  end
144

145
146
  def description?
    description.present?
147
148
  end

Valery Sizov's avatar
Valery Sizov committed
149
  def hook_attrs(with_changed_files: false)
Valery Sizov's avatar
Valery Sizov committed
150
    data = {
Kirill Zaitsev's avatar
Kirill Zaitsev committed
151
152
153
      id: id,
      message: safe_message,
      timestamp: committed_date.xmlschema,
154
      url: Gitlab::UrlBuilder.build(self),
Kirill Zaitsev's avatar
Kirill Zaitsev committed
155
156
157
158
159
      author: {
        name: author_name,
        email: author_email
      }
    }
Valery Sizov's avatar
Valery Sizov committed
160
161

    if with_changed_files
Valery Sizov's avatar
Valery Sizov committed
162
      data.merge!(repo_changes)
Valery Sizov's avatar
Valery Sizov committed
163
164
165
    end

    data
Kirill Zaitsev's avatar
Kirill Zaitsev committed
166
167
  end

168
169
  # Discover issues should be closed when this commit is pushed to a project's
  # default branch.
170
  def closes_issues(current_user = self.committer)
Douwe Maan's avatar
Douwe Maan committed
171
    Gitlab::ClosingIssueExtractor.new(project, current_user).closed_by_message(safe_message)
172
173
  end

Douwe Maan's avatar
Douwe Maan committed
174
  def author
Stan Hu's avatar
Stan Hu committed
175
176
177
178
179
180
181
182
183
184
185
    if RequestStore.active?
      key = "commit_author:#{author_email.downcase}"
      # nil is a valid value since no author may exist in the system
      if RequestStore.store.has_key?(key)
        @author = RequestStore.store[key]
      else
        @author = find_author_by_any_email
        RequestStore.store[key] = @author
      end
    else
      @author ||= find_author_by_any_email
186
    end
Douwe Maan's avatar
Douwe Maan committed
187
188
189
  end

  def committer
190
    @committer ||= User.find_by_any_email(committer_email.downcase)
Douwe Maan's avatar
Douwe Maan committed
191
192
  end

193
194
195
196
197
198
199
200
  def parents
    @parents ||= parent_ids.map { |id| project.commit(id) }
  end

  def parent
    @parent ||= project.commit(self.parent_id) if self.parent_id
  end

201
  def notes
202
203
204
    project.notes.for_commit_id(self.id)
  end

Yorick Peterse's avatar
Yorick Peterse committed
205
  def notes_with_associations
206
    notes.includes(:author)
Yorick Peterse's avatar
Yorick Peterse committed
207
208
  end

209
210
  def method_missing(m, *args, &block)
    @raw.send(m, *args, &block)
211
  end
212

213
214
  def respond_to_missing?(method, include_private = false)
    @raw.respond_to?(method, include_private) || super
215
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
216

217
218
219
220
221
  # Truncate sha to 8 characters
  def short_id
    @raw.short_id(7)
  end

222
223
  def diff_refs
    Gitlab::Diff::DiffRefs.new(
224
      base_sha: self.parent_id || Gitlab::Git::BLANK_SHA,
225
226
227
228
      head_sha: self.sha
    )
  end

229
  def pipelines
230
    project.pipelines.where(sha: sha)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
231
232
  end

233
  def status(ref = nil)
234
235
    @statuses ||= {}

236
237
    return @statuses[ref] if @statuses.key?(ref)

238
    @statuses[ref] = pipelines.latest_status(ref)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
239
  end
240

241
  def revert_branch_name
242
    "revert-#{short_id}"
243
  end
Yorick Peterse's avatar
Yorick Peterse committed
244

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

249
250
251
  def revert_description(user)
    if merged_merge_request?(user)
      "This reverts merge request #{merged_merge_request(user).to_reference}"
252
253
254
255
256
    else
      "This reverts commit #{sha}"
    end
  end

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

261
262
  def reverts_commit?(commit, user)
    description? && description.include?(commit.revert_description(user))
263
264
  end

265
  def merge_commit?
266
267
268
    parents.size > 1
  end

269
270
271
272
273
  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
274

275
    @merged_merge_request_hash[current_user]
276
277
  end

278
  def has_been_reverted?(current_user, noteable = self)
Yorick Peterse's avatar
Yorick Peterse committed
279
280
281
282
283
284
    ext = all_references(current_user)

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

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

288
289
  def change_type_title(user)
    merged_merge_request?(user) ? 'merge request' : 'commit'
290
291
  end

292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
  # 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
  def uri_type(path)
    entry = @raw.tree.path(path)
    if entry[:type] == :blob
309
310
      blob = ::Blob.decorate(Gitlab::Git::Blob.new(name: entry[:name]))
      blob.image? || blob.video? ? :raw : :blob
311
312
313
314
315
316
317
    else
      entry[:type]
    end
  rescue Rugged::TreeError
    nil
  end

318
319
320
321
322
  def raw_diffs(*args)
    raw.diffs(*args)
  end

  def diffs(diff_options = nil)
323
324
325
    Gitlab::Diff::FileCollection::Commit.new(self, diff_options: diff_options)
  end

P.S.V.R's avatar
P.S.V.R committed
326
327
328
329
330
331
332
333
  def persisted?
    true
  end

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

334
335
336
337
338
339
  WIP_REGEX = /\A\s*(((?i)(\[WIP\]|WIP:|WIP)\s|WIP$))|(fixup!|squash!)\s/.freeze

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

340
341
  private

342
343
  def commit_reference(from_project, referable_commit_id, full: false)
    reference = project.to_reference(from_project, full: full)
344
345
346
347
348
349
350
351

    if reference.present?
      "#{reference}#{self.class.reference_prefix}#{referable_commit_id}"
    else
      referable_commit_id
    end
  end

Stan Hu's avatar
Stan Hu committed
352
353
354
355
  def find_author_by_any_email
    User.find_by_any_email(author_email.downcase)
  end

356
357
358
  def repo_changes
    changes = { added: [], modified: [], removed: [] }

Ahmad Sherif's avatar
Ahmad Sherif committed
359
    raw_diffs(deltas_only: true).each do |diff|
Valery Sizov's avatar
Valery Sizov committed
360
361
362
363
364
365
      if diff.deleted_file
        changes[:removed] << diff.old_path
      elsif diff.renamed_file || diff.new_file
        changes[:added] << diff.new_path
      else
        changes[:modified] << diff.new_path
366
367
368
369
370
      end
    end

    changes
  end
371
372
373
374
375
376
377
378

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

  def merged_merge_request_no_cache(user)
    MergeRequestsFinder.new(user, project_id: project.id).find_by(merge_commit_sha: id) if merge_commit?
  end
Dmitriy Zaporozhets's avatar
Dmitriy Zaporozhets committed
379
end