PageRenderTime 41ms CodeModel.GetById 11ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/gitlab/diff/file.rb

https://gitlab.com/realsatomic/gitlab
Ruby | 486 lines | 341 code | 111 blank | 34 comment | 70 complexity | ce38ea58b7203fd1d86999f5ed55f728 MD5 | raw file
  1. # frozen_string_literal: true
  2. module Gitlab
  3. module Diff
  4. class File
  5. include Gitlab::Utils::StrongMemoize
  6. attr_reader :diff, :repository, :diff_refs, :fallback_diff_refs, :unique_identifier
  7. delegate :new_file?, :deleted_file?, :renamed_file?,
  8. :old_path, :new_path, :a_mode, :b_mode, :mode_changed?,
  9. :submodule?, :expanded?, :too_large?, :collapsed?, :line_count, :has_binary_notice?, to: :diff, prefix: false
  10. # Finding a viewer for a diff file happens based only on extension and whether the
  11. # diff file blobs are binary or text, which means 1 diff file should only be matched by 1 viewer,
  12. # and the order of these viewers doesn't really matter.
  13. #
  14. # However, when the diff file blobs are LFS pointers, we cannot know for sure whether the
  15. # file being pointed to is binary or text. In this case, we match only on
  16. # extension, preferring binary viewers over text ones if both exist, since the
  17. # large files referred to in "Large File Storage" are much more likely to be
  18. # binary than text.
  19. RICH_VIEWERS = [
  20. DiffViewer::Image
  21. ].sort_by { |v| v.binary? ? 0 : 1 }.freeze
  22. def initialize(
  23. diff,
  24. repository:,
  25. diff_refs: nil,
  26. fallback_diff_refs: nil,
  27. stats: nil,
  28. unique_identifier: nil)
  29. @diff = diff
  30. @stats = stats
  31. @repository = repository
  32. @diff_refs = diff_refs
  33. @fallback_diff_refs = fallback_diff_refs
  34. @unique_identifier = unique_identifier
  35. @unfolded = false
  36. # Ensure items are collected in the the batch
  37. new_blob_lazy
  38. old_blob_lazy
  39. diff.diff = Gitlab::Diff::CustomDiff.preprocess_before_diff(diff.new_path, old_blob_lazy, new_blob_lazy) || diff.diff unless use_renderable_diff?
  40. end
  41. def use_renderable_diff?
  42. strong_memoize(:_renderable_diff_enabled) { Feature.enabled?(:rendered_diffs_viewer, repository.project, default_enabled: :yaml) }
  43. end
  44. def has_renderable?
  45. rendered&.has_renderable?
  46. end
  47. def position(position_marker, position_type: :text)
  48. return unless diff_refs
  49. data = {
  50. diff_refs: diff_refs,
  51. position_type: position_type.to_s,
  52. old_path: old_path,
  53. new_path: new_path
  54. }
  55. if position_type == :text
  56. data.merge!(text_position_properties(position_marker))
  57. else
  58. data.merge!(image_position_properties(position_marker))
  59. end
  60. Position.new(data)
  61. end
  62. def line_code(line)
  63. return if line.meta?
  64. Gitlab::Git.diff_line_code(file_path, line.new_pos, line.old_pos)
  65. end
  66. def line_for_line_code(code)
  67. diff_lines.find { |line| line_code(line) == code }
  68. end
  69. def line_for_position(pos)
  70. return unless pos.position_type == 'text'
  71. # This method is normally used to find which line the diff was
  72. # commented on, and in this context, it's normally the raw diff persisted
  73. # at `note_diff_files`, which is a fraction of the entire diff
  74. # (it goes from the first line, to the commented line, or
  75. # one line below). Therefore it's more performant to fetch
  76. # from bottom to top instead of the other way around.
  77. diff_lines
  78. .reverse_each
  79. .find { |line| line.old_line == pos.old_line && line.new_line == pos.new_line }
  80. end
  81. def position_for_line_code(code)
  82. line = line_for_line_code(code)
  83. position(line) if line
  84. end
  85. def line_code_for_position(pos)
  86. line = line_for_position(pos)
  87. line_code(line) if line
  88. end
  89. # Returns the raw diff content up to the given line index
  90. def diff_hunk(diff_line)
  91. diff_line_index = diff_line.index
  92. # @@ (match) header is not kept if it's found in the top of the file,
  93. # therefore we should keep an extra line on this scenario.
  94. diff_line_index += 1 unless diff_lines.first.match?
  95. diff_lines.select { |line| line.index <= diff_line_index }.map(&:text).join("\n")
  96. end
  97. def old_sha
  98. diff_refs&.base_sha
  99. end
  100. def new_sha
  101. diff_refs&.head_sha
  102. end
  103. def new_content_sha
  104. return if deleted_file?
  105. return @new_content_sha if defined?(@new_content_sha)
  106. refs = diff_refs || fallback_diff_refs
  107. @new_content_sha = refs&.head_sha
  108. end
  109. def old_content_sha
  110. return if new_file?
  111. return @old_content_sha if defined?(@old_content_sha)
  112. refs = diff_refs || fallback_diff_refs
  113. @old_content_sha = refs&.base_sha
  114. end
  115. def new_blob
  116. strong_memoize(:new_blob) do
  117. new_blob_lazy&.itself
  118. end
  119. end
  120. def old_blob
  121. strong_memoize(:old_blob) do
  122. old_blob_lazy&.itself
  123. end
  124. end
  125. def new_blob_lines_between(from_line, to_line)
  126. return [] unless new_blob
  127. from_index = from_line - 1
  128. to_index = to_line - 1
  129. new_blob.load_all_data!
  130. new_blob.data.lines[from_index..to_index]
  131. end
  132. def content_sha
  133. new_content_sha || old_content_sha
  134. end
  135. def blob
  136. new_blob || old_blob
  137. end
  138. def highlighted_diff_lines=(value)
  139. clear_memoization(:diff_lines_for_serializer)
  140. @highlighted_diff_lines = value
  141. end
  142. # Array of Gitlab::Diff::Line objects
  143. def diff_lines
  144. @diff_lines ||=
  145. Gitlab::Diff::Parser.new.parse(raw_diff.each_line, diff_file: self).to_a
  146. end
  147. # Changes diff_lines according to the given position. That is,
  148. # it checks whether the position requires blob lines into the diff
  149. # in order to be presented.
  150. def unfold_diff_lines(position)
  151. return unless position
  152. unfolder = Gitlab::Diff::LinesUnfolder.new(self, position)
  153. if unfolder.unfold_required?
  154. @diff_lines = unfolder.unfolded_diff_lines
  155. @unfolded = true
  156. end
  157. end
  158. def unfolded?
  159. @unfolded
  160. end
  161. def highlight_loaded?
  162. @highlighted_diff_lines.present?
  163. end
  164. def highlighted_diff_lines
  165. @highlighted_diff_lines ||=
  166. Gitlab::Diff::Highlight.new(self, repository: self.repository).highlight
  167. end
  168. # Array[<Hash>] with right/left keys that contains Gitlab::Diff::Line objects which text is highlighted
  169. def parallel_diff_lines
  170. @parallel_diff_lines ||= Gitlab::Diff::ParallelDiff.new(self).parallelize
  171. end
  172. def raw_diff
  173. diff.diff.to_s
  174. end
  175. def next_line(index)
  176. diff_lines[index + 1]
  177. end
  178. def prev_line(index)
  179. diff_lines[index - 1] if index > 0
  180. end
  181. def paths
  182. [old_path, new_path].compact
  183. end
  184. def file_path
  185. new_path.presence || old_path
  186. end
  187. def file_hash
  188. Digest::SHA1.hexdigest(file_path)
  189. end
  190. def added_lines
  191. strong_memoize(:added_lines) do
  192. @stats&.additions || diff_lines.count(&:added?)
  193. end
  194. end
  195. def removed_lines
  196. strong_memoize(:removed_lines) do
  197. @stats&.deletions || diff_lines.count(&:removed?)
  198. end
  199. end
  200. def file_identifier
  201. "#{file_path}-#{new_file?}-#{deleted_file?}-#{renamed_file?}"
  202. end
  203. def file_identifier_hash
  204. Digest::SHA1.hexdigest(file_identifier)
  205. end
  206. def diffable?
  207. diffable_by_attribute? && !text_with_binary_notice?
  208. end
  209. def binary_in_repo?
  210. has_binary_notice? || try_blobs(:binary_in_repo?)
  211. end
  212. def text_in_repo?
  213. !binary_in_repo?
  214. end
  215. def external_storage_error?
  216. try_blobs(:external_storage_error?)
  217. end
  218. def stored_externally?
  219. try_blobs(:stored_externally?)
  220. end
  221. def external_storage
  222. try_blobs(:external_storage)
  223. end
  224. def content_changed?
  225. return blobs_changed? if diff_refs
  226. return false if new_file? || deleted_file? || renamed_file?
  227. text? && diff_lines.any?
  228. end
  229. def different_type?
  230. old_blob && new_blob && old_blob.binary? != new_blob.binary?
  231. end
  232. # rubocop: disable CodeReuse/ActiveRecord
  233. def size
  234. valid_blobs.sum(&:size)
  235. end
  236. # rubocop: enable CodeReuse/ActiveRecord
  237. # rubocop: disable CodeReuse/ActiveRecord
  238. def raw_size
  239. valid_blobs.sum(&:raw_size)
  240. end
  241. # rubocop: enable CodeReuse/ActiveRecord
  242. def empty?
  243. valid_blobs.map(&:empty?).all?
  244. end
  245. def binary?
  246. strong_memoize(:is_binary) do
  247. try_blobs(:binary?)
  248. end
  249. end
  250. def text?
  251. strong_memoize(:is_text) do
  252. !binary? && !different_type?
  253. end
  254. end
  255. def viewer
  256. rich_viewer || simple_viewer
  257. end
  258. def simple_viewer
  259. @simple_viewer ||= simple_viewer_class.new(self)
  260. end
  261. def rich_viewer
  262. return @rich_viewer if defined?(@rich_viewer)
  263. @rich_viewer = rich_viewer_class&.new(self)
  264. end
  265. def alternate_viewer
  266. alternate_viewer_class&.new(self)
  267. end
  268. def rendered_as_text?(ignore_errors: true)
  269. simple_viewer.is_a?(DiffViewer::Text) && (ignore_errors || simple_viewer.render_error.nil?)
  270. end
  271. # This adds the bottom match line to the array if needed. It contains
  272. # the data to load more context lines.
  273. def diff_lines_for_serializer
  274. strong_memoize(:diff_lines_for_serializer) do
  275. lines = highlighted_diff_lines
  276. next if lines.empty?
  277. next if blob.nil?
  278. last_line = lines.last
  279. if last_line.new_pos < total_blob_lines(blob) && !deleted_file?
  280. match_line = Gitlab::Diff::Line.new("", 'match', nil, last_line.old_pos, last_line.new_pos)
  281. lines.push(match_line)
  282. end
  283. lines
  284. end
  285. end
  286. def fully_expanded?
  287. return true if binary?
  288. lines = diff_lines_for_serializer
  289. return true if lines.nil?
  290. lines.none? { |line| line.type.to_s == 'match' }
  291. end
  292. def rendered
  293. return unless use_renderable_diff? && ipynb?
  294. strong_memoize(:rendered) { Rendered::Notebook::DiffFile.new(self) }
  295. end
  296. private
  297. def diffable_by_attribute?
  298. repository.attributes(file_path).fetch('diff', true)
  299. end
  300. # NOTE: Files with unsupported encodings (e.g. UTF-16) are treated as binary by git, but they are recognized as text files during encoding detection. These files have `Binary files a/filename and b/filename differ' as their raw diff content which cannot be used. We need to handle this special case and avoid displaying incorrect diff.
  301. def text_with_binary_notice?
  302. text? && has_binary_notice?
  303. end
  304. def fetch_blob(sha, path)
  305. return unless sha
  306. Blob.lazy(repository, sha, path)
  307. end
  308. def total_blob_lines(blob)
  309. @total_lines ||= begin
  310. line_count = blob.lines.size
  311. line_count -= 1 if line_count > 0 && blob.lines.last.blank?
  312. line_count
  313. end
  314. end
  315. def modified_file?
  316. new_file? || deleted_file? || content_changed?
  317. end
  318. def ipynb?
  319. modified_file? && file_path.ends_with?('.ipynb')
  320. end
  321. # We can't use Object#try because Blob doesn't inherit from Object, but
  322. # from BasicObject (via SimpleDelegator).
  323. def try_blobs(meth)
  324. old_blob&.public_send(meth) || new_blob&.public_send(meth)
  325. end
  326. def valid_blobs
  327. [old_blob, new_blob].compact
  328. end
  329. def text_position_properties(line)
  330. { old_line: line.old_line, new_line: line.new_line }
  331. end
  332. def image_position_properties(image_point)
  333. image_point.to_h
  334. end
  335. def blobs_changed?
  336. old_blob && new_blob && old_blob.id != new_blob.id
  337. end
  338. def new_blob_lazy
  339. fetch_blob(new_content_sha, file_path)
  340. end
  341. def old_blob_lazy
  342. fetch_blob(old_content_sha, old_path)
  343. end
  344. def simple_viewer_class
  345. return DiffViewer::Collapsed if collapsed?
  346. return DiffViewer::NotDiffable unless diffable?
  347. return DiffViewer::Text if modified_file? && text?
  348. return DiffViewer::NoPreview if content_changed?
  349. return DiffViewer::Added if new_file?
  350. return DiffViewer::Deleted if deleted_file?
  351. return DiffViewer::Renamed if renamed_file?
  352. return DiffViewer::ModeChanged if mode_changed?
  353. DiffViewer::NoPreview
  354. end
  355. def rich_viewer_class
  356. viewer_class_from(RICH_VIEWERS)
  357. end
  358. def viewer_class_from(classes)
  359. return if collapsed?
  360. return unless diffable?
  361. return unless modified_file?
  362. find_renderable_viewer_class(classes)
  363. end
  364. def alternate_viewer_class
  365. return unless viewer.instance_of?(DiffViewer::Renamed)
  366. find_renderable_viewer_class(RICH_VIEWERS) || (DiffViewer::Text if text?)
  367. end
  368. def find_renderable_viewer_class(classes)
  369. return if different_type? || external_storage_error?
  370. verify_binary = !stored_externally?
  371. classes.find { |viewer_class| viewer_class.can_render?(self, verify_binary: verify_binary) }
  372. end
  373. end
  374. end
  375. end