file.rb 8.1 KB
Newer Older
D
Dmitriy Zaporozhets 已提交
1 2 3
module Gitlab
  module Diff
    class File
4
      attr_reader :diff, :repository, :diff_refs, :fallback_diff_refs
D
Dmitriy Zaporozhets 已提交
5

6 7
      delegate :new_file?, :deleted_file?, :renamed_file?,
        :old_path, :new_path, :a_mode, :b_mode, :mode_changed?,
8
        :submodule?, :expanded?, :too_large?, :collapsed?, :line_count, :has_binary_notice?, to: :diff, prefix: false
D
Douwe Maan 已提交
9 10 11 12 13 14 15 16 17 18 19 20 21

      # 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.
      RICH_VIEWERS = [
        DiffViewer::Image
      ].sort_by { |v| v.binary? ? 0 : 1 }.freeze
D
Dmitriy Zaporozhets 已提交
22

23
      def initialize(diff, repository:, diff_refs: nil, fallback_diff_refs: nil)
D
Dmitriy Zaporozhets 已提交
24
        @diff = diff
25
        @repository = repository
26
        @diff_refs = diff_refs
27
        @fallback_diff_refs = fallback_diff_refs
28 29

        # Ensure items are collected in the the batch
30 31
        new_blob_lazy
        old_blob_lazy
32 33
      end

F
Felipe Artur 已提交
34
      def position(position_marker, position_type: :text)
D
Douwe Maan 已提交
35 36
        return unless diff_refs

F
Felipe Artur 已提交
37 38 39
        data = {
          diff_refs: diff_refs,
          position_type: position_type.to_s,
D
Douwe Maan 已提交
40
          old_path: old_path,
F
Felipe Artur 已提交
41 42 43 44 45 46 47 48 49 50
          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)
D
Douwe Maan 已提交
51 52
      end

53 54 55
      def line_code(line)
        return if line.meta?

56
        Gitlab::Git.diff_line_code(file_path, line.new_pos, line.old_pos)
57 58 59 60 61 62
      end

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

D
Douwe Maan 已提交
63
      def line_for_position(pos)
64 65 66
        return nil unless pos.position_type == 'text'

        diff_lines.find { |line| line.old_line == pos.old_line && line.new_line == pos.new_line }
D
Douwe Maan 已提交
67 68 69 70 71 72 73 74 75 76 77 78
      end

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

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

79 80 81 82 83 84 85
      # Returns the raw diff content up to the given line index
      def diff_hunk(diff_line)
        # Adding 2 because of the @@ diff header and Enum#take should consider
        # an extra line, because we're passing an index.
        raw_diff.each_line.take(diff_line.index + 2).join
      end

86 87 88 89 90 91 92 93
      def old_sha
        diff_refs&.base_sha
      end

      def new_sha
        diff_refs&.head_sha
      end

94 95 96
      def new_content_sha
        return if deleted_file?
        return @new_content_sha if defined?(@new_content_sha)
97 98

        refs = diff_refs || fallback_diff_refs
99
        @new_content_sha = refs&.head_sha
100 101 102 103 104
      end

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

106 107
        refs = diff_refs || fallback_diff_refs
        @old_content_sha = refs&.base_sha
D
Douwe Maan 已提交
108 109
      end

110
      def new_blob
111
        new_blob_lazy&.itself
112 113
      end

114
      def old_blob
115
        old_blob_lazy&.itself
D
Dmitriy Zaporozhets 已提交
116 117
      end

118 119 120 121 122
      def content_sha
        new_content_sha || old_content_sha
      end

      def blob
123
        new_blob || old_blob
124 125
      end

126
      attr_writer :highlighted_diff_lines
127

128
      # Array of Gitlab::Diff::Line objects
D
Dmitriy Zaporozhets 已提交
129
      def diff_lines
130
        @diff_lines ||= Gitlab::Diff::Parser.new.parse(raw_diff.each_line).to_a
131 132
      end

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

137
      # Array[<Hash>] with right/left keys that contains Gitlab::Diff::Line objects which text is hightlighted
138
      def parallel_diff_lines
139
        @parallel_diff_lines ||= Gitlab::Diff::ParallelDiff.new(self).parallelize
140 141
      end

142
      def raw_diff
143
        diff.diff.to_s
144 145
      end

D
Dmitriy Zaporozhets 已提交
146 147 148 149 150
      def next_line(index)
        diff_lines[index + 1]
      end

      def prev_line(index)
