summaryrefslogtreecommitdiff
path: root/lib/banzai/filter/commit_trailers_filter.rb
blob: ef16df1f3ae7f07b33edd5bb4cd2b173a4730f82 (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
module Banzai
  module Filter
    # HTML filter that replaces users' names and emails in commit trailers
    # with links to their GitLab accounts or mailto links to their mentioned
    # emails.
    #
    # Commit trailers are special labels in the form of `*-by:` and fall on a
    # single line, ex:
    #
    #   Reported-By: John S. Doe <john.doe@foo.bar>
    #
    # More info about this can be found here:
    # * https://git.wiki.kernel.org/index.php/CommitMessageConventions
    class CommitTrailersFilter < HTML::Pipeline::Filter
      include ActionView::Helpers::TagHelper
      include ApplicationHelper
      include AvatarsHelper

      TRAILER_REGEXP = /(?<label>[[:alpha:]-]+-by:)/i.freeze
      AUTHOR_REGEXP = /(?<author_name>.+)/.freeze
      # Devise.email_regexp wouldn't work here since its designed to match
      # against strings that only contains email addresses; the \A and \z
      # around the expression will only match if the string being matched
      # contains just the email nothing else.
      MAIL_REGEXP = /&lt;(?<author_email>[^@\s]+@[^@\s]+)&gt;/.freeze
      FILTER_REGEXP = /(?<trailer>^\s*#{TRAILER_REGEXP}\s*#{AUTHOR_REGEXP}\s+#{MAIL_REGEXP}$)/mi.freeze

      def call
        doc.xpath('descendant-or-self::text()').each do |node|
          content = node.to_html

          next unless content.match(FILTER_REGEXP)

          html = trailer_filter(content)

          next if html == content

          node.replace(html)
        end

        doc
      end

      private

      # Replace trailer lines with links to GitLab users or mailto links to
      # non GitLab users.
      #
      # text - String text to replace trailers in.
      #
      # Returns a String with all trailer lines replaced with links to GitLab
      # users and mailto links to non GitLab users. All links have `data-trailer`
      # and `data-user` attributes attached.
      def trailer_filter(text)
        text.gsub(FILTER_REGEXP) do |author_match|
          label = $~[:label]
          "#{label} #{parse_user($~[:author_name], $~[:author_email], label)}"
        end
      end

      # Find a GitLab user using the supplied email and generate
      # a valid link to them, otherwise, generate a mailto link.
      #
      # name - String name used in the commit message for the user
      # email - String email used in the commit message for the user
      # trailer - String trailer used in the commit message
      #
      # Returns a String with a link to the user.
      def parse_user(name, email, trailer)
        link_to_user User.find_by_any_email(email),
          name: name,
          email: email,
          trailer: trailer
      end

      def urls
        Gitlab::Routing.url_helpers
      end

      def link_to_user(user, name:, email:, trailer:)
        wrapper = link_wrapper(data: {
          trailer: trailer,
          user: user.try(:id)
        })

        avatar = user_avatar_without_link(
          user: user,
          user_email: email,
          css_class: 'avatar-inline',
          has_tooltip: false
        )

        link_href = user.nil? ? "mailto:#{email}" : urls.user_url(user)

        avatar_link = link_tag(
          link_href,
          content: avatar,
          title: email
        )

        name_link = link_tag(
          link_href,
          content: name,
          title: email
        )

        email_link = link_tag(
          "mailto:#{email}",
          content: email,
          title: email
        )

        wrapper << "#{avatar_link}#{name_link} <#{email_link}>"
      end

      def link_wrapper(data: {})
        data_attributes = data_attributes_from_hash(data)

        doc.document.create_element(
          'span',
          data_attributes
        )
      end

      def link_tag(url, title: "", content: "", data: {})
        data_attributes = data_attributes_from_hash(data)

        attributes = data_attributes.merge(
          href: url,
          title: title
        )

        link = doc.document.create_element('a', attributes)

        if content.html_safe?
          link << content
        else
          link.content = content # make sure we escape content using nokogiri's #content=
        end

        link
      end

      def data_attributes_from_hash(data = {})
        data.reject! {|_, value| value.nil?}
        data.map do |key, value|
          [%(data-#{key.to_s.dasherize}), value]
        end.to_h
      end
    end
  end
end