lazylead 0.5.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +49 -1
  3. data/.simplecov +1 -1
  4. data/Guardfile +1 -1
  5. data/Rakefile +4 -3
  6. data/bin/lazylead +9 -5
  7. data/lazylead.gemspec +20 -15
  8. data/lib/lazylead/cc.rb +21 -20
  9. data/lib/lazylead/cli/app.rb +13 -5
  10. data/lib/lazylead/confluence.rb +8 -1
  11. data/lib/lazylead/email.rb +0 -20
  12. data/lib/lazylead/exchange.rb +16 -28
  13. data/lib/lazylead/log.rb +2 -1
  14. data/lib/lazylead/model.rb +31 -16
  15. data/lib/lazylead/opts.rb +65 -2
  16. data/lib/lazylead/postman.rb +18 -16
  17. data/lib/lazylead/salt.rb +1 -0
  18. data/lib/lazylead/smtp.rb +3 -1
  19. data/lib/lazylead/system/jira.rb +31 -6
  20. data/lib/lazylead/task/accuracy/accuracy.rb +8 -10
  21. data/lib/lazylead/task/accuracy/attachment.rb +0 -4
  22. data/lib/lazylead/task/accuracy/logs.rb +8 -4
  23. data/lib/lazylead/task/accuracy/onlyll.rb +148 -0
  24. data/lib/lazylead/task/accuracy/records.rb +1 -1
  25. data/lib/lazylead/task/accuracy/servers.rb +16 -7
  26. data/lib/lazylead/task/accuracy/stacktrace.rb +50 -10
  27. data/lib/lazylead/task/accuracy/testcase.rb +23 -6
  28. data/lib/lazylead/task/assignment.rb +96 -0
  29. data/lib/lazylead/task/fix_version.rb +6 -0
  30. data/lib/lazylead/task/propagate_down.rb +1 -1
  31. data/lib/lazylead/task/svn/diff.rb +77 -0
  32. data/lib/lazylead/task/svn/grep.rb +139 -0
  33. data/lib/lazylead/task/svn/touch.rb +99 -0
  34. data/lib/lazylead/version.rb +1 -1
  35. data/lib/messages/illegal_assignee_change.erb +123 -0
  36. data/lib/messages/illegal_fixversion_change.erb +8 -0
  37. data/lib/messages/only_ll.erb +107 -0
  38. data/lib/messages/svn_diff.erb +110 -0
  39. data/lib/messages/{svn_log.erb → svn_diff_attachment.erb} +19 -9
  40. data/lib/messages/svn_grep.erb +114 -0
  41. data/test/lazylead/cc_test.rb +1 -0
  42. data/test/lazylead/model_test.rb +20 -0
  43. data/test/lazylead/opts_test.rb +47 -0
  44. data/test/lazylead/postman_test.rb +8 -5
  45. data/test/lazylead/smoke_test.rb +13 -0
  46. data/test/lazylead/system/jira_test.rb +6 -7
  47. data/test/lazylead/task/accuracy/logs_test.rb +62 -2
  48. data/test/lazylead/task/accuracy/onlyll_test.rb +138 -0
  49. data/test/lazylead/task/accuracy/servers_test.rb +2 -2
  50. data/test/lazylead/task/accuracy/stacktrace_test.rb +227 -0
  51. data/test/lazylead/task/accuracy/testcase_test.rb +49 -0
  52. data/test/lazylead/task/assignment_test.rb +53 -0
  53. data/test/lazylead/task/fix_version_test.rb +1 -0
  54. data/test/lazylead/task/propagate_down_test.rb +4 -3
  55. data/test/lazylead/task/savepoint_test.rb +7 -4
  56. data/test/lazylead/task/svn/diff_test.rb +97 -0
  57. data/test/lazylead/task/svn/grep_test.rb +103 -0
  58. data/test/lazylead/task/{touch_test.rb → svn/touch_test.rb} +7 -34
  59. data/test/test.rb +7 -6
  60. data/upgrades/sqlite/999.testdata.sql +3 -1
  61. metadata +120 -34
  62. data/lib/lazylead/task/touch.rb +0 -119
@@ -23,6 +23,7 @@
23
23
  # OR OTHER DEALINGS IN THE SOFTWARE.
24
24
 
25
25
  require "logging"
26
+ require "colorize"
26
27
  require "forwardable"
27
28
 
28
29
  module Lazylead
