jirametrics 2.8 → 2.10
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.
- checksums.yaml +4 -4
- data/lib/jirametrics/aggregate_config.rb +1 -1
- data/lib/jirametrics/aging_work_bar_chart.rb +7 -5
- data/lib/jirametrics/board.rb +2 -3
- data/lib/jirametrics/board_config.rb +6 -2
- data/lib/jirametrics/chart_base.rb +36 -9
- data/lib/jirametrics/cycletime_config.rb +10 -4
- data/lib/jirametrics/cycletime_histogram.rb +65 -2
- data/lib/jirametrics/data_quality_report.rb +53 -34
- data/lib/jirametrics/downloader.rb +0 -14
- data/lib/jirametrics/examples/standard_project.rb +2 -2
- data/lib/jirametrics/exporter.rb +10 -20
- data/lib/jirametrics/file_config.rb +21 -4
- data/lib/jirametrics/file_system.rb +23 -4
- data/lib/jirametrics/groupable_issue_chart.rb +1 -3
- data/lib/jirametrics/html/aging_work_bar_chart.erb +3 -12
- data/lib/jirametrics/html/aging_work_table.erb +1 -1
- data/lib/jirametrics/html/cycletime_histogram.erb +74 -0
- data/lib/jirametrics/html/cycletime_scatterplot.erb +1 -10
- data/lib/jirametrics/html/daily_wip_chart.erb +1 -10
- data/lib/jirametrics/html/expedited_chart.erb +1 -10
- data/lib/jirametrics/html/hierarchy_table.erb +1 -1
- data/lib/jirametrics/html/sprint_burndown.erb +1 -10
- data/lib/jirametrics/html/throughput_chart.erb +1 -10
- data/lib/jirametrics/html_report_config.rb +18 -23
- data/lib/jirametrics/issue.rb +51 -27
- data/lib/jirametrics/jira_gateway.rb +16 -3
- data/lib/jirametrics/project_config.rb +102 -45
- data/lib/jirametrics/status.rb +23 -7
- data/lib/jirametrics/status_collection.rb +69 -68
- data/lib/jirametrics/value_equality.rb +2 -2
- data/lib/jirametrics.rb +0 -1
- metadata +4 -9
- data/lib/jirametrics/discard_changes_before.rb +0 -37
|
@@ -31,13 +31,22 @@ class FileSystem
|
|
|
31
31
|
File.write(filename, content)
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
def warning message
|
|
35
|
-
log "Warning: #{message}", also_write_to_stderr: true
|
|
34
|
+
def warning message, more: nil
|
|
35
|
+
log "Warning: #{message}", more: more, also_write_to_stderr: true
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
def
|
|
38
|
+
def error message, more: nil
|
|
39
|
+
log "Error: #{message}", more: more, also_write_to_stderr: true
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def log message, more: nil, also_write_to_stderr: false
|
|
43
|
+
message += " See #{logfile_name} for more details about this message." if more
|
|
44
|
+
|
|
39
45
|
logfile.puts message
|
|
40
|
-
|
|
46
|
+
logfile.puts more if more
|
|
47
|
+
return unless also_write_to_stderr
|
|
48
|
+
|
|
49
|
+
$stderr.puts message # rubocop:disable Style/StderrPuts
|
|
41
50
|
end
|
|
42
51
|
|
|
43
52
|
# In some Jira instances, a sizeable portion of the JSON is made up of empty fields. I've seen
|
|
@@ -59,4 +68,14 @@ class FileSystem
|
|
|
59
68
|
def file_exist? filename
|
|
60
69
|
File.exist? filename
|
|
61
70
|
end
|
|
71
|
+
|
|
72
|
+
def deprecated message:, date:, depth: 2
|
|
73
|
+
text = +''
|
|
74
|
+
text << "Deprecated(#{date}): "
|
|
75
|
+
text << message
|
|
76
|
+
caller(1..depth).each do |line|
|
|
77
|
+
text << "\n-> Called from #{line}"
|
|
78
|
+
end
|
|
79
|
+
log text, also_write_to_stderr: true
|
|
80
|
+
end
|
|
62
81
|
end
|
|
@@ -6,9 +6,7 @@ require 'jirametrics/grouping_rules'
|
|
|
6
6
|
module GroupableIssueChart
|
|
7
7
|
def init_configuration_block user_provided_block, &default_block
|
|
8
8
|
instance_eval(&user_provided_block)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
instance_eval(&default_block)
|
|
9
|
+
instance_eval(&default_block) unless @group_by_block
|
|
12
10
|
end
|
|
13
11
|
|
|
14
12
|
def grouping_rules &block
|
|
@@ -38,22 +38,13 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
|
38
38
|
plugins: {
|
|
39
39
|
annotation: {
|
|
40
40
|
annotations: {
|
|
41
|
-
|
|
42
|
-
holiday<%= index %>: {
|
|
43
|
-
drawTime: 'beforeDraw',
|
|
44
|
-
type: 'box',
|
|
45
|
-
xMin: '<%= range.begin %>T00:00:00',
|
|
46
|
-
xMax: '<%= range.end %>T23:59:59',
|
|
47
|
-
backgroundColor: <%= CssVariable.new('--non-working-days-color').to_json %>,
|
|
48
|
-
borderColor: <%= CssVariable.new('--non-working-days-color').to_json %>
|
|
49
|
-
},
|
|
50
|
-
<% end %>
|
|
41
|
+
<%= working_days_annotation %>
|
|
51
42
|
|
|
52
43
|
<% if percentage_line_x %>
|
|
53
44
|
line: {
|
|
54
45
|
type: 'line',
|
|
55
|
-
|
|
56
|
-
|
|
46
|
+
scaleID: 'x',
|
|
47
|
+
value: '<%= percentage_line_x %>',
|
|
57
48
|
borderColor: <%= CssVariable.new('--aging-work-bar-chart-percentage-line-color').to_json %>,
|
|
58
49
|
borderWidth: 1,
|
|
59
50
|
drawTime: 'afterDraw'
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
</div>
|
|
41
41
|
<% end %>
|
|
42
42
|
</td>
|
|
43
|
-
<td><%= format_status issue.status
|
|
43
|
+
<td><%= format_status issue.status, board: issue.board %></td>
|
|
44
44
|
<td><%= fix_versions_text(issue) %></td>
|
|
45
45
|
<% if any_scrum_boards %>
|
|
46
46
|
<td><%= sprints_text(issue) %></td>
|
|
@@ -1,6 +1,57 @@
|
|
|
1
1
|
<div class="chart">
|
|
2
2
|
<canvas id="<%= chart_id %>" width="<%= canvas_width %>" height="<%= canvas_height %>"></canvas>
|
|
3
3
|
</div>
|
|
4
|
+
<%
|
|
5
|
+
if show_stats
|
|
6
|
+
link_id = next_id
|
|
7
|
+
issues_id = next_id
|
|
8
|
+
%>
|
|
9
|
+
[<a id='<%= link_id %>' href="#" onclick='expand_collapse("<%= link_id %>", "<%= issues_id %>"); return false;'>Show details</a>]
|
|
10
|
+
<div id="<%= issues_id %>" style="display: none;">
|
|
11
|
+
<div>
|
|
12
|
+
<table class="standard">
|
|
13
|
+
<tr>
|
|
14
|
+
<th>Issue Type</th>
|
|
15
|
+
<th>Min</th>
|
|
16
|
+
<th>Max</th>
|
|
17
|
+
<th>Avg</th>
|
|
18
|
+
<th>Mode</th>
|
|
19
|
+
<% percentiles.each do |p| %>
|
|
20
|
+
<th><%= p %>th</th>
|
|
21
|
+
<% end %>
|
|
22
|
+
</tr>
|
|
23
|
+
<% the_stats.each do |k, v| %>
|
|
24
|
+
<tr>
|
|
25
|
+
<td><%= k %></td>
|
|
26
|
+
<td style="text-align: right;"><%= v[:min] %></td>
|
|
27
|
+
<td style="text-align: right;"><%= v[:max] %></td>
|
|
28
|
+
<td style="text-align: right;"><%= sprintf('%.2f', v[:average]) %></td>
|
|
29
|
+
<td><%= v[:mode].join(', ') %></td>
|
|
30
|
+
<% percentiles.each do |p| %>
|
|
31
|
+
<td style="text-align: right;"><%= v[:percentiles][p] %></td>
|
|
32
|
+
<% end %>
|
|
33
|
+
</tr>
|
|
34
|
+
<% end %>
|
|
35
|
+
</table>
|
|
36
|
+
</div>
|
|
37
|
+
<div>
|
|
38
|
+
<p>These statistics help understand the <i>"shape"</i> of the cycletime histogram distribution, to help us with predictions.</p>
|
|
39
|
+
<ul>
|
|
40
|
+
<li><b>Min & Max:</b> the observed spread for the data set. Useful to judge how wide the variation is. </li>
|
|
41
|
+
<li><b>Average:</b> the arithmetic mean of the data set. Useful as a <i>"typical representative"</i> of the complete set.</li>
|
|
42
|
+
<li><b>Mode:</b> the most repeated value(s) in the data set. This is the value we're most likely to remember. </li>
|
|
43
|
+
<li><b>Percentiles:</b> they partition the data set. If X is the Nth percentile, it means that N% of cycletime values are X or less. Typical percentiles of interest are:</li>
|
|
44
|
+
<ul>
|
|
45
|
+
<li><b>50%</b>: also known as the <b>Median</b>. Useful to establish short feedback loops, to monitor that it's not drifting to the right.</li>
|
|
46
|
+
<li><b>85%</b>: useful to establish service level expectations, accounting for rare events..</li>
|
|
47
|
+
<li><b>98% (or higher)</b>: useful to gauge worst case expectations..</li>
|
|
48
|
+
</ul>
|
|
49
|
+
</ul>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
<%
|
|
53
|
+
end
|
|
54
|
+
%>
|
|
4
55
|
<script>
|
|
5
56
|
new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
6
57
|
{
|
|
@@ -21,6 +72,8 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
|
21
72
|
grid: {
|
|
22
73
|
color: <%= CssVariable['--grid-line-color'].to_json %>
|
|
23
74
|
},
|
|
75
|
+
min: 0,
|
|
76
|
+
offset: false, // Gets rid of the ugly padding on left.
|
|
24
77
|
},
|
|
25
78
|
y: {
|
|
26
79
|
stacked: true,
|
|
@@ -34,6 +87,27 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
|
34
87
|
}
|
|
35
88
|
},
|
|
36
89
|
plugins: {
|
|
90
|
+
annotation: {
|
|
91
|
+
annotations: {
|
|
92
|
+
<%
|
|
93
|
+
results = the_stats[:all][:percentiles]
|
|
94
|
+
results.each do |percentile, value|
|
|
95
|
+
%>
|
|
96
|
+
percentile<%= percentile.to_s %>: {
|
|
97
|
+
type: 'line',
|
|
98
|
+
scaleID: 'x',
|
|
99
|
+
value: <%= value %>,
|
|
100
|
+
borderWidth: 1,
|
|
101
|
+
drawTime: 'beforeDatasetsDraw',
|
|
102
|
+
label: {
|
|
103
|
+
enabled: true,
|
|
104
|
+
content: '<%= "#{percentile}%" %>',
|
|
105
|
+
position: 'start',
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
<% end %>
|
|
109
|
+
},
|
|
110
|
+
},
|
|
37
111
|
tooltip: {
|
|
38
112
|
callbacks: {
|
|
39
113
|
label: function(context) {
|
|
@@ -53,16 +53,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
53
53
|
autocolors: false,
|
|
54
54
|
annotation: {
|
|
55
55
|
annotations: {
|
|
56
|
-
|
|
57
|
-
holiday<%= index %>: {
|
|
58
|
-
drawTime: 'beforeDraw',
|
|
59
|
-
type: 'box',
|
|
60
|
-
xMin: '<%= range.begin %>T00:00:00',
|
|
61
|
-
xMax: '<%= range.end %>T23:59:59',
|
|
62
|
-
backgroundColor: <%= CssVariable.new('--non-working-days-color').to_json %>,
|
|
63
|
-
borderColor: <%= CssVariable.new('--non-working-days-color').to_json %>
|
|
64
|
-
},
|
|
65
|
-
<% end %>
|
|
56
|
+
<%= working_days_annotation %>
|
|
66
57
|
|
|
67
58
|
<% @percentage_lines.each_with_index do |args, index| %>
|
|
68
59
|
<% percent, color = args %>
|
|
@@ -50,16 +50,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'),
|
|
|
50
50
|
},
|
|
51
51
|
annotation: {
|
|
52
52
|
annotations: {
|
|
53
|
-
|
|
54
|
-
holiday<%= index %>: {
|
|
55
|
-
drawTime: 'beforeDraw',
|
|
56
|
-
type: 'box',
|
|
57
|
-
xMin: '<%= range.begin %>T00:00:00',
|
|
58
|
-
xMax: '<%= range.end %>T23:59:59',
|
|
59
|
-
backgroundColor: <%= CssVariable.new('--non-working-days-color').to_json %>,
|
|
60
|
-
borderColor: <%= CssVariable.new('--non-working-days-color').to_json %>
|
|
61
|
-
},
|
|
62
|
-
<% end %>
|
|
53
|
+
<%= working_days_annotation %>
|
|
63
54
|
}
|
|
64
55
|
},
|
|
65
56
|
legend: {
|
|
@@ -55,16 +55,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
55
55
|
autocolors: false,
|
|
56
56
|
annotation: {
|
|
57
57
|
annotations: {
|
|
58
|
-
|
|
59
|
-
holiday<%= index %>: {
|
|
60
|
-
drawTime: 'beforeDraw',
|
|
61
|
-
type: 'box',
|
|
62
|
-
xMin: '<%= range.begin %>T00:00:00',
|
|
63
|
-
xMax: '<%= range.end %>T23:59:59',
|
|
64
|
-
backgroundColor: <%= CssVariable.new('--non-working-days-color').to_json %>,
|
|
65
|
-
borderColor: <%= CssVariable.new('--non-working-days-color').to_json %>
|
|
66
|
-
},
|
|
67
|
-
<% end %>
|
|
58
|
+
<%= working_days_annotation %>
|
|
68
59
|
}
|
|
69
60
|
}
|
|
70
61
|
}
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
</span>
|
|
23
23
|
</td>
|
|
24
24
|
<td><span style="color: <%= color %>; font-style: italic;"><%= issue.summary[0..80] %></span></td>
|
|
25
|
-
<td><%= format_status issue.status
|
|
25
|
+
<td><%= format_status issue.status, board: issue.board %></td>
|
|
26
26
|
</tr>
|
|
27
27
|
<% end %>
|
|
28
28
|
</tbody>
|
|
@@ -56,16 +56,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
56
56
|
},
|
|
57
57
|
annotation: {
|
|
58
58
|
annotations: {
|
|
59
|
-
|
|
60
|
-
holiday<%= index %>: {
|
|
61
|
-
drawTime: 'beforeDraw',
|
|
62
|
-
type: 'box',
|
|
63
|
-
xMin: '<%= range.begin %>T00:00:00',
|
|
64
|
-
xMax: '<%= range.end %>T23:59:59',
|
|
65
|
-
backgroundColor: <%= CssVariable.new('--non-working-days-color').to_json %>,
|
|
66
|
-
borderColor: <%= CssVariable.new('--non-working-days-color').to_json %>
|
|
67
|
-
},
|
|
68
|
-
<% end %>
|
|
59
|
+
<%= working_days_annotation %>
|
|
69
60
|
}
|
|
70
61
|
}
|
|
71
62
|
}
|
|
@@ -52,16 +52,7 @@ new Chart(document.getElementById('<%= chart_id %>').getContext('2d'), {
|
|
|
52
52
|
},
|
|
53
53
|
annotation: {
|
|
54
54
|
annotations: {
|
|
55
|
-
|
|
56
|
-
holiday<%= index %>: {
|
|
57
|
-
drawTime: 'beforeDraw',
|
|
58
|
-
type: 'box',
|
|
59
|
-
xMin: '<%= range.begin %>T00:00:00',
|
|
60
|
-
xMax: '<%= range.end %>T23:59:59',
|
|
61
|
-
backgroundColor: <%= CssVariable.new('--non-working-days-color').to_json %>,
|
|
62
|
-
borderColor: <%= CssVariable.new('--non-working-days-color').to_json %>
|
|
63
|
-
},
|
|
64
|
-
<% end %>
|
|
55
|
+
<%= working_days_annotation %>
|
|
65
56
|
}
|
|
66
57
|
}
|
|
67
58
|
}
|
|
@@ -5,16 +5,15 @@ require 'jirametrics/self_or_issue_dispatcher'
|
|
|
5
5
|
|
|
6
6
|
class HtmlReportConfig
|
|
7
7
|
include SelfOrIssueDispatcher
|
|
8
|
-
include DiscardChangesBefore
|
|
9
8
|
|
|
10
|
-
attr_reader :file_config, :sections
|
|
9
|
+
attr_reader :file_config, :sections, :charts
|
|
11
10
|
|
|
12
11
|
def self.define_chart name:, classname:, deprecated_warning: nil, deprecated_date: nil
|
|
13
12
|
lines = []
|
|
14
13
|
lines << "def #{name} &block"
|
|
15
14
|
lines << ' block = ->(_) {} unless block'
|
|
16
15
|
if deprecated_warning
|
|
17
|
-
lines << " deprecated date: #{deprecated_date.inspect}, message: #{deprecated_warning.inspect}"
|
|
16
|
+
lines << " file_system.deprecated date: #{deprecated_date.inspect}, message: #{deprecated_warning.inspect}"
|
|
18
17
|
end
|
|
19
18
|
lines << " execute_chart #{classname}.new(block)"
|
|
20
19
|
lines << 'end'
|
|
@@ -43,14 +42,15 @@ class HtmlReportConfig
|
|
|
43
42
|
def initialize file_config:, block:
|
|
44
43
|
@file_config = file_config
|
|
45
44
|
@block = block
|
|
46
|
-
@sections = []
|
|
45
|
+
@sections = [] # Where we store the chunks of text that will be assembled into the HTML
|
|
46
|
+
@charts = [] # Where we store all the charts we executed so we can assert against them.
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def cycletime label = nil, &block
|
|
50
50
|
@file_config.project_config.all_boards.each_value do |board|
|
|
51
51
|
raise 'Multiple cycletimes not supported' if board.cycletime
|
|
52
52
|
|
|
53
|
-
board.cycletime = CycleTimeConfig.new(parent_config: self, label: label, block: block)
|
|
53
|
+
board.cycletime = CycleTimeConfig.new(parent_config: self, label: label, block: block, file_system: file_system)
|
|
54
54
|
end
|
|
55
55
|
end
|
|
56
56
|
|
|
@@ -64,7 +64,7 @@ class HtmlReportConfig
|
|
|
64
64
|
|
|
65
65
|
# The quality report has to be generated last because otherwise cycletime won't have been
|
|
66
66
|
# set. Then we have to rotate it to the first position so it's at the top of the report.
|
|
67
|
-
execute_chart DataQualityReport.new(
|
|
67
|
+
execute_chart DataQualityReport.new(file_config.project_config.discarded_changes_data)
|
|
68
68
|
@sections.rotate!(-1)
|
|
69
69
|
|
|
70
70
|
html create_footer
|
|
@@ -101,9 +101,8 @@ class HtmlReportConfig
|
|
|
101
101
|
base_css
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
-
def board_id id
|
|
105
|
-
@board_id = id
|
|
106
|
-
@board_id
|
|
104
|
+
def board_id id
|
|
105
|
+
@board_id = id
|
|
107
106
|
end
|
|
108
107
|
|
|
109
108
|
def timezone_offset
|
|
@@ -143,19 +142,6 @@ class HtmlReportConfig
|
|
|
143
142
|
end
|
|
144
143
|
end
|
|
145
144
|
|
|
146
|
-
def discard_changes_before_hook issues_cutoff_times
|
|
147
|
-
# raise 'Cycletime must be defined before using discard_changes_before' unless @cycletime
|
|
148
|
-
|
|
149
|
-
@original_issue_times = {}
|
|
150
|
-
issues_cutoff_times.each do |issue, cutoff_time|
|
|
151
|
-
started = issue.board.cycletime.started_stopped_times(issue).first
|
|
152
|
-
if started && started <= cutoff_time
|
|
153
|
-
# We only need to log this if data was discarded
|
|
154
|
-
@original_issue_times[issue] = { cutoff_time: cutoff_time, started_time: started }
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
|
|
159
145
|
def dependency_chart &block
|
|
160
146
|
execute_chart DependencyChart.new block
|
|
161
147
|
end
|
|
@@ -175,7 +161,7 @@ class HtmlReportConfig
|
|
|
175
161
|
chart.settings = settings
|
|
176
162
|
|
|
177
163
|
chart.all_boards = project_config.all_boards
|
|
178
|
-
chart.board_id = find_board_id
|
|
164
|
+
chart.board_id = find_board_id
|
|
179
165
|
chart.holiday_dates = project_config.exporter.holiday_dates
|
|
180
166
|
|
|
181
167
|
time_range = @file_config.project_config.time_range
|
|
@@ -184,6 +170,7 @@ class HtmlReportConfig
|
|
|
184
170
|
|
|
185
171
|
after_init_block&.call chart
|
|
186
172
|
|
|
173
|
+
@charts << chart
|
|
187
174
|
html chart.run
|
|
188
175
|
end
|
|
189
176
|
|
|
@@ -216,4 +203,12 @@ class HtmlReportConfig
|
|
|
216
203
|
</section>
|
|
217
204
|
HTML
|
|
218
205
|
end
|
|
206
|
+
|
|
207
|
+
def discard_changes_before status_becomes: nil, &block
|
|
208
|
+
file_system.deprecated(
|
|
209
|
+
date: '2025-01-09',
|
|
210
|
+
message: 'discard_changes_before is now only supported at the project level'
|
|
211
|
+
)
|
|
212
|
+
file_config.project_config.discard_changes_before status_becomes: status_becomes, &block
|
|
213
|
+
end
|
|
219
214
|
end
|
data/lib/jirametrics/issue.rb
CHANGED
|
@@ -13,7 +13,12 @@ class Issue
|
|
|
13
13
|
@changes = []
|
|
14
14
|
@board = board
|
|
15
15
|
|
|
16
|
+
# We only check for this here because if a board isn't passed in then things will fail much
|
|
17
|
+
# later and be hard to find. Let's find out early.
|
|
16
18
|
raise "No board for issue #{key}" if board.nil?
|
|
19
|
+
|
|
20
|
+
# There are cases where we create an Issue of fragments like linked issues and those won't have
|
|
21
|
+
# changelogs.
|
|
17
22
|
return unless @raw['changelog']
|
|
18
23
|
|
|
19
24
|
load_history_into_changes
|
|
@@ -93,9 +98,7 @@ class Issue
|
|
|
93
98
|
|
|
94
99
|
def still_in
|
|
95
100
|
result = nil
|
|
96
|
-
|
|
97
|
-
next unless change.status?
|
|
98
|
-
|
|
101
|
+
status_changes.each do |change|
|
|
99
102
|
current_status_matched = yield change
|
|
100
103
|
|
|
101
104
|
if current_status_matched && result.nil?
|
|
@@ -117,63 +120,71 @@ class Issue
|
|
|
117
120
|
|
|
118
121
|
# If it ever entered one of these categories and it's still there then what was the last time it entered
|
|
119
122
|
def still_in_status_category *category_names
|
|
123
|
+
category_ids = find_status_category_ids_by_names category_names
|
|
124
|
+
|
|
120
125
|
still_in do |change|
|
|
121
|
-
status =
|
|
122
|
-
|
|
126
|
+
status = find_or_create_status id: change.value_id, name: change.value
|
|
127
|
+
category_ids.include? status.category.id
|
|
123
128
|
end
|
|
124
129
|
end
|
|
125
130
|
|
|
126
131
|
def most_recent_status_change
|
|
132
|
+
# We artificially insert a status change to represent creation so by definition there will always be at least one.
|
|
127
133
|
changes.reverse.find { |change| change.status? }
|
|
128
134
|
end
|
|
129
135
|
|
|
130
136
|
# Are we currently in this status? If yes, then return the most recent status change.
|
|
131
137
|
def currently_in_status *status_names
|
|
132
138
|
change = most_recent_status_change
|
|
133
|
-
return false if change.nil?
|
|
134
139
|
|
|
135
140
|
change if change.current_status_matches(*status_names)
|
|
136
141
|
end
|
|
137
142
|
|
|
138
143
|
# Are we currently in this status category? If yes, then return the most recent status change.
|
|
139
144
|
def currently_in_status_category *category_names
|
|
145
|
+
category_ids = find_status_category_ids_by_names category_names
|
|
146
|
+
|
|
140
147
|
change = most_recent_status_change
|
|
141
|
-
return false if change.nil?
|
|
142
148
|
|
|
143
|
-
status =
|
|
144
|
-
change if status &&
|
|
149
|
+
status = find_or_create_status id: change.value_id, name: change.value
|
|
150
|
+
change if status && category_ids.include?(status.category.id)
|
|
145
151
|
end
|
|
146
152
|
|
|
147
|
-
def
|
|
153
|
+
def find_or_create_status id:, name:
|
|
148
154
|
status = board.possible_statuses.find_by_id(id)
|
|
149
|
-
return status if status
|
|
150
155
|
|
|
151
|
-
status
|
|
156
|
+
unless status
|
|
157
|
+
# Have to pull this list before the call to fabricate or else the warning will incorrectly
|
|
158
|
+
# list this status as one it actually found
|
|
159
|
+
found_statuses = board.possible_statuses.to_s
|
|
160
|
+
|
|
161
|
+
status = board.possible_statuses.fabricate_status_for id: id, name: name
|
|
152
162
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
+
message = +'The history for issue '
|
|
164
|
+
message << key
|
|
165
|
+
message << ' references the status ('
|
|
166
|
+
message << "#{name.inspect}:#{id.inspect}"
|
|
167
|
+
message << ') that can\'t be found. We are guessing that this belongs to the '
|
|
168
|
+
message << status.category.to_s
|
|
169
|
+
message << ' status category but that may be wrong. See https://jirametrics.org/faq/#q1 for more '
|
|
170
|
+
message << 'details on defining statuses.'
|
|
171
|
+
board.project_config.file_system.warning message, more: "The statuses we did find are: #{found_statuses}"
|
|
172
|
+
end
|
|
163
173
|
|
|
164
174
|
status
|
|
165
175
|
end
|
|
166
176
|
|
|
167
177
|
def first_status_change_after_created
|
|
168
|
-
|
|
178
|
+
status_changes.find { |change| change.artificial? == false }
|
|
169
179
|
end
|
|
170
180
|
|
|
171
181
|
def first_time_in_status_category *category_names
|
|
172
|
-
|
|
173
|
-
next unless change.status?
|
|
182
|
+
category_ids = find_status_category_ids_by_names category_names
|
|
174
183
|
|
|
175
|
-
|
|
176
|
-
|
|
184
|
+
status_changes.each do |change|
|
|
185
|
+
to_status = find_or_create_status(id: change.value_id, name: change.value)
|
|
186
|
+
id = to_status.category.id
|
|
187
|
+
return change if category_ids.include? id
|
|
177
188
|
end
|
|
178
189
|
nil
|
|
179
190
|
end
|
|
@@ -645,6 +656,10 @@ class Issue
|
|
|
645
656
|
end
|
|
646
657
|
end
|
|
647
658
|
|
|
659
|
+
def status_changes
|
|
660
|
+
@changes.select { |change| change.status? }
|
|
661
|
+
end
|
|
662
|
+
|
|
648
663
|
private
|
|
649
664
|
|
|
650
665
|
def assemble_author raw
|
|
@@ -720,4 +735,13 @@ class Issue
|
|
|
720
735
|
'toString' => first_status
|
|
721
736
|
}
|
|
722
737
|
end
|
|
738
|
+
|
|
739
|
+
def find_status_category_ids_by_names category_names
|
|
740
|
+
category_names.filter_map do |name|
|
|
741
|
+
list = board.possible_statuses.find_all_categories_by_name name
|
|
742
|
+
raise "No status categories found for name: #{name}" if list.empty?
|
|
743
|
+
|
|
744
|
+
list
|
|
745
|
+
end.flatten.collect(&:id)
|
|
746
|
+
end
|
|
723
747
|
end
|
|
@@ -14,9 +14,15 @@ class JiraGateway
|
|
|
14
14
|
def call_url relative_url:
|
|
15
15
|
command = make_curl_command url: "#{@jira_url}#{relative_url}"
|
|
16
16
|
result = call_command command
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
begin
|
|
18
|
+
json = JSON.parse(result)
|
|
19
|
+
rescue # rubocop:disable Style/RescueStandardError
|
|
20
|
+
raise "Error when parsing result: #{result.inspect}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
raise "Download failed with: #{JSON.pretty_generate(json)}" unless json_successful?(json)
|
|
24
|
+
|
|
25
|
+
json
|
|
20
26
|
end
|
|
21
27
|
|
|
22
28
|
def call_command command
|
|
@@ -61,4 +67,11 @@ class JiraGateway
|
|
|
61
67
|
command << " --url \"#{url}\""
|
|
62
68
|
command
|
|
63
69
|
end
|
|
70
|
+
|
|
71
|
+
def json_successful? json
|
|
72
|
+
return false if json.is_a?(Hash) && (json['error'] || json['errorMessages'] || json['errorMessage'])
|
|
73
|
+
return false if json.is_a?(Array) && json.first == 'errorMessage'
|
|
74
|
+
|
|
75
|
+
true
|
|
76
|
+
end
|
|
64
77
|
end
|