summaryrefslogtreecommitdiff
path: root/app/models/commit_range.rb
blob: 86fc9eb01a3e0077ce1a20a289c1a37474c109db (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# CommitRange makes it easier to work with commit ranges
#
# Examples:
#
#   range = CommitRange.new('f3f85602...e86e1013')
#   range.exclude_start?  # => false
#   range.reference_title # => "Commits f3f85602 through e86e1013"
#   range.to_s            # => "f3f85602...e86e1013"
#
#   range = CommitRange.new('f3f856029bc5f966c5a7ee24cf7efefdd20e6019..e86e1013709735be5bb767e2b228930c543f25ae')
#   range.exclude_start?  # => true
#   range.reference_title # => "Commits f3f85602^ through e86e1013"
#   range.to_param        # => {from: "f3f856029bc5f966c5a7ee24cf7efefdd20e6019^", to: "e86e1013709735be5bb767e2b228930c543f25ae"}
#   range.to_s            # => "f3f85602..e86e1013"
#
#   # Assuming `project` is a Project with a repository containing both commits:
#   range.project = project
#   range.valid_commits? # => true
#
class CommitRange
  include ActiveModel::Conversion
  include Referable

  attr_reader :sha_from, :notation, :sha_to

  # Optional Project model
  attr_accessor :project

  # See `exclude_start?`
  attr_reader :exclude_start

  # The beginning and ending SHAs can be between 6 and 40 hex characters, and
  # the range notation can be double- or triple-dot.
  PATTERN = /\h{6,40}\.{2,3}\h{6,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
    %r{
      (?:#{Project.reference_pattern}#{reference_prefix})?
      (?<commit_range>#{PATTERN})
    }x
  end

  # Initialize a CommitRange
  #
  # range_string - The String commit range.
  # project      - An optional Project model.
  #
  # Raises ArgumentError if `range_string` does not match `PATTERN`.
  def initialize(range_string, project = nil)
    range_string.strip!

    unless range_string.match(/\A#{PATTERN}\z/)
      raise ArgumentError, "invalid CommitRange string format: #{range_string}"
    end

    @exclude_start = !range_string.include?('...')
    @sha_from, @notation, @sha_to = range_string.split(/(\.{2,3})/, 2)

    @project = project
  end

  def inspect
    %(#<#{self.class}:#{object_id} #{to_s}>)
  end

  def to_s
    "#{sha_from[0..7]}#{notation}#{sha_to[0..7]}"
  end

  def to_reference(from_project = nil)
    # Not using to_s because we want the full SHAs
    reference = sha_from + notation + sha_to

    if cross_project_reference?(from_project)
      reference = project.to_reference + '@' + reference
    end

    reference
  end

  # Returns a String for use in a link's title attribute
  def reference_title
    "Commits #{suffixed_sha_from} through #{sha_to}"
  end

  # Return a Hash of parameters for passing to a URL helper
  #
  # See `namespace_project_compare_url`
  def to_param
    { from: suffixed_sha_from, to: sha_to }
  end

  def exclude_start?
    exclude_start
  end

  # Check if both the starting and ending commit IDs exist in a project's
  # repository
  #
  # project - An optional Project to check (default: `project`)
  def valid_commits?(project = project)
    return nil   unless project.present?
    return false unless project.valid_repo?

    commit_from.present? && commit_to.present?
  end

  def persisted?
    true
  end

  def commit_from
    @commit_from ||= project.repository.commit(suffixed_sha_from)
  end

  def commit_to
    @commit_to ||= project.repository.commit(sha_to)
  end

  private

  def suffixed_sha_from
    sha_from + (exclude_start? ? '^' : '')
  end
end