@@ -68,7 +69,7 @@ module Lazylead
68
69
  Logging.appenders.stdout(
69
70
  "stdout",
70
71
  layout: Logging.layouts.pattern(
71
- pattern: "[%d] %-5l [%X{tid}] %m\n",
72
+ pattern: "[%d] %-5l #{'[%X{tid}]'.colorize(:light_green)} %m\n",
72
73
  color_scheme: "bright"
73
74
  )
74
75
  )
@@ -68,9 +68,7 @@ module Lazylead
68
68
  opts.each_with_object({}) do |e, o|
69
69
  k = e[0]
70
70
  v = e[1]
71
- if v.respond_to? :start_with?
72
- v = ENV[v.slice(2, v.length - 3)] if v.start_with? "${"
73
- end
71
+ v = ENV[v.slice(2, v.length - 3)] if v.respond_to?(:start_with?) && v.start_with?("${")
74
72
  o[k] = v
75
73
  end
76
74
  end
@@ -80,6 +78,13 @@ module Lazylead
80
78
  JSON.parse(properties).to_h
81
79
  end
82
80
 
81
+ def to_h?
82
+ return true unless to_hash.nil?
83
+ false
84
+ rescue StandardError => _e
85
+ false
86
+ end
87
+
83
88
  def to_s
84
89
  attributes.map { |k, v| "#{k}='#{v}'" }.join(", ")
85
90
  end
@@ -99,6 +104,7 @@ module Lazylead
99
104
  sys = system.connect
100
105
  opts = props
101
106
  opts = detect_cc(sys) if opts.key? "cc"
107
+ opts["system"] = second_sys if opts.numeric? "system"
102
108
  action.constantize.new.run(sys, postman, opts)
103
109
  end
104
110
 
@@ -130,12 +136,12 @@ module Lazylead
130
136
 
131
137
  def props
132
138
  @props ||= begin
133
- if team.nil?
134
- Opts.new(env(to_hash))
135
- else
136
- Opts.new(env(team.to_hash.merge(to_hash)))
137
- end
138
- end
139
+ if team.nil?
140
+ Opts.new(env(to_hash))
141
+ else
142
+ Opts.new(env(team.to_hash.merge(to_hash)))
143
+ end
144
+ end
139
145
  end
140
146
 
141
147
  def postman
@@ -146,17 +152,21 @@ module Lazylead
146
152
  end
147
153
  end
148
154
 
155
+ def second_sys
156
+ sys = System.find(props["system"])
157
+ raise "ll-014: No ticketing system found for #{self}" if sys.nil?
158
+ sys.connect
159
+ end
160
+
149
161
  private
150
162
 
151
163
  # Parse scheduling #type and #unit
152
164
  def trigger
153
165
  @trigger ||= begin
154
- trg = schedule.split(":")
155
- unless trg.size == 2
156
- raise "ll-007: illegal schedule format '#{schedule}'"
157
- end
158
- trg.map(&:strip).map(&:chomp)
159
- end
166
+ trg = schedule.split(":")
167
+ raise "ll-007: illegal schedule format '#{schedule}'" unless trg.size == 2
168
+ trg.map(&:strip).map(&:chomp)
169
+ end
160
170
  end
161
171
  end
162
172
 
@@ -172,6 +182,10 @@ module Lazylead
172
182
  @log = log
173
183
  end
174
184
 
185
+ # @todo #/DEV Remove the suppression during next refactoring (or enhancements)
186
+ # for the method below
187
+ #
188
+ # rubocop:disable Metrics/AbcSize
175
189
  def exec
176
190
  Logging.mdc["tid"] = "task #{id}"
177
191
  @log.debug "'#{name}' is started."
@@ -182,12 +196,13 @@ module Lazylead
182
196
  rescue StandardError => e
183
197
  msg = <<~MSG
184
198
  ll-006: Task ##{id} #{e} (#{e.class}) at #{self}
185
- #{Backtrace.new(e) if ARGV.include? '--trace'}"
199
+ #{Backtrace.new(e) if ARGV.include? '--trace'}
186
200
  MSG
187
201
  @log.error msg
188
202
  ensure
189
203
  Logging.mdc["tid"] = ""
190
204
  end
205
+ # rubocop:enable Metrics/AbcSize
191
206
  end
192
207
 
193
208
  # Ticketing systems to monitor.
@@ -34,7 +34,8 @@ module Lazylead
34
34
  # License:: MIT
35
35
  class Opts
36
36
  extend Forwardable
