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

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Utils::StrongMemoize

#clear_memoization, #strong_memoize, #strong_memoized?

Constructor Details

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

Returns a new instance of File.


27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/gitlab/diff/file.rb', line 27

def initialize(
  diff,
  repository:,
  diff_refs: nil,
  fallback_diff_refs: nil,
  stats: nil,
  unique_identifier: nil)

  @diff = diff
  @stats = stats
  @repository = repository
  @diff_refs = diff_refs
  @fallback_diff_refs = fallback_diff_refs
  @unique_identifier = unique_identifier
  @unfolded = false

  # Ensure items are collected in the the batch
  new_blob_lazy
  old_blob_lazy
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

#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

#added_linesObject


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

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

#alternate_viewerObject


337
338
339
# File 'lib/gitlab/diff/file.rb', line 337

def alternate_viewer
  alternate_viewer_class&.new(self)
end

#binary?Boolean

Returns:

  • (Boolean)

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

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

#binary_in_repo?Boolean

Returns:

  • (Boolean)

264
265
266
# File 'lib/gitlab/diff/file.rb', line 264

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

#blobObject


169
170
171
# File 'lib/gitlab/diff/file.rb', line 169

def blob
  new_blob || old_blob
end

#content_changed?Boolean

Returns:

  • (Boolean)

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

def content_changed?
  return blobs_changed? if diff_refs
  return false if new_file? || deleted_file? || renamed_file?

  text? && diff_lines.any?
end

#content_shaObject


165
166
167
# File 'lib/gitlab/diff/file.rb', line 165

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


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

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


179
180
181
182
# File 'lib/gitlab/diff/file.rb', line 179

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.


347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
# File 'lib/gitlab/diff/file.rb', line 347

def diff_lines_for_serializer
  strong_memoize(:diff_lines_for_serializer) do
    lines = highlighted_diff_lines

    next if lines.empty?
    next 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
end

#diffable?Boolean

Returns:

  • (Boolean)

260
261
262
# File 'lib/gitlab/diff/file.rb', line 260

def diffable?
  diffable_by_attribute? && !text_with_binary_notice?
end

#different_type?Boolean

Returns:

  • (Boolean)

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

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

#empty?Boolean

rubocop: enable CodeReuse/ActiveRecord

Returns:

  • (Boolean)

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

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

#external_storageObject


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

def external_storage
  try_blobs(:external_storage)
end

#external_storage_error?Boolean

Returns:

  • (Boolean)

272
273
274
# File 'lib/gitlab/diff/file.rb', line 272

def external_storage_error?
  try_blobs(:external_storage_error?)
end

#file_hashObject


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

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

#file_identifierObject


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

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

#file_identifier_hashObject


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

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

#file_pathObject


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

def file_path
  new_path.presence || old_path
end

#fully_expanded?Boolean

Returns:

  • (Boolean)

365
366
367
368
369
370
371
372
373
# File 'lib/gitlab/diff/file.rb', line 365

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)

52
53
54
# File 'lib/gitlab/diff/file.rb', line 52

def has_renderable?
  rendered&.has_renderable?
end

#highlight_loaded?Boolean

Returns:

  • (Boolean)

202
203
204
# File 'lib/gitlab/diff/file.rb', line 202

def highlight_loaded?
  @highlighted_diff_lines.present?
end

#highlighted_diff_linesObject


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

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

#highlighted_diff_lines=(value) ⇒ Object


173
174
175
176
# File 'lib/gitlab/diff/file.rb', line 173

def highlighted_diff_lines=(value)
  clear_memoization(:diff_lines_for_serializer)
  @highlighted_diff_lines = value
end

#ipynb?Boolean

Returns:

  • (Boolean)

381
382
383
# File 'lib/gitlab/diff/file.rb', line 381

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

#line_code(line) ⇒ Object


75
76
77
78
79
# File 'lib/gitlab/diff/file.rb', line 75

def line_code(line)
  return if line.meta?

  Gitlab::Git.diff_line_code(file_path, line.new_pos, line.old_pos)
end

#line_code_for_position(pos) ⇒ Object


104
105
106
107
# File 'lib/gitlab/diff/file.rb', line 104

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

#line_for_line_code(code) ⇒ Object


81
82
83
# File 'lib/gitlab/diff/file.rb', line 81

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

#line_for_position(pos) ⇒ Object


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

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

#new_blobObject


143
144
145
146
147
# File 'lib/gitlab/diff/file.rb', line 143

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

#new_blob_lines_between(from_line, to_line) ⇒ Object


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

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


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

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


123
124
125
# File 'lib/gitlab/diff/file.rb', line 123

def new_sha
  diff_refs&.head_sha
end

#next_line(index) ⇒ Object


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

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

#old_blobObject


149
150
151
152
153
# File 'lib/gitlab/diff/file.rb', line 149

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

#old_content_shaObject


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

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


119
120
121
# File 'lib/gitlab/diff/file.rb', line 119

def old_sha
  diff_refs&.base_sha
end

#parallel_diff_linesObject


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

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

#pathsObject


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

def paths
  [old_path, new_path].compact
end

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


56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/gitlab/diff/file.rb', line 56

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


99
100
101
102
# File 'lib/gitlab/diff/file.rb', line 99

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

#prev_line(index) ⇒ Object


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

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

#raw_diffObject


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

def raw_diff
  diff.diff.to_s
end

#raw_sizeObject

rubocop: disable CodeReuse/ActiveRecord


302
303
304
# File 'lib/gitlab/diff/file.rb', line 302

def raw_size
  valid_blobs.sum(&:raw_size)
end

#removed_linesObject


246
247
248
249
250
# File 'lib/gitlab/diff/file.rb', line 246

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

#renderedObject


375
376
377
378
379
# File 'lib/gitlab/diff/file.rb', line 375

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

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

#rendered_as_text?(ignore_errors: true) ⇒ Boolean

Returns:

  • (Boolean)

341
342
343
# File 'lib/gitlab/diff/file.rb', line 341

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

#rich_viewerObject


331
332
333
334
335
# File 'lib/gitlab/diff/file.rb', line 331

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

  @rich_viewer = rich_viewer_class&.new(self)
end

#simple_viewerObject


327
328
329
# File 'lib/gitlab/diff/file.rb', line 327

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

#sizeObject

rubocop: disable CodeReuse/ActiveRecord


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

def size
  valid_blobs.sum(&:size)
end

#stored_externally?Boolean

Returns:

  • (Boolean)

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

def stored_externally?
  try_blobs(:stored_externally?)
end

#text?Boolean

Returns:

  • (Boolean)

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

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

#text_in_repo?Boolean

Returns:

  • (Boolean)

268
269
270
# File 'lib/gitlab/diff/file.rb', line 268

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.


187
188
189
190
191
192
193
194
195
196
# File 'lib/gitlab/diff/file.rb', line 187

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)

198
199
200
# File 'lib/gitlab/diff/file.rb', line 198

def unfolded?
  @unfolded
end

#use_semantic_ipynb_diff?Boolean

Returns:

  • (Boolean)

48
49
50
# File 'lib/gitlab/diff/file.rb', line 48

def use_semantic_ipynb_diff?
  strong_memoize(:_use_semantic_ipynb_diff) { Feature.enabled?(:ipynb_semantic_diff, repository.project) }
end

#viewerObject


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

def viewer
  rich_viewer || simple_viewer
end