Class: Gitlab::Diff::File

Inherits:
Object
  • Object
show all
Includes:
Utils::StrongMemoize
Defined in:
lib/gitlab/diff/file.rb

Direct Known Subclasses

Rendered::Notebook::DiffFile

Constant Summary collapse

RICH_VIEWERS =

Finding a viewer for a diff file happens based only on extension and whether the diff file blobs are binary or text, which means 1 diff file should only be matched by 1 viewer, and the order of these viewers doesn’t really matter.

However, when the diff file blobs are LFS pointers, we cannot know for sure whether the file being pointed to is binary or text. In this case, we match only on extension, preferring binary viewers over text ones if both exist, since the large files referred to in “Large File Storage” are much more likely to be binary than text.

[
  DiffViewer::Image
].sort_by { |v| v.binary? ? 0 : 1 }.freeze
ROWS_CONTENT_VISIBILITY_THRESHOLD =

Diff file with more than 200 diff line rows could slow down the page interactions we enable content visibility on every row if it reaches this threshold to reduce diff impact on page reflows

200

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(diff, repository:, diff_refs: nil, fallback_diff_refs: nil, stats: nil, unique_identifier: nil, max_blob_size: nil) ⇒ File

Returns a new instance of File.



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/gitlab/diff/file.rb', line 33

def initialize(
  diff,
  repository:,
  diff_refs: nil,
  fallback_diff_refs: nil,
  stats: nil,
  unique_identifier: nil,
  max_blob_size: nil
)
  @diff = diff
  @stats = stats
  @repository = repository
  @diff_refs = diff_refs
  @fallback_diff_refs = fallback_diff_refs
  @unique_identifier = unique_identifier
  @max_blob_size = max_blob_size
  @unfolded = false
  @linked = false

  # Ensure items are collected in the the batch
  add_blobs_to_batch_loader
end

Instance Attribute Details

#diffObject (readonly)

Returns the value of attribute diff.



8
9
10
# File 'lib/gitlab/diff/file.rb', line 8

def diff
  @diff
end

#diff_refsObject (readonly)

Returns the value of attribute diff_refs.



8
9
10
# File 'lib/gitlab/diff/file.rb', line 8

def diff_refs
  @diff_refs
end

#fallback_diff_refsObject (readonly)

Returns the value of attribute fallback_diff_refs.



8
9
10
# File 'lib/gitlab/diff/file.rb', line 8

def fallback_diff_refs
  @fallback_diff_refs
end

#linkedObject

Returns the value of attribute linked.



9
10
11
# File 'lib/gitlab/diff/file.rb', line 9

def linked
  @linked
end

#max_blob_sizeObject (readonly)

Returns the value of attribute max_blob_size.



8
9
10
# File 'lib/gitlab/diff/file.rb', line 8

def max_blob_size
  @max_blob_size
end

#repositoryObject (readonly)

Returns the value of attribute repository.



8
9
10
# File 'lib/gitlab/diff/file.rb', line 8

def repository
  @repository
end

#unique_identifierObject (readonly)

Returns the value of attribute unique_identifier.



8
9
10
# File 'lib/gitlab/diff/file.rb', line 8

def unique_identifier
  @unique_identifier
end

Instance Method Details

#add_blobs_to_batch_loaderObject



407
408
409
410
# File 'lib/gitlab/diff/file.rb', line 407

def add_blobs_to_batch_loader
  new_blob_lazy
  old_blob_lazy
end

#added_linesObject



250
251
252
253
254
# File 'lib/gitlab/diff/file.rb', line 250

def added_lines
  strong_memoize(:added_lines) do
    @stats&.additions || diff_lines.count(&:added?)
  end
end

#ai_reviewable?Boolean

Returns:

  • (Boolean)


412
413
414
# File 'lib/gitlab/diff/file.rb', line 412

def ai_reviewable?
  diffable? && text?
end

#alternate_viewerObject



349
350
351
# File 'lib/gitlab/diff/file.rb', line 349

def alternate_viewer
  alternate_viewer_class&.new(self)
end

#binary?Boolean

Returns:

  • (Boolean)


323
324
325
326
327
# File 'lib/gitlab/diff/file.rb', line 323

def binary?
  strong_memoize(:is_binary) do
    try_blobs(:binary?)
  end
end