37
- def_delegators :@origin, :[], :[]=, :to_s, :key?, :fetch, :merge, :except
37
+ def_delegators :@origin, :[], :[]=, :to_s, :key?, :fetch, :except, :each,
38
+ :each_pair, :sort_by
38
39
 
39
40
  def initialize(origin = {})
40
41
  @origin = origin
@@ -42,7 +43,14 @@ module Lazylead
42
43
 
43
44
  # Split text value by delimiter, trim all spaces and reject blank items
44
45
  def slice(key, delim)
45
- to_h[key].split(delim).map(&:chomp).map(&:strip).reject(&:blank?)
46
+ return [] unless to_h.key? key
47
+ trim to_h[key].split(delim)
48
+ end
49
+
50
+ # Trim all spaces and reject blank items in array
51
+ def trim(arr)
52
+ return [] if arr.nil?
53
+ arr.map(&:chomp).map(&:strip).reject(&:blank?)
46
54
  end
47
55
 
48
56
  def blank?(key)
@@ -76,5 +84,60 @@ module Lazylead
76
84
  return Salt.new(sid).decrypt(text) if ENV.key? sid
77
85
  text
78
86
  end
87
+
88
+ def merge(args)
89
+ return self unless args.is_a? Hash
90
+ Opts.new @origin.merge(args)
91
+ end
92
+
93
+ # Construct html document from template and binds.
94
+ def msg_body(template = "template")
95
+ Email.new(
96
+ to_h[template],
97
+ to_h.merge(version: Lazylead::VERSION)
98
+ ).render
99
+ end
100
+
101
+ def msg_to(delim = ",")
102
+ sliced delim, :to, "to"
103
+ end
104
+
105
+ def msg_cc(delim = ",")
106
+ sliced delim, :cc, "cc"
107
+ end
108
+
109
+ def msg_from(delim = ",")
110
+ sliced delim, :from, "from"
111
+ end
112
+
113
+ def msg_attachments(delim = ",")
114
+ sliced(delim, :attachments, "attachments").select { |f| File.file? f }
115
+ end
116
+
117
+ #
118
+ # Find the option by key and split by delimiter
119
+ # Opts.new("key" => "a,b").sliced(",", "key") => [a, b]
120
+ # Opts.new(key: "a,b").sliced(",", :key) => [a, b]
121
+ # Opts.new(key: "a,b").sliced(",", "key", :key) => [a, b]
122
+ # Opts.new(key: "").sliced ",", :key) => []
123
+ #
124
+ def sliced(delim, *keys)
125
+ return [] if keys.empty?
126
+ key = keys.detect { |k| key? k }
127
+ val = to_h[key]
128
+ return [] if val.nil? || val.blank?
129
+ return val if val.is_a? Array
130
+ return [val] unless val.include? delim
131
+ slice key, delim
132
+ end
133
+
134
+ # Ensure that particular key from options is a positive numer
135
+ # Opts.new("key" => "1").numeric? "key" => true
136
+ # Opts.new("key" => "0").numeric? "key" => false
137
+ # Opts.new("key" => ".").numeric? "key" => false
138
+ # Opts.new("key" => "nil").numeric? "key" => false
139
+ def numeric?(key)
140
+ to_h[key].to_i.positive?
141
+ end
79
142
  end
80
143
  end
@@ -44,8 +44,6 @@ module Lazylead
44
44
  # Copyright:: Copyright (c) 2019-2020 Yurii Dubinka
45
45
  # License:: MIT
46
46
  class Postman
47
- include Emailing
48
-
49
47
  def initialize(log = Log.new)
50
48
  @log = log
51
49
  end
@@ -53,25 +51,29 @@ module Lazylead
53
51
  # Send an email.
54
52
  # :opts :: the mail configuration like to, from, cc, subject, template.
55
53
  def send(opts)
56
- html = make_body(opts)
54
+ if opts.msg_to.empty?
55
+ @log.warn "ll-013: Email can't be sent to '#{opts.msg_to}," \
56
+ " more: '#{opts}'"
57
+ else
58
+ mail = make_email(opts)
59
+ mail.deliver
60
+ @log.debug "#{__FILE__} sent '#{mail.subject}' to '#{mail.to}'."
61
+ end
62
+ end
63
+
64
+ # Construct an email based on input arguments
65
+ def make_email(opts)
57
66
  mail = Mail.new
58
- mail.to opts[:to] || opts["to"]
59
- mail.from opts["from"]
60
- mail.cc opts["cc"] if opts.key? "cc"
67
+ mail.to opts.msg_to
68
+ mail.from opts.msg_from
69
+ mail.cc opts.msg_cc if opts.key? "cc"
61
70
  mail.subject opts["subject"]
