BigW Consortium Gitlab

diff.rb 8.35 KB
Newer Older
1 2
# Gitaly note: JV: needs RPC for Gitlab::Git::Diff.between.

Robert Speicher committed
3 4 5 6
# Gitlab::Git::Diff is a wrapper around native Rugged::Diff object
module Gitlab
  module Git
    class Diff
7
      TimeoutError = Class.new(StandardError)
8
      include Gitlab::EncodingHelper
Robert Speicher committed
9 10 11 12 13 14 15

      # Diff properties
      attr_accessor :old_path, :new_path, :a_mode, :b_mode, :diff

      # Stats properties
      attr_accessor :new_file, :renamed_file, :deleted_file

16 17 18 19
      alias_method :new_file?, :new_file
      alias_method :deleted_file?, :deleted_file
      alias_method :renamed_file?, :renamed_file

20
      attr_accessor :expanded
21
      attr_writer :too_large
Robert Speicher committed
22

Douwe Maan committed
23 24
      alias_method :expanded?, :expanded

25
      SERIALIZE_KEYS = %i(diff new_path old_path a_mode b_mode new_file renamed_file deleted_file too_large).freeze
26

27 28 29
      class << self
        # The maximum size of a diff to display.
        def size_limit
30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
          if RequestStore.active?
            RequestStore['gitlab_git_diff_size_limit'] ||= find_size_limit
          else
            find_size_limit
          end
        end

        # The maximum size before a diff is collapsed.
        def collapse_limit
          if RequestStore.active?
            RequestStore['gitlab_git_diff_collapse_limit'] ||= find_collapse_limit
          else
            find_collapse_limit
          end
        end

        def find_size_limit
47 48 49 50 51 52
          if Feature.enabled?('gitlab_git_diff_size_limit_increase')
            200.kilobytes
          else
            100.kilobytes
          end
        end
Robert Speicher committed
53

54
        def find_collapse_limit
55 56 57 58 59 60
          if Feature.enabled?('gitlab_git_diff_size_limit_increase')
            100.kilobytes
          else
            10.kilobytes
          end
        end
Robert Speicher committed
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93

        def between(repo, head, base, options = {}, *paths)
          straight = options.delete(:straight) || false

          common_commit = if straight
                            base
                          else
                            # Only show what is new in the source branch
                            # compared to the target branch, not the other way
                            # around. The linex below with merge_base is
                            # equivalent to diff with three dots (git diff
                            # branch1...branch2) From the git documentation:
                            # "git diff A...B" is equivalent to "git diff
                            # $(git-merge-base A B) B"
                            repo.merge_base_commit(head, base)
                          end

          options ||= {}
          actual_options = filter_diff_options(options)
          repo.diff(common_commit, head, actual_options, *paths)
        end

        # Return a copy of the +options+ hash containing only keys that can be
        # passed to Rugged.  Allowed options are:
        #
        #  :ignore_whitespace_change ::
        #    If true, changes in amount of whitespace will be ignored.
        #
        #  :disable_pathspec_match ::
        #    If true, the given +*paths+ will be applied as exact matches,
        #    instead of as fnmatch patterns.
        #
        def filter_diff_options(options, default_options = {})
94 95
          allowed_options = [:ignore_whitespace_change,
                             :disable_pathspec_match, :paths,
96
                             :max_files, :max_lines, :limits, :expanded]
Robert Speicher committed
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120

          if default_options
            actual_defaults = default_options.dup
            actual_defaults.keep_if do |key|
              allowed_options.include?(key)
            end
          else
            actual_defaults = {}
          end

          if options
            filtered_opts = options.dup
            filtered_opts.keep_if do |key|
              allowed_options.include?(key)
            end
            filtered_opts = actual_defaults.merge(filtered_opts)
          else
            filtered_opts = actual_defaults
          end

          filtered_opts
        end
      end

121 122 123
      def initialize(raw_diff, expanded: true)
        @expanded = expanded

Robert Speicher committed
124 125
        case raw_diff
        when Hash
126
          init_from_hash(raw_diff)
127
          prune_diff_if_eligible
Robert Speicher committed
128
        when Rugged::Patch, Rugged::Diff::Delta
129
          init_from_rugged(raw_diff)
130
        when Gitlab::GitalyClient::Diff
131
          init_from_gitaly(raw_diff)
132
          prune_diff_if_eligible
133 134
        when Gitaly::CommitDelta
          init_from_gitaly(raw_diff)
Robert Speicher committed
135 136 137 138 139 140 141 142 143 144
        when nil
          raise "Nil as raw diff passed"
        else
          raise "Invalid raw diff type: #{raw_diff.class}"
        end
      end

      def to_hash
        hash = {}

145
        SERIALIZE_KEYS.each do |key|
