summaryrefslogtreecommitdiff
path: root/lib/gitlab/danger/teammate.rb
blob: ebd96be40d7851a791057c9b26bba9d3ed566f48 (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
# frozen_string_literal: true

module Gitlab
  module Danger
    class Teammate
      attr_reader :options, :username, :name, :role, :projects, :available, :tz_offset_hours

      # The options data are produced by https://gitlab.com/gitlab-org/gitlab-roulette/-/blob/master/lib/team_member.rb
      def initialize(options = {})
        @options = options
        @username = options['username']
        @name = options['name']
        @markdown_name = options['markdown_name']
        @role = options['role']
        @projects = options['projects']
        @available = options['available']
        @tz_offset_hours = options['tz_offset_hours']
      end

      def to_h
        options
      end

      def ==(other)
        return false unless other.respond_to?(:username)

        other.username == username
      end

      def in_project?(name)
        projects&.has_key?(name)
      end

      # Traintainers also count as reviewers
      def reviewer?(project, category, labels)
        has_capability?(project, category, :reviewer, labels) ||
          traintainer?(project, category, labels)
      end

      def traintainer?(project, category, labels)
        has_capability?(project, category, :trainee_maintainer, labels)
      end

      def maintainer?(project, category, labels)
        has_capability?(project, category, :maintainer, labels)
      end

      def markdown_name(author: nil)
        "#{@markdown_name} (#{utc_offset_text(author)})"
      end

      def local_hour
        (Time.now.utc + tz_offset_hours * 3600).hour
      end

      protected

      def floored_offset_hours
        floored_offset = tz_offset_hours.floor(0)

        floored_offset == tz_offset_hours ? floored_offset : tz_offset_hours
      end

      private

      def utc_offset_text(author = nil)
        offset_text =
          if floored_offset_hours >= 0
            "UTC+#{floored_offset_hours}"
          else
            "UTC#{floored_offset_hours}"
          end

        return offset_text unless author

        "#{offset_text}, #{offset_diff_compared_to_author(author)}"
      end

      def offset_diff_compared_to_author(author)
        diff = floored_offset_hours - author.floored_offset_hours
        return "same timezone as `@#{author.username}`" if diff == 0

        ahead_or_behind = diff < 0 ? 'behind' : 'ahead of'
        pluralized_hours = pluralize(diff.abs, 'hour', 'hours')

        "#{pluralized_hours} #{ahead_or_behind} `@#{author.username}`"
      end

      def has_capability?(project, category, kind, labels)
        case category
        when :test
          area = role[/Software Engineer in Test(?:.*?, (\w+))/, 1]

          area && labels.any?("devops::#{area.downcase}") if kind == :reviewer
        when :engineering_productivity
          return false unless role[/Engineering Productivity/]
          return true if kind == :reviewer

          capabilities(project).include?("#{kind} backend")
        else
          capabilities(project).include?("#{kind} #{category}")
        end
      end

      def capabilities(project)
        Array(projects.fetch(project, []))
      end

      def pluralize(count, singular, plural)
        word = count == 1 || count.to_s =~ /^1(\.0+)?$/ ? singular : plural

        "#{count || 0} #{word}"
      end
    end
  end
end