62
71
  mail.html_part do
63
72
  content_type "text/html; charset=UTF-8"
64
- body html
73
+ body opts.msg_body
65
74
  end
66
- add_attachments mail, opts
67
- mail.deliver
68
- @log.debug "#{__FILE__} sent email based on #{opts}."
69
- end
70
-
71
- def add_attachments(mail, opts)
72
- return unless opts.key? "attachments"
73
- opts["attachments"].select { |a| File.file? a }
74
- .each { |a| mail.add_file a }
75
+ opts.msg_attachments.each { |f| mail.add_file f }
76
+ mail
75
77
  end
76
78
  end
77
79
  end
@@ -39,6 +39,7 @@ module Lazylead
39
39
  #
40
40
  class Salt
41
41
  attr_reader :id
42
+
42
43
  #
43
44
  # Each salt should be defined as a environment variable with id, like
44
45
  # salt1=E1F53135E559C253
@@ -23,6 +23,7 @@
23
23
  # OR OTHER DEALINGS IN THE SOFTWARE.
24
24
 
25
25
  require "mail"
26
+ require "colorize"
26
27
  require_relative "log"
27
28
  require_relative "salt"
28
29
 
@@ -45,7 +46,8 @@ module Lazylead
45
46
  Mail.defaults do
46
47
  delivery_method :test
47
48
  end
48
- @log.warn "SMTP connection enabled in test mode."
49
+ @log.warn "SMTP connection enabled in " \
50
+ "#{'test'.colorize(:light_yellow)} mode."
49
51
  else
50
52
  setup_smtp
51
53
  end
@@ -25,6 +25,7 @@
25
25
  require "jira-ruby"
26
26
  require "forwardable"
27
27
  require_relative "../salt"
28
+ require_relative "../opts"
28
29
 
29
30
  module Lazylead
30
31
  # Jira system for manipulation with issues.
@@ -45,16 +46,32 @@ module Lazylead
45
46
  " and salt #{@salt.id} (found=#{@salt.specified?})"
46
47
  end
47
48
 
48
- def issues(jql, opts = {})
49
+ # Find the jira issues by 'JQL'
50
+ # @param 'jql' - Jira search query
51
+ # @param 'opts' - Parameters for Jira search query.
52
+ # :max_results => maximum number of tickets per one iteration.
53
+ # :fields => ticket fields to be fetched like assignee, summary, etc.
54
+ def issues(jql, opts = { max_results: 50, fields: nil, expand: nil })
49
55
  raw do |jira|
50
- jira.Issue.jql(jql, opts).map { |i| Lazylead::Issue.new(i, jira) }
56
+ start = 0
57
+ tickets = []
58
+ total = jira.Issue.jql(jql, max_results: 0)
59
+ @log.debug "Found #{total} ticket(s) in '#{jql}'"
60
+ loop do
61
+ tickets.concat(jira.Issue.jql(jql, opts.merge(start_at: start))
62
+ .map { |i| Lazylead::Issue.new(i, jira) })
63
+ @log.debug "Fetched #{tickets.size}"
64
+ start += opts.fetch(:max_results, 50).to_i
65
+ break if start > total
66
+ end
67
+ tickets
51
68
  end
52
69
  end
53
70
 
54
71
  # Execute request to the ticketing system using raw client.
55
72
  # For Jira the raw client is 'jira-ruby' gem.
56
73
  def raw
57
- raise "ll-06: No block given to method" unless block_given?
74
+ raise "ll-009: No block given to method" unless block_given?
58
75
  yield client
59
76
  end
60
77
 
@@ -160,7 +177,7 @@ module Lazylead
160
177
  end
161
178
 
162
179
  def url
163
- @issue.attrs["self"].split("/rest/api/").first + "/browse/" + key
180
+ "#{@issue.attrs['self'].split('/rest/api/').first}/browse/#{key}"
164
181
  end
165
182
 
166
183
  def duedate
@@ -237,15 +254,23 @@ module Lazylead
237
254
 
238
255
  def add_label(label, *more)
239
256
  lbl = labels
257
+ lbl = [] if lbl.nil?
240
258
  lbl << label
241
259
  lbl += more if more.size.positive?
242
- save!("fields" => { "labels" => lbl.uniq })
260
+ labels! lbl
243
261
  end
244
262
 
263
+ # Get the labels for a particular issue
245
264
  def labels