#binary_in_repo?Boolean

Returns:

  • (Boolean)


279
280
281
# File 'lib/gitlab/diff/file.rb', line 279

def binary_in_repo?
  has_binary_notice? || try_blobs(:binary_in_repo?)
end

#blobObject



171
172
173
# File 'lib/gitlab/diff/file.rb', line 171

def blob
  new_blob || old_blob
end

#code_review_idObject



270
271
272
# File 'lib/gitlab/diff/file.rb', line 270

def code_review_id
  Digest::SHA1.hexdigest("#{file_identifier}-#{blob&.id}")
end

#content_changed?Boolean

Returns:

  • (Boolean)


299
300
301
302
303
304
305
# File 'lib/gitlab/diff/file.rb', line 299

def content_changed?
  return blobs_changed? if diff_refs && new_blob

  return false if new_file? || deleted_file? || renamed_file?

  text? && diff_lines.any?
end

#content_shaObject



167
168
169
# File 'lib/gitlab/diff/file.rb', line 167

def content_sha
  new_content_sha || old_content_sha
end

#diff_hunk(diff_line) ⇒ Object

Returns the raw diff content up to the given line index



112
113
114
115
116
117
118
119
# File 'lib/gitlab/diff/file.rb', line 112

def diff_hunk(diff_line)
  diff_line_index = diff_line.index
  # @@ (match) header is not kept if it's found in the top of the file,
  # therefore we should keep an extra line on this scenario.
  diff_line_index += 1 unless diff_lines.first.match?

  diff_lines.select { |line| line.index <= diff_line_index }.map(&:text).join("\n")
end

#diff_linesObject

Array of Gitlab::Diff::Line objects



184
185
186
187
# File 'lib/gitlab/diff/file.rb', line 184

def diff_lines
  @diff_lines ||=
    Gitlab::Diff::Parser.new.parse(raw_diff.each_line, diff_file: self).to_a
end

#diff_lines_for_serializerObject

This adds the bottom match line to the array if needed. It contains the data to load more context lines.



359
360
361
362
363
364
# File 'lib/gitlab/diff/file.rb', line 359

def diff_lines_for_serializer
  lines = diff_lines_with_match_tail
  return if lines.empty?

  lines
end

#diff_lines_with_match_tailObject



366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
# File 'lib/gitlab/diff/file.rb', line 366

def diff_lines_with_match_tail
  lines = highlighted_diff_lines

  return [] if lines.empty?
  return [] if blob.nil?

  last_line = lines.last

  if last_line.new_pos < total_blob_lines(blob) && !deleted_file?
    match_line = Gitlab::Diff::Line.new("", 'match', nil, last_line.old_pos, last_line.new_pos)
    lines.push(match_line)
  end

  lines
end

#diffable?Boolean

Returns:

  • (Boolean)


275
276
277
# File 'lib/gitlab/diff/file.rb', line 275

def diffable?
  diffable_by_attribute? && !text_with_binary_notice?
end

#diffable_text?Boolean

Returns:

  • (Boolean)


424
425
426
# File 'lib/gitlab/diff/file.rb', line 424

def diffable_text?
  !too_large? && diffable? && text? && !whitespace_only?
end

#different_type?Boolean

Returns:

  • (Boolean)


307
308
309
# File 'lib/gitlab/diff/file.rb', line 307

def different_type?
  old_blob && new_blob && old_blob.binary? != new_blob.binary?
end

#empty?Boolean

Returns:

  • (Boolean)


319
320
321
# File 'lib/gitlab/diff/file.rb', line 319

def empty?
  valid_blobs.map(&:empty?).all?
end

#expand_to_full!Object

Expands the diff to show the complete file content with changes merged in. This replicates the frontend convertExpandLines behavior from legacy diffs, but performs the merging on the backend for rapid diffs rendering.



441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
# File 'lib/gitlab/diff/file.rb', line 441

