merge_request_diff.rb 8.14 KB
Newer Older
1
class MergeRequestDiff < ActiveRecord::Base
2
  include Sortable
3
  include Importable
4
  include ManualInverseAssociation
5
  include IgnorableColumn
6

7
  # Don't display more than 100 commits at once
8
  COMMITS_SAFE_SIZE = 100
9

10
11
  ignore_column :st_commits,
                :st_diffs
12

13
  belongs_to :merge_request
14
15
  manual_inverse_association :merge_request, :merge_request_diff

16
  has_many :merge_request_diff_files, -> { order(:merge_request_diff_id, :relative_order) }
17
  has_many :merge_request_diff_commits, -> { order(:merge_request_diff_id, :relative_order) }
18
19
20

  state_machine :state, initial: :empty do
    state :collected
21
22
23
    state :overflow
    # Deprecated states: these are no longer used but these values may still occur
    # in the database.
24
25
26
27
28
29
    state :timeout
    state :overflow_commits_safe_size
    state :overflow_diff_files_limit
    state :overflow_diff_lines_limit
  end

30
  scope :viewable, -> { without_state(:empty) }
31
32
33
  scope :by_commit_sha, ->(sha) do
    joins(:merge_request_diff_commits).where(merge_request_diff_commits: { sha: sha }).reorder(nil)
  end
34

35
36
  scope :recent, -> { order(id: :desc).limit(100) }

37
38
  # All diff information is collected from repository after object is created.
  # It allows you to override variables like head_commit_sha before getting diff.
39
  after_create :save_git_content, unless: :importing?
40

Douwe Maan's avatar
Douwe Maan committed
41
  def self.find_by_diff_refs(diff_refs)
Douwe Maan's avatar
Douwe Maan committed
42
    find_by(start_commit_sha: diff_refs.start_sha, head_commit_sha: diff_refs.head_sha, base_commit_sha: diff_refs.base_sha)
Douwe Maan's avatar
Douwe Maan committed
43
44
  end

45
46
47
  # Collect information about commits and diff from repository
  # and save it to the database as serialized data
  def save_git_content
48
49
50
51
    MergeRequest
      .where('id = ? AND COALESCE(latest_merge_request_diff_id, 0) < ?', self.merge_request_id, self.id)
      .update_all(latest_merge_request_diff_id: self.id)

52
    ensure_commit_shas
53
54
    save_commits
    save_diffs
55
    save
56
57
58
    keep_around_commits
  end

59
  def ensure_commit_shas
60
    self.start_commit_sha ||= merge_request.target_branch_sha
61
    self.head_commit_sha  ||= merge_request.source_branch_sha
62
    self.base_commit_sha  ||= find_base_sha
63
64
65
66
67
68
  end

  # Override head_commit_sha to keep compatibility with merge request diff
  # created before version 8.4 that does not store head_commit_sha in separate db field.
  def head_commit_sha
    if persisted? && super.nil?
69
      last_commit_sha
70
71
72
    else
      super
    end
73
74
  end

75
76
77
78
79
80
81
  # This method will rely on repository branch sha
  # in case start_commit_sha is nil. Its necesarry for old merge request diff
  # created before version 8.4 to work
  def safe_start_commit_sha
    start_commit_sha || merge_request.target_branch_sha
  end

82
  def size
83
    real_size.presence || raw_diffs.size
84
85
  end

86
  def raw_diffs(options = {})
87
    if options[:ignore_whitespace_change]
88
      @diffs_no_whitespace ||= compare.diffs(options)
89
    else
90
      @raw_diffs ||= {}
91
      @raw_diffs[options] ||= load_diffs(options)
92
    end
93
94
  end

95
  def commits
96
    @commits ||= load_commits
97
98
  end

99
100
  def last_commit_sha
    commit_shas.first
101
102
  end

103
104
105
106
  def first_commit
    commits.last
  end

107
  def base_commit
108
    return unless base_commit_sha
109

110
    project.commit_by(oid: base_commit_sha)
111
112
  end

113
  def start_commit
114
    return unless start_commit_sha
115

116
    project.commit_by(oid: start_commit_sha)
117
118
119
  end

  def head_commit
120
    return unless head_commit_sha
121

122
    project.commit_by(oid: head_commit_sha)
123
124
  end

125
  def commit_shas
126
    merge_request_diff_commits.map(&:sha)
127
128
  end

Douwe Maan's avatar
Douwe Maan committed
129
  def diff_refs=(new_diff_refs)
Douwe Maan's avatar
Douwe Maan committed
130
131
132
    self.base_commit_sha = new_diff_refs&.base_sha
    self.start_commit_sha = new_diff_refs&.start_sha
    self.head_commit_sha = new_diff_refs&.head_sha
Douwe Maan's avatar
Douwe Maan committed
133
134
  end

135
  def diff_refs
136
    return unless start_commit_sha || base_commit_sha
137
138
139
140
141
142
143
144

    Gitlab::Diff::DiffRefs.new(
      base_sha:  base_commit_sha,
      start_sha: start_commit_sha,
      head_sha:  head_commit_sha
    )
  end