246
265
  fields["labels"]
247
266
  end
248
267
 
268
+ # Update the labels for a particular issue
269
+ def labels!(lbl)
270
+ return if lbl.nil? || lbl.empty?
271
+ save!("fields" => { "labels" => lbl.uniq })
272
+ end
273
+
249
274
  def save!(opts)
250
275
  @issue.save(opts)
251
276
  end
@@ -324,7 +349,7 @@ module Lazylead
324
349
 
325
350
  # Execute request to the ticketing system using raw client.
326
351
  def raw
327
- raise "ll-08: No block given to method" unless block_given?
352
+ raise "ll-008: No block given to method" unless block_given?
328
353
  yield(OpenStruct.new(Issue: self))
329
354
  end
330
355
 
@@ -81,7 +81,7 @@ module Lazylead
81
81
  def post
82
82
  return if @opts.key? "silent"
83
83
  @issue.post comment
84
- @issue.add_label "LL.accuracy", grade(@accuracy)
84
+ @issue.add_label "LL.accuracy", "#{grade(@accuracy)}%"
85
85
  end
86
86
 
87
87
  # The jira comment in markdown format
@@ -114,9 +114,7 @@ module Lazylead
114
114
  end
115
115
 
116
116
  def color
117
- if colors.nil? || !defined?(@score) || !@score.is_a?(Numeric)
118
- return "#061306"
119
- end
117
+ return "#061306" if colors.nil? || !defined?(@score) || !@score.is_a?(Numeric)
120
118
  colors.reverse_each do |color|
121
119
  return color.last if @accuracy >= color.first
122
120
  end
@@ -125,12 +123,12 @@ module Lazylead
125
123
 
126
124
  def colors
127
125
  @colors ||= begin
128
- JSON.parse(@opts["colors"])
129
- .to_h
130
- .to_a
131
- .each { |e| e[0] = e[0].to_i }
132
- .sort_by { |e| e[0] }
133
- end
126
+ JSON.parse(@opts["colors"])
127
+ .to_h
128
+ .to_a
129
+ .each { |e| e[0] = e[0].to_i }
130
+ .sort_by { |e| e[0] }
131
+ end
134
132
  end
135
133
 
136
134
  # Calculate grade for accuracy
@@ -27,10 +27,6 @@ require_relative "requirement"
27
27
  module Lazylead
28
28
  # Check that ticket has an attachment.
29
29
  class Attachment < Lazylead::Requirement
30
- def initialize(desc, score, field)
31
- super(desc, score, field)
32
- end
33
-
34
30
  def passed(issue)
35
31
  issue.attachments.any?(&method(:matching))
36
32
  end
@@ -27,14 +27,18 @@ require_relative "attachment"
27
27
  module Lazylead
28
28
  # Check that ticket has log file(s) in attachment.
29
29
  class Logs < Lazylead::Attachment
30
- def initialize
30
+ def initialize(files = %w[log.zip logs.zip log.gz logs.gz log.tar.gz
31
+ logs.tar.gz log.7z logs.7z log.tar logs.tar])
31
32
  super("Log files", 2, "Attachments")
33
+ @files = files
32
34
  end
33
35
 
34
- # Ensure that ticket has a '*.log' file more '10KB'
36
+ # Ensure that ticket has a '*.log' file more '5KB'
35
37
  def matching(attachment)
36
- attachment.attrs["size"].to_i > 10_240 &&
37
- File.extname(attachment.attrs["filename"]).downcase.start_with?(".log")
38
+ name = attachment.attrs["filename"].downcase
39
+ return false unless attachment.attrs["size"].to_i > 5120
40
+ return true if File.extname(name).start_with? ".log", ".txt", ".out"
41
+ @files.any? { |l| name.end_with? l }
38
42
  end
39
43
  end
40
44
  end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The MIT License