def expand_to_full!
  return if blob.binary_in_repo?

  presenter = Blobs::UnfoldPresenter.new(blob, { full: true })
  blob_lines = presenter.diff_lines(with_positions_and_indent: true).to_a

  merged_lines = []

  diff_lines_with_match_tail.each_with_index do |line, index|
    if line.type == 'match'
      prev_line = index == 0 ? nil : diff_lines_with_match_tail[index - 1]
      next_line = diff_lines_with_match_tail[index + 1]
      start_index = prev_line ? prev_line.new_pos : 0
      end_index = next_line ? next_line.new_pos - 1 : blob_lines.count
      expanded_lines = blob_lines[start_index..end_index]
      if prev_line
        expanded_lines.each_with_index do |expanded_line, line_index|
          expanded_line.old_pos = prev_line.old_pos + 1 + line_index
          expanded_line.new_pos = prev_line.new_pos + 1 + line_index
        end
      end

      merged_lines.concat(expanded_lines)
    else
      merged_lines << line
    end
  end

  self.highlighted_diff_lines = merged_lines

  @expanded_to_full = true
end

#external_storageObject



295
296
297
# File 'lib/gitlab/diff/file.rb', line 295

def external_storage
  try_blobs(:external_storage)
end

#external_storage_error?Boolean

Returns:

  • (Boolean)


287
288
289
# File 'lib/gitlab/diff/file.rb', line 287

def external_storage_error?
  try_blobs(:external_storage_error?)
end

#file_hashObject



245
246
247
# File 'lib/gitlab/diff/file.rb', line 245

def file_hash
  Digest::SHA1.hexdigest(file_path)
end

#file_identifierObject



262
263
264
# File 'lib/gitlab/diff/file.rb', line 262

def file_identifier
  "#{file_path}-#{new_file?}-#{deleted_file?}-#{renamed_file?}"
end

#file_identifier_hashObject



266
267
268
# File 'lib/gitlab/diff/file.rb', line 266

def file_identifier_hash
  Digest::SHA1.hexdigest(file_identifier)
end

#file_pathObject



241
242
243
# File 'lib/gitlab/diff/file.rb', line 241

def file_path
  new_path.presence || old_path
end

#fully_expanded?Boolean

Returns:

  • (Boolean)


383
384
385
386
387
388
389
390
391
# File 'lib/gitlab/diff/file.rb', line 383

def fully_expanded?
  return true if binary?

  lines = diff_lines_for_serializer

  return true if lines.nil?

  lines.none? { |line| line.type.to_s == 'match' }
end

#has_renderable?Boolean

Returns:

  • (Boolean)


56
57
58
# File 'lib/gitlab/diff/file.rb', line 56

def has_renderable?
  rendered&.has_renderable?
end

#highlight_loaded?Boolean

Returns:

  • (Boolean)


211
212
213
# File 'lib/gitlab/diff/file.rb', line 211

def highlight_loaded?
  @highlighted_diff_lines.present?
end

#highlighted_diff_linesObject



215
216
217
218
# File 'lib/gitlab/diff/file.rb', line 215

def highlighted_diff_lines
  @highlighted_diff_lines ||=
    Gitlab::Diff::Highlight.new(self, repository: self.repository).highlight
end

#highlighted_diff_lines=(value) ⇒ Object



175
176
177
178
179
180
181
# File 'lib/gitlab/diff/file.rb', line 175

def highlighted_diff_lines=(value)
  clear_memoization(:diff_lines_for_serializer)

  # Clear match tail cache as highlighted lines have changed
  clear_memoization(:diff_lines_with_match_tail)
  @highlighted_diff_lines = value
end

#image_diff?Boolean

Returns:

  • (Boolean)


432
433
434
435
436
# File 'lib/gitlab/diff/file.rb', line 432

def image_diff?
  return false if different_type? || external_storage_error?

  DiffViewer::Image.can_render?(self, verify_binary: !stored_externally?)
end

#ipynb?Boolean

Returns:

  • (Boolean)


403
404
405
# File 'lib/gitlab/diff/file.rb', line 403

def ipynb?
  file_path.ends_with?('.ipynb')
end

#line_code(line) ⇒ Object



79
80
81
# File 'lib/gitlab/diff/file.rb', line 79

def line_code(line)
  line.legacy_id(file_path)
end

#line_code_for_position(pos) ⇒ Object



106
107
108
109
# File 'lib/gitlab/diff/file.rb', line 106

def line_code_for_position(pos)
  line = line_for_position(pos)
  line_code(line) if line
end

#line_for_line_code(code) ⇒ Object



83
84
85
# File 'lib/gitlab/diff/file.rb', line 83

