summaryrefslogtreecommitdiff
path: root/lib/gitlab/reference_extractor.rb
blob: 949dd5d26b10cdfef0cd58b2a0ed96a502b1a984 (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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
module Gitlab
  # Extract possible GFM references from an arbitrary String for further processing.
  class ReferenceExtractor
    attr_accessor :project, :current_user, :references

    def initialize(project, current_user = nil)
      @project = project
      @current_user = current_user
    end

    def can?(user, action, subject)
      Ability.abilities.allowed?(user, action, subject)
    end

    def analyze(text)
      text = text.dup

      # Remove preformatted/code blocks so that references are not included
      text.gsub!(/^```.*?^```/m, '')
      text.gsub!(/[^`]`[^`]*?`[^`]/, '')

      @references = Hash.new { |hash, type| hash[type] = [] }
      parse_references(text)
    end

    # Given a valid project, resolve the extracted identifiers of the requested type to
    # model objects.

    def users
      references[:user].uniq.map do |project, identifier|
        if identifier == "all"
          project.team.members.flatten
        elsif namespace = Namespace.find_by(path: identifier)
          if namespace.is_a?(Group)
            namespace.users if can?(current_user, :read_group, namespace)
          else
            namespace.owner
          end
        end
      end.flatten.compact.uniq
    end

    def labels
      references[:label].uniq.map do |project, identifier|
        project.labels.where(id: identifier).first
      end.compact.uniq
    end

    def issues
      references[:issue].uniq.map do |project, identifier|
        if project.default_issues_tracker?
          project.issues.where(iid: identifier).first
        end
      end.compact.uniq
    end

    def merge_requests
      references[:merge_request].uniq.map do |project, identifier|
        project.merge_requests.where(iid: identifier).first
      end.compact.uniq
    end

    def snippets
      references[:snippet].uniq.map do |project, identifier|
        project.snippets.where(id: identifier).first
      end.compact.uniq
    end

    def commits
      references[:commit].uniq.map do |project, identifier|
        repo = project.repository
        repo.commit(identifier) if repo
      end.compact.uniq
    end

    def commit_ranges
      references[:commit_range].uniq.map do |project, identifier|
        repo = project.repository
        if repo
          from_id, to_id = identifier.split(/\.{2,3}/, 2)
          [repo.commit(from_id), repo.commit(to_id)]
        end
      end.compact.uniq
    end

    private

    NAME_STR = Gitlab::Regex::NAMESPACE_REGEX_STR
    PROJ_STR = "(?<project>#{NAME_STR}/#{NAME_STR})"

    REFERENCE_PATTERN = %r{
      (?<prefix>\W)?                         # Prefix
      (                                      # Reference
         @(?<user>#{NAME_STR})               # User name
        |~(?<label>\d+)                      # Label ID
        |(?<issue>([A-Z\-]+-)\d+)            # JIRA Issue ID
        |#{PROJ_STR}?\#(?<issue>([a-zA-Z\-]+-)?\d+) # Issue ID
        |#{PROJ_STR}?!(?<merge_request>\d+)  # MR ID
        |\$(?<snippet>\d+)                   # Snippet ID
        |(#{PROJ_STR}@)?(?<commit_range>[\h]{6,40}\.{2,3}[\h]{6,40}) # Commit range
        |(#{PROJ_STR}@)?(?<commit>[\h]{6,40}) # Commit ID
      )
      (?<suffix>\W)?                         # Suffix
    }x.freeze

    TYPES = %i(user issue label merge_request snippet commit commit_range).freeze

    def parse_references(text, project = @project)
      # parse reference links
      text.gsub!(REFERENCE_PATTERN) do |match|
        type = TYPES.detect { |t| $~[t].present? }

        actual_project = project
        project_prefix = nil
        project_path = $LAST_MATCH_INFO[:project]
        if project_path
          actual_project = ::Project.find_with_namespace(project_path)
          actual_project = nil unless can?(current_user, :read_project, actual_project)
          project_prefix = project_path
        end

        parse_result($LAST_MATCH_INFO, type,
                     actual_project, project_prefix) || match
      end
    end

    # Called from #parse_references.  Attempts to build a gitlab reference
    # link.  Returns nil if +type+ is nil, if the match string is an HTML
    # entity, if the reference is invalid, or if the matched text includes an
    # invalid project path.
    def parse_result(match_info, type, project, project_prefix)
      prefix = match_info[:prefix]
      suffix = match_info[:suffix]

      return nil if html_entity?(prefix, suffix) || type.nil?
      return nil if project.nil? && !project_prefix.nil?

      identifier = match_info[type]
      ref_link = reference_link(type, identifier, project, project_prefix)

      if ref_link
        "#{prefix}#{ref_link}#{suffix}"
      else
        nil
      end
    end

    # Return true if the +prefix+ and +suffix+ indicate that the matched string
    # is an HTML entity like &amp;
    def html_entity?(prefix, suffix)
      prefix && suffix && prefix[0] == '&' && suffix[-1] == ';'
    end

    def reference_link(type, identifier, project, _)
      references[type] << [project, identifier]
    end
  end
end