145
146
  # MRs created before 8.4 don't store their true diff refs (start and base),
  # but we need to get a commit SHA for the "View file @ ..." link by a file,
Douwe Maan's avatar
Douwe Maan committed
147
148
  # so we use an approximation of the diff refs if we can't get the actual one.
  #
149
150
151
  # These will not be the actual diff refs if the target branch was merged into
  # the source branch after the merge request was created, but it is good enough
  # for the specific purpose of linking to a commit.
Douwe Maan's avatar
Douwe Maan committed
152
  #
153
154
155
  # It is not good enough for highlighting diffs, so we can't simply pass
  # these as `diff_refs.`
  def fallback_diff_refs
156
157
158
    real_refs = diff_refs
    return real_refs if real_refs

159
160
161
162
163
164
165
166
167
    likely_base_commit_sha = (first_commit&.parent || first_commit)&.sha

    Gitlab::Diff::DiffRefs.new(
      base_sha:  likely_base_commit_sha,
      start_sha: safe_start_commit_sha,
      head_sha:  head_commit_sha
    )
  end

168
169
170
171
  def diff_refs_by_sha?
    base_commit_sha? && head_commit_sha? && start_commit_sha?
  end

172
173
174
175
176
177
178
  def diffs(diff_options = nil)
    Gitlab::Diff::FileCollection::MergeRequestDiff.new(self, diff_options: diff_options)
  end

  def project
    merge_request.target_project
  end
179

180
181
  def compare
    @compare ||=
182
183
184
185
186
      Gitlab::Git::Compare.new(
        repository.raw_repository,
        safe_start_commit_sha,
        head_commit_sha
      )
187
188
  end

189
  def latest?
190
    self.id == merge_request.latest_merge_request_diff_id
191
192
  end

193
  def compare_with(sha)
194
    # When compare merge request versions we want diff A..B instead of A...B
195
    # so we handle cases when user does squash and rebase of the commits between versions.
196
    # For this reason we set straight to true by default.
197
    CompareService.new(project, head_commit_sha).execute(project, sha, straight: true)
198
199
  end

200
201
  private

202
203
  def create_merge_request_diff_files(diffs)
    rows = diffs.map.with_index do |diff, index|
204
205
      diff_hash = diff.to_hash.merge(
        binary: false,
206
207
208
        merge_request_diff_id: self.id,
        relative_order: index
      )
209
210
211
212
213
214
215
216
217
218

      # Compatibility with old diffs created with Psych.
      diff_hash.tap do |hash|
        diff_text = hash[:diff]

        if diff_text.encoding == Encoding::BINARY && !diff_text.ascii_only?
          hash[:binary] = true
          hash[:diff] = [diff_text].pack('m0')
        end
      end
219
    end
220
221

    Gitlab::Database.bulk_insert('merge_request_diff_files', rows)
222
223
  end

224
  def load_diffs(options)
225
    raw = merge_request_diff_files.map(&:to_hash)
226
227
228
229
230

    if paths = options[:paths]
      raw = raw.select do |diff|
        paths.include?(diff[:old_path]) || paths.include?(diff[:new_path])
      end
231
    end
232
233
234
235

    Gitlab::Git::DiffCollection.new(raw, options)
  end

236
  def load_commits
237
    commits = merge_request_diff_commits.map { |commit| Commit.from_hash(commit.to_hash, project) }
238

239
240
    CommitCollection
      .new(merge_request.source_project, commits, merge_request.source_branch)
241
242
  end

243
  def save_diffs
244
    new_attributes = {}
245

246
    if compare.commits.size.zero?
247
      new_attributes[:state] = :empty
248
    else
249
      diff_collection = compare.diffs(Commit.max_diff_options)
250
      new_attributes[:real_size] = diff_collection.real_size
251

252
      if diff_collection.any?
253
        new_attributes[:state] = :collected
254

255
256
        create_merge_request_diff_files(diff_collection)
      end
257
258
259
260
261
262
263

      # Set our state to 'overflow' to make the #empty? and #collected?
      # methods (generated by StateMachine) return false.
      #
      # This attribution has to come at the end of the method so 'overflow'
      # state does not get overridden by 'collected'.
      new_attributes[:state] = :overflow if diff_collection.overflow?
264
    end
265

266
    assign_attributes(new_attributes)
267
268
269
270
271
  end

  def save_commits
    MergeRequestDiffCommit.create_bulk(self.id, compare.commits.reverse)

272
273
274
275
    # merge_request_diff_commits.reload is preferred way to reload associated
    # objects but it returns cached result for some reason in this case
    commits = merge_request_diff_commits(true)
    self.commits_count = commits.size
276
277
278
  end

  def repository
279
    project.repository
280
  end
281

282
283
  def find_base_sha
    return unless head_commit_sha && start_commit_sha
284

285
    project.merge_base_commit(head_commit_sha, start_commit_sha).try(:sha)
286
287
  end

288
  def keep_around_commits
289
    [repository, merge_request.source_project.repository].uniq.each do |repo|
290
291
292
293
      repo.keep_around(start_commit_sha)
      repo.keep_around(head_commit_sha)
      repo.keep_around(base_commit_sha)
    end
294
  end
295
end