4
+ #
5
+ # Copyright (c) 2019-2020 Yurii Dubinka
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ # of this software and associated documentation files (the "Software"),
9
+ # to deal in the Software without restriction, including without limitation
10
+ # the rights to use, copy, modify, merge, publish, distribute, sublicense,
11
+ # and/or sell copies of the Software, and to permit persons to whom
12
+ # the Software is furnished to do so, subject to the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be included
15
+ # in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ # FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
20
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
22
+ # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
23
+ # OR OTHER DEALINGS IN THE SOFTWARE.
24
+
25
+ require_relative "../../log"
26
+ require_relative "../../opts"
27
+
28
+ module Lazylead
29
+ module Task
30
+ #
31
+ # Ensure that ticket accuracy evaluation labels are set only by LL, not by
32
+ # some person.
33
+ #
34
+ # The task supports the following features:
35
+ # - fetch issues from remote ticketing system by query
36
+ # - check the history of labels modification
37
+ # - remove evaluation labels if they set not by LL
38
+ class OnlyLL
39
+ def initialize(log = Log.new)
40
+ @log = log
41
+ end
42
+
43
+ def run(sys, postman, opts)
44
+ found = sys.issues(opts["jql"],
45
+ opts.jira_defaults.merge(expand: "changelog"))
46
+ .map { |i| Labels.new(i, opts) }
47
+ .select(&:exists?)
48
+ .reject(&:valid?)
49
+ .each(&:remove)
50
+ postman.send opts.merge(tickets: found) unless found.empty?
51
+ end
52
+ end
53
+ end
54
+
55
+ # The ticket with grid labels
56
+ class Labels
57
+ attr_reader :issue
58
+
59
+ def initialize(issue, opts)
60
+ @issue = issue
61
+ @opts = opts
62
+ end
63
+
64
+ # Ensure that issue has evaluation labels for accuracy rules
65
+ def exists?
66
+ return false if @issue.labels.nil? || @issue.labels.empty?
67
+ return false unless @issue.labels.is_a? Array
68
+ grid.any? { |g| @issue.labels.any? { |l| g.eql? l } }
69
+ end
70
+
71
+ # Compare the score evaluated by LL and current ticket score.
72
+ # @return true if current score equal to LL evaluation
73
+ def valid?
74
+ score.eql?(@issue.labels.sort.find { |l| grid.any? { |g| l.eql? g } })
75
+ end
76
+
77
+ # Find expected ticket score evaluated by LL.
78
+ # If LL evaluated the same ticket several times (no matter why),
79
+ # then the last score would be returned.
80
+ def score
81
+ to_l(
82
+ @issue.history
83
+ .select { |h| h["author"]["key"].eql? @opts["author"] }
84
+ .select { |h| to_l(h) }
85
+ .reverse
86
+ .first
87
+ ).fetch("toString", "").split.find { |l| grid.any? { |g| l.eql? g } }
88
+ end
89
+
90
+ # Find history record with labels changes.
91
+ def to_l(history)
92
+ return {} if history.nil? || history.empty?
93
+ history["items"].find { |f| f["field"].eql? "labels" }
94
+ end
95
+
96
+ # Detect the percentage grid for tickets, by default its 0%, 10%, 20%, etc.
97
+ def grid
98
+ @grid ||= begin
99
+ if @opts.key? "grid"
100
+ @opts.slice("grid", ",")
101
+ else
102
+ %w[0% 10% 20% 30% 40% 50% 60% 70% 80% 90% 100%]
103
+ end
104
+ end
105
+ end
106
+
107
+ # Remove score labels from the ticket.
108
+ def remove
109
+ @issue.labels!(@issue.labels - grid)
110
+ end
111
+
112
+ # Detect the violators with their changes.
113
+ # .violators => ["Tom Hhhh set 40%", "Bob Mmmm set 50%"]
114
+ def violators
115
+ @issue.history
116
+ .reject { |h| h["author"]["key"].eql? @opts["author"] }
117
+ .select { |h| grid?(h) }
118
+ .group_by { |h| h["author"]["key"] }
119
+ .map do |a|
120
+ "#{a.last.first['author']['displayName']} set #{hacked(a.last)}"
121
+ end
122
+ end
123
+
124
+ # Ensure that history record has label change related to LL grid labels.
125
+ # @return true if LL grid labels added
126
+ def grid?(record)
127
+ diff(record).any? { |d| d.find { |l| grid.any? { |g| l.eql? g } } }
128
+ end
129
+
130
+ # Detect label diff in single history record from ticket's history.
131
+ def diff(record)
132
+ record["items"].select { |f| f["field"].eql? "labels" }
133
+ .reject { |f| f["toString"].nil? || f["toString"].blank? }
134
+ .map do |f|
135
+ from = []
136
+ from = f["fromString"].split unless f["fromString"].nil?
137
+ f["toString"].split - from
138
+ end
139
+ end
140
+
141
+ # Hacked score by violator in ticket's history.
142
+ def hacked(record)
143
+ diff(record.first)
144
+ .first
145
+ .find { |l| grid.any? { |g| l.eql? g } }
146
+ end
147
+ end
148
+ end