def line_for_line_code(code)
  diff_lines.find { |line| line_code(line) == code }
end

#line_for_position(pos) ⇒ Object



87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/gitlab/diff/file.rb', line 87

def line_for_position(pos)
  return unless pos.position_type == 'text'

  # This method is normally used to find which line the diff was
  # commented on, and in this context, it's normally the raw diff persisted
  # at `note_diff_files`, which is a fraction of the entire diff
  # (it goes from the first line, to the commented line, or
  # one line below). Therefore it's more performant to fetch
  # from bottom to top instead of the other way around.
  diff_lines
    .reverse_each
    .find { |line| line.old_line == pos.old_line && line.new_line == pos.new_line }
end

#manually_expanded?Boolean

Returns:

  • (Boolean)


474
475
476
# File 'lib/gitlab/diff/file.rb', line 474

def manually_expanded?
  @expanded_to_full || false
end

#modified_file?Boolean

Returns:

  • (Boolean)


416
417
418
# File 'lib/gitlab/diff/file.rb', line 416

def modified_file?
  new_file? || deleted_file? || content_changed?
end

#new_blobObject



145
146
147
148
149
# File 'lib/gitlab/diff/file.rb', line 145

def new_blob
  strong_memoize(:new_blob) do
    new_blob_lazy&.itself
  end
end

#new_blob_lines_between(from_line, to_line) ⇒ Object



157
158
159
160
161
162
163
164
165
# File 'lib/gitlab/diff/file.rb', line 157

def new_blob_lines_between(from_line, to_line)
  return [] unless new_blob

  from_index = from_line - 1
  to_index = to_line - 1

  new_blob.load_all_data!
  new_blob.data.lines[from_index..to_index]
end

#new_content_shaObject



129
130
131
132
133
134
135
# File 'lib/gitlab/diff/file.rb', line 129

def new_content_sha
  return if deleted_file?
  return @new_content_sha if defined?(@new_content_sha)

  refs = diff_refs || fallback_diff_refs
  @new_content_sha = refs&.head_sha
end

#new_shaObject



125
126
127
# File 'lib/gitlab/diff/file.rb', line 125

def new_sha
  diff_refs&.head_sha
end

#next_line(index) ⇒ Object



229
230
231
# File 'lib/gitlab/diff/file.rb', line 229

def next_line(index)
  diff_lines[index + 1]
end

#no_preview?Boolean

Returns:

  • (Boolean)


420
421
422
# File 'lib/gitlab/diff/file.rb', line 420

def no_preview?
  collapsed? || !modified_file? || (empty? && !content_changed? && !submodule?)
end

#old_blobObject



151
152
153
154
155
# File 'lib/gitlab/diff/file.rb', line 151

def old_blob
  strong_memoize(:old_blob) do
    old_blob_lazy&.itself
  end
end

#old_content_shaObject



137
138
139
140
141
142
143
# File 'lib/gitlab/diff/file.rb', line 137

def old_content_sha
  return if new_file?
  return @old_content_sha if defined?(@old_content_sha)

  refs = diff_refs || fallback_diff_refs
  @old_content_sha = refs&.base_sha
end

#old_shaObject



121
122
123
# File 'lib/gitlab/diff/file.rb', line 121

def old_sha
  diff_refs&.base_sha
end

#parallel_diff_linesObject

Array with right/left keys that contains Gitlab::Diff::Line objects which text is highlighted



221
222
223
# File 'lib/gitlab/diff/file.rb', line 221

def parallel_diff_lines
  @parallel_diff_lines ||= Gitlab::Diff::ParallelDiff.new(self).parallelize
end

#pathsObject



237
238
239
# File 'lib/gitlab/diff/file.rb', line 237

def paths
  [old_path, new_path].compact
end

#position(position_marker, position_type: :text) ⇒ Object



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/gitlab/diff/file.rb', line 60

def position(position_marker, position_type: :text)
  return unless diff_refs

  data = {
    diff_refs: diff_refs,
    position_type: position_type.to_s,
    old_path: old_path,
    new_path: new_path
  }

  if position_type == :text
    data.merge!(text_position_properties(position_marker))
  else
    data.merge!(image_position_properties(position_marker))
  end

  Position.new(data)
end

#position_for_line_code(code) ⇒ Object



