lazylead 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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