# Gitlab::Git::Diff is a wrapper around native Rugged::Diff object module Gitlab module Git class Diff class TimeoutError < StandardError; end include EncodingHelper # Diff properties attr_accessor :old_path, :new_path, :a_mode, :b_mode, :diff # Stats properties attr_accessor :new_file, :renamed_file, :deleted_file class << self def between(repo, head, base, options = {}, *paths) # 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" common_commit = repo.merge_base_commit(head, base) options ||= {} break_rewrites = options[:break_rewrites] 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: # # :max_size :: # An integer specifying the maximum byte size of a file before a it # will be treated as binary. The default value is 512MB. # # :context_lines :: # The number of unchanged lines that define the boundary of a hunk # (and to display before and after the actual changes). The default is # 3. # # :interhunk_lines :: # The maximum number of unchanged lines between hunk boundaries before # the hunks will be merged into a one. The default is 0. # # :old_prefix :: # The virtual "directory" to prefix to old filenames in hunk headers. # The default is "a". # # :new_prefix :: # The virtual "directory" to prefix to new filenames in hunk headers. # The default is "b". # # :reverse :: # If true, the sides of the diff will be reversed. # # :force_text :: # If true, all files will be treated as text, disabling binary # attributes & detection. # # :ignore_whitespace :: # If true, all whitespace will be ignored. # # :ignore_whitespace_change :: # If true, changes in amount of whitespace will be ignored. # # :ignore_whitespace_eol :: # If true, whitespace at end of line will be ignored. # # :ignore_submodules :: # if true, submodules will be excluded from the diff completely. # # :patience :: # If true, the "patience diff" algorithm will be used (currenlty # unimplemented). # # :include_ignored :: # If true, ignored files will be included in the diff. # # :include_untracked :: # If true, untracked files will be included in the diff. # # :include_unmodified :: # If true, unmodified files will be included in the diff. # # :recurse_untracked_dirs :: # Even if +:include_untracked+ is true, untracked directories will # only be marked with a single entry in the diff. If this flag is set # to true, all files under ignored directories will be included in the # diff, too. # # :disable_pathspec_match :: # If true, the given +*paths+ will be applied as exact matches, # instead of as fnmatch patterns. # # :deltas_are_icase :: # If true, filename comparisons will be made with case-insensitivity. # # :include_untracked_content :: # if true, untracked content will be contained in the the diff patch # text. # # :skip_binary_check :: # If true, diff deltas will be generated without spending time on # binary detection. This is useful to improve performance in cases # where the actual file content difference is not needed. # # :include_typechange :: # If true, type changes for files will not be interpreted as deletion # of the "old file" and addition of the "new file", but will generate # typechange records. # # :include_typechange_trees :: # Even if +:include_typechange+ is true, blob -> tree changes will # still usually be handled as a deletion of the blob. If this flag is # set to true, blob -> tree changes will be marked as typechanges. # # :ignore_filemode :: # If true, file mode changes will be ignored. # # :recurse_ignored_dirs :: # Even if +:include_ignored+ is true, ignored directories will only be # marked with a single entry in the diff. If this flag is set to true, # all files under ignored directories will be included in the diff, # too. def filter_diff_options(options, default_options = {}) allowed_options = [:max_size, :context_lines, :interhunk_lines, :old_prefix, :new_prefix, :reverse, :force_text, :ignore_whitespace, :ignore_whitespace_change, :ignore_whitespace_eol, :ignore_submodules, :patience, :include_ignored, :include_untracked, :include_unmodified, :recurse_untracked_dirs, :disable_pathspec_match, :deltas_are_icase, :include_untracked_content, :skip_binary_check, :include_typechange, :include_typechange_trees, :ignore_filemode, :recurse_ignored_dirs, :paths, :max_files, :max_lines, :all_diffs] 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 def initialize(raw_diff) raise "Nil as raw diff passed" unless raw_diff if raw_diff.is_a?(Hash) init_from_hash(raw_diff) elsif raw_diff.is_a?(Rugged::Patch) init_from_rugged(raw_diff) else raise "Invalid raw diff type: #{raw_diff.class}" end end def serialize_keys @serialize_keys ||= %w(diff new_path old_path a_mode b_mode new_file renamed_file deleted_file).map(&:to_sym) end def to_hash hash = {} keys = serialize_keys keys.each do |key| hash[key] = send(key) end hash end def submodule? a_mode == '160000' || b_mode == '160000' end def line_count @line_count ||= Util.count_lines(@diff) end private def init_from_rugged(rugged) @diff = encode!(strip_diff_headers(rugged.to_s)) d = rugged.delta @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 def init_from_hash(hash) raw_diff = hash.symbolize_keys serialize_keys.each do |key| send(:"#{key}=", raw_diff[key.to_sym]) end 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?('---') or 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