151
        diff_lines[index - 1] if index > 0
D
Dmitriy Zaporozhets 已提交
152
      end
153

D
Douwe Maan 已提交
154 155 156 157
      def paths
        [old_path, new_path].compact
      end

158
      def file_path
159
        new_path.presence || old_path
160
      end
161 162

      def added_lines
G
Gabriel Mazetto 已提交
163
        diff_lines.count(&:added?)
164 165 166
      end

      def removed_lines
G
Gabriel Mazetto 已提交
167
        diff_lines.count(&:removed?)
168
      end
D
Douwe Maan 已提交
169

170
      def file_identifier
171
        "#{file_path}-#{new_file?}-#{deleted_file?}-#{renamed_file?}"
172
      end
173 174 175 176 177 178

      def diffable?
        repository.attributes(file_path).fetch('diff') { true }
      end

      def binary?
179
        has_binary_notice? || try_blobs(:binary?)
180 181 182 183 184
      end

      def text?
        !binary?
      end
D
Douwe Maan 已提交
185 186

      def external_storage_error?
187
        try_blobs(:external_storage_error?)
D
Douwe Maan 已提交
188 189 190
      end

      def stored_externally?
191
        try_blobs(:stored_externally?)
D
Douwe Maan 已提交
192 193 194
      end

      def external_storage
195
        try_blobs(:external_storage)
D
Douwe Maan 已提交
196 197 198
      end

      def content_changed?
199 200 201 202
        return blobs_changed? if diff_refs
        return false if new_file? || deleted_file? || renamed_file?

        text? && diff_lines.any?
D
Douwe Maan 已提交
203 204 205 206 207 208 209
      end

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

      def size
210
        valid_blobs.map(&:size).sum
D
Douwe Maan 已提交
211 212 213
      end

      def raw_size
214
        valid_blobs.map(&:raw_size).sum
D
Douwe Maan 已提交
215 216 217
      end

      def raw_binary?
218
        try_blobs(:raw_binary?)
D
Douwe Maan 已提交
219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240
      end

      def raw_text?
        !raw_binary? && !different_type?
      end

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

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

        @rich_viewer = rich_viewer_class&.new(self)
      end

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

      private

241 242
      # We can't use Object#try because Blob doesn't inherit from Object, but
      # from BasicObject (via SimpleDelegator).
243
      def try_blobs(meth)
244
        old_blob&.public_send(meth) || new_blob&.public_send(meth)
245 246 247
      end

      def valid_blobs
248
        [old_blob, new_blob].compact
249 250
      end

F
Felipe Artur 已提交
251 252 253 254 255 256 257 258
      def text_position_properties(line)
        { old_line: line.old_line, new_line: line.new_line }
      end

      def image_position_properties(image_point)
        image_point.to_h
      end

259 260 261 262
      def blobs_changed?
        old_blob && new_blob && old_blob.id != new_blob.id
      end

263 264 265 266 267 268 269 270 271 272 273 274
      def new_blob_lazy
        return unless new_content_sha

        Blob.lazy(repository.project, new_content_sha, file_path)
      end

      def old_blob_lazy
        return unless old_content_sha

        Blob.lazy(repository.project, old_content_sha, old_path)
      end

D
Douwe Maan 已提交
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299
      def simple_viewer_class
        return DiffViewer::NotDiffable unless diffable?

        if content_changed?
          if raw_text?
            DiffViewer::Text
          else
            DiffViewer::NoPreview
          end
        elsif new_file?
          if raw_text?
            DiffViewer::Text
          else
            DiffViewer::Added
          end
        elsif deleted_file?
          if raw_text?
            DiffViewer::Text
          else
            DiffViewer::Deleted
          end
        elsif renamed_file?
          DiffViewer::Renamed
        elsif mode_changed?
          DiffViewer::ModeChanged
300 301
        else
          DiffViewer::NoPreview
D
Douwe Maan 已提交
302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317
        end
      end

      def rich_viewer_class
        viewer_class_from(RICH_VIEWERS)
      end

      def viewer_class_from(classes)
        return unless diffable?
        return if different_type? || external_storage_error?
        return unless new_file? || deleted_file? || content_changed?

        verify_binary = !stored_externally?

        classes.find { |viewer_class| viewer_class.can_render?(self, verify_binary: verify_binary) }
      end
D
Dmitriy Zaporozhets 已提交
318 319 320
    end
  end
end