101
102
103
104
# File 'lib/gitlab/diff/file.rb', line 101

def position_for_line_code(code)
  line = line_for_line_code(code)
  position(line) if line
end

#prev_line(index) ⇒ Object



233
234
235
# File 'lib/gitlab/diff/file.rb', line 233

def prev_line(index)
  diff_lines[index - 1] if index > 0
end

#raw_diffObject



225
226
227
# File 'lib/gitlab/diff/file.rb', line 225

def raw_diff
  diff.diff.to_s
end

#raw_sizeObject



315
316
317
# File 'lib/gitlab/diff/file.rb', line 315

def raw_size
  valid_blobs.sum(&:raw_size)
end

#removed_linesObject



256
257
258
259
260
# File 'lib/gitlab/diff/file.rb', line 256

def removed_lines
  strong_memoize(:removed_lines) do
    @stats&.deletions || diff_lines.count(&:removed?)
  end
end

#renderedObject



393
394
395
396
397
# File 'lib/gitlab/diff/file.rb', line 393

def rendered
  return unless ipynb? && modified_file? && !collapsed? && !too_large?

  strong_memoize(:rendered) { Rendered::Notebook::DiffFile.new(self) }
end

#rendered?Boolean

Returns:

  • (Boolean)


399
400
401
# File 'lib/gitlab/diff/file.rb', line 399

def rendered?
  false
end

#rendered_as_text?(ignore_errors: true) ⇒ Boolean

Returns:

  • (Boolean)


353
354
355
# File 'lib/gitlab/diff/file.rb', line 353

def rendered_as_text?(ignore_errors: true)
  simple_viewer.is_a?(DiffViewer::Text) && (ignore_errors || simple_viewer.render_error.nil?)
end

#rich_viewerObject



343
344
345
346
347
# File 'lib/gitlab/diff/file.rb', line 343

def rich_viewer
  return @rich_viewer if defined?(@rich_viewer)

  @rich_viewer = rich_viewer_class&.new(self)
end

#simple_viewerObject



339
340
341
# File 'lib/gitlab/diff/file.rb', line 339

def simple_viewer
  @simple_viewer ||= simple_viewer_class.new(self)
end

#sizeObject



311
312
313
# File 'lib/gitlab/diff/file.rb', line 311

def size
  valid_blobs.sum(&:size)
end

#stored_externally?Boolean

Returns:

  • (Boolean)


291
292
293
# File 'lib/gitlab/diff/file.rb', line 291

def stored_externally?
  try_blobs(:stored_externally?)
end

#text?Boolean

Returns:

  • (Boolean)


329
330
331
332
333
# File 'lib/gitlab/diff/file.rb', line 329

def text?
  strong_memoize(:is_text) do
    !binary? && !different_type?
  end
end

#text_in_repo?Boolean

Returns:

  • (Boolean)


283
284
285
# File 'lib/gitlab/diff/file.rb', line 283

def text_in_repo?
  !binary_in_repo?
end

#unfold_diff_lines(position) ⇒ Object

Changes diff_lines according to the given position. That is, it checks whether the position requires blob lines into the diff in order to be presented.



196
197
198
199
200
201
202
203
204
205
# File 'lib/gitlab/diff/file.rb', line 196

def unfold_diff_lines(position)
  return unless position

  unfolder = Gitlab::Diff::LinesUnfolder.new(self, position)

  if unfolder.unfold_required?
    @diff_lines = unfolder.unfolded_diff_lines
    @unfolded = true
  end
end

#unfolded?Boolean

Returns:

  • (Boolean)


207
208
209
# File 'lib/gitlab/diff/file.rb', line 207

def unfolded?
  @unfolded
end

#viewerObject



335
336
337
# File 'lib/gitlab/diff/file.rb', line 335

def viewer
  rich_viewer || simple_viewer
end

#viewer_hunksObject



189
190
191
# File 'lib/gitlab/diff/file.rb', line 189

def viewer_hunks
  ViewerHunk.init_from_diff_lines(diff_lines_with_match_tail)
end

#whitespace_only?Boolean

Returns:

  • (Boolean)


428
429
430
# File 'lib/gitlab/diff/file.rb', line 428

def whitespace_only?
  !collapsed? && diff_lines_for_serializer.nil? && (added_lines != 0 || removed_lines != 0)
end