# CommitRange makes it easier to work with commit ranges # # Examples: # # range = CommitRange.new('f3f85602...e86e1013', project) # range.exclude_start? # => false # range.reference_title # => "Commits f3f85602 through e86e1013" # range.to_s # => "f3f85602...e86e1013" # # range = CommitRange.new('f3f856029bc5f966c5a7ee24cf7efefdd20e6019..e86e1013709735be5bb767e2b228930c543f25ae', project) # range.exclude_start? # => true # range.reference_title # => "Commits f3f85602^ through e86e1013" # range.to_param # => {from: "f3f856029bc5f966c5a7ee24cf7efefdd20e6019^", to: "e86e1013709735be5bb767e2b228930c543f25ae"} # range.to_s # => "f3f85602..e86e1013" # # # Assuming the specified project has a repository containing both commits: # range.valid_commits? # => true # class CommitRange include ActiveModel::Conversion include Referable attr_reader :commit_from, :notation, :commit_to attr_reader :ref_from, :ref_to # The Project model attr_accessor :project # The beginning and ending refs can be named or SHAs, and # the range notation can be double- or triple-dot. REF_PATTERN = /[0-9a-zA-Z][0-9a-zA-Z_.-]*[0-9a-zA-Z\^]/ PATTERN = /#{REF_PATTERN}\.{2,3}#{REF_PATTERN}/ # In text references, the beginning and ending refs can only be SHAs # between 7 and 40 hex characters. STRICT_PATTERN = /\h{7,40}\.{2,3}\h{7,40}/ def self.reference_prefix '@' end # Pattern used to extract commit range references from text # # This pattern supports cross-project references. def self.reference_pattern @reference_pattern ||= %r{ (?:#{Project.reference_pattern}#{reference_prefix})? (?<commit_range>#{STRICT_PATTERN}) }x end def self.link_reference_pattern @link_reference_pattern ||= super("compare", /(?<commit_range>#{PATTERN})/) end # Initialize a CommitRange # # range_string - The String commit range. # project - The Project model. # # Raises ArgumentError if `range_string` does not match `PATTERN`. def initialize(range_string, project) @project = project range_string = range_string.strip unless range_string =~ /\A#{PATTERN}\z/ raise ArgumentError, "invalid CommitRange string format: #{range_string}" end @ref_from, @notation, @ref_to = range_string.split(/(\.{2,3})/, 2) if project.valid_repo? @commit_from = project.commit(@ref_from) @commit_to = project.commit(@ref_to) end if valid_commits? @ref_from = Commit.truncate_sha(sha_from) if sha_from.start_with?(@ref_from) @ref_to = Commit.truncate_sha(sha_to) if sha_to.start_with?(@ref_to) end end def inspect %(#<#{self.class}:#{object_id} #{to_s}>) end def to_s sha_from + notation + sha_to end alias_method :id, :to_s def to_reference(from_project = nil) if cross_project_reference?(from_project) project.to_reference + self.class.reference_prefix + self.id else self.id end end def reference_link_text(from_project = nil) reference = ref_from + notation + ref_to if cross_project_reference?(from_project) reference = project.to_reference + self.class.reference_prefix + reference end reference end # Returns a String for use in a link's title attribute def reference_title "Commits #{sha_start} through #{sha_to}" end # Return a Hash of parameters for passing to a URL helper # # See `namespace_project_compare_url` def to_param { from: sha_start, to: sha_to } end def exclude_start? @notation == '..' end # Check if both the starting and ending commit IDs exist in a project's # repository def valid_commits? commit_start.present? && commit_end.present? end def persisted? true end def sha_from return nil unless @commit_from @commit_from.id end def sha_to return nil unless @commit_to @commit_to.id end def sha_start return nil unless sha_from exclude_start? ? sha_from + '^' : sha_from end def commit_start return nil unless sha_start if exclude_start? @commit_start ||= project.commit(sha_start) else commit_from end end alias_method :sha_end, :sha_to alias_method :commit_end, :commit_to end