146
          hash[key] = send(key) # rubocop:disable GitlabSecurity/PublicSend
Robert Speicher committed
147 148 149 150 151
        end

        hash
      end

152 153 154 155
      def mode_changed?
        a_mode && b_mode && a_mode != b_mode
      end

Robert Speicher committed
156 157 158 159 160 161 162 163 164
      def submodule?
        a_mode == '160000' || b_mode == '160000'
      end

      def line_count
        @line_count ||= Util.count_lines(@diff)
      end

      def too_large?
165
        if @too_large.nil?
166
          @too_large = @diff.bytesize >= self.class.size_limit
167 168 169
        else
          @too_large
        end
Robert Speicher committed
170 171
      end

172 173 174
      # This is used by `to_hash` and `init_from_hash`.
      alias_method :too_large, :too_large?

175
      def too_large!
Robert Speicher committed
176 177 178 179 180 181 182
        @diff = ''
        @line_count = 0
        @too_large = true
      end

      def collapsed?
        return @collapsed if defined?(@collapsed)
183

184
        @collapsed = !expanded && @diff.bytesize >= self.class.collapse_limit
Robert Speicher committed
185 186
      end

187
      def collapse!
Robert Speicher committed
188 189 190 191 192 193 194
        @diff = ''
        @line_count = 0
        @collapsed = true
      end

      private

195
      def init_from_rugged(rugged)
Robert Speicher committed
196
        if rugged.is_a?(Rugged::Patch)
197
          init_from_rugged_patch(rugged)
Robert Speicher committed
198 199 200 201 202 203 204 205 206 207 208 209 210 211
          d = rugged.delta
        else
          d = rugged
        end

        @new_path = encode!(d.new_file[:path])
        @old_path = encode!(d.old_file[:path])
        @a_mode = d.old_file[:mode].to_s(8)
        @b_mode = d.new_file[:mode].to_s(8)
        @new_file = d.added?
        @renamed_file = d.renamed?
        @deleted_file = d.deleted?
      end

212
      def init_from_rugged_patch(patch)
Robert Speicher committed
213 214
        # Don't bother initializing diffs that are too large. If a diff is
        # binary we're not going to display anything so we skip the size check.
215
        return if !patch.delta.binary? && prune_large_patch(patch)
Robert Speicher committed
216 217 218 219

        @diff = encode!(strip_diff_headers(patch.to_s))
      end

220
      def init_from_hash(hash)
Robert Speicher committed
221 222
        raw_diff = hash.symbolize_keys

223
        SERIALIZE_KEYS.each do |key|
224
          send(:"#{key}=", raw_diff[key.to_sym]) # rubocop:disable GitlabSecurity/PublicSend
Robert Speicher committed
225
        end
226 227
      end

228
      def init_from_gitaly(diff)
229
        @diff = encode!(diff.patch) if diff.respond_to?(:patch)
230 231 232 233 234 235 236
        @new_path = encode!(diff.to_path.dup)
        @old_path = encode!(diff.from_path.dup)
        @a_mode = diff.old_mode.to_s(8)
        @b_mode = diff.new_mode.to_s(8)
        @new_file = diff.from_id == BLANK_SHA
        @renamed_file = diff.from_path != diff.to_path
        @deleted_file = diff.to_id == BLANK_SHA
237 238

        collapse! if diff.respond_to?(:collapsed) && diff.collapsed
239
      end
Robert Speicher committed
240

241 242 243 244 245 246
      def prune_diff_if_eligible
        if too_large?
          too_large!
        elsif collapsed?
          collapse!
        end
Robert Speicher committed
247 248 249 250
      end

      # If the patch surpasses any of the diff limits it calls the appropiate
      # prune method and returns true. Otherwise returns false.
251
      def prune_large_patch(patch)
Robert Speicher committed
252 253 254 255 256 257
        size = 0

        patch.each_hunk do |hunk|
          hunk.each_line do |line|
            size += line.content.bytesize

258
            if size >= self.class.size_limit
259
              too_large!
Robert Speicher committed
260 261 262 263 264
              return true
            end
          end
        end

265
        if !expanded && size >= self.class.collapse_limit
266
          collapse!
Robert Speicher committed
267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291
          return true
        end

        false
      end

      # Strip out the information at the beginning of the patch's text to match
      # Grit's output
      def strip_diff_headers(diff_text)
        # Delete everything up to the first line that starts with '---' or
        # 'Binary'
        diff_text.sub!(/\A.*?^(---|Binary)/m, '\1')

        if diff_text.start_with?('---', 'Binary')
          diff_text
        else
          # If the diff_text did not contain a line starting with '---' or
          # 'Binary', return the empty string. No idea why; we are just
          # preserving behavior from before the refactor.
          ''
        end
      end
    end
  end
end