jirametrics 2.11 → 2.14
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/aging_work_table.rb +6 -11
- data/lib/jirametrics/atlassian_document_format.rb +160 -0
- data/lib/jirametrics/board.rb +8 -9
- data/lib/jirametrics/board_config.rb +2 -0
- data/lib/jirametrics/board_movement_calculator.rb +8 -0
- data/lib/jirametrics/change_item.rb +30 -15
- data/lib/jirametrics/chart_base.rb +5 -4
- data/lib/jirametrics/cycletime_config.rb +1 -1
- data/lib/jirametrics/daily_view.rb +295 -0
- data/lib/jirametrics/downloader.rb +61 -21
- data/lib/jirametrics/estimate_accuracy_chart.rb +34 -10
- data/lib/jirametrics/estimation_configuration.rb +25 -0
- data/lib/jirametrics/examples/standard_project.rb +2 -0
- data/lib/jirametrics/file_config.rb +1 -1
- data/lib/jirametrics/html/aging_work_table.erb +2 -0
- data/lib/jirametrics/html/index.css +76 -0
- data/lib/jirametrics/html/index.erb +19 -2
- data/lib/jirametrics/html_report_config.rb +2 -0
- data/lib/jirametrics/issue.rb +42 -23
- data/lib/jirametrics/issue_collection.rb +33 -0
- data/lib/jirametrics/jira_gateway.rb +8 -1
- data/lib/jirametrics/project_config.rb +44 -10
- data/lib/jirametrics/settings.json +2 -1
- data/lib/jirametrics/sprint.rb +1 -0
- data/lib/jirametrics/sprint_burndown.rb +35 -33
- data/lib/jirametrics/sprint_issue_change_data.rb +3 -3
- data/lib/jirametrics/status_collection.rb +7 -0
- data/lib/jirametrics/user.rb +12 -0
- data/lib/jirametrics.rb +5 -0
- metadata +7 -2
|
@@ -45,6 +45,7 @@ class Downloader
|
|
|
45
45
|
board = download_board_configuration board_id: id
|
|
46
46
|
download_issues board: board
|
|
47
47
|
end
|
|
48
|
+
download_users
|
|
48
49
|
|
|
49
50
|
save_metadata
|
|
50
51
|
end
|
|
@@ -96,30 +97,59 @@ class Downloader
|
|
|
96
97
|
log " JQL: #{jql}"
|
|
97
98
|
escaped_jql = CGI.escape jql
|
|
98
99
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
json = @jira_gateway.call_url relative_url: '/rest/api/2/search' \
|
|
104
|
-
"?jql=#{escaped_jql}&maxResults=#{max_results}&startAt=#{start_at}&expand=changelog&fields=*all"
|
|
105
|
-
|
|
106
|
-
json['issues'].each do |issue_json|
|
|
107
|
-
issue_json['exporter'] = {
|
|
108
|
-
'in_initial_query' => initial_query
|
|
109
|
-
}
|
|
110
|
-
identify_other_issues_to_be_downloaded raw_issue: issue_json, board: board
|
|
111
|
-
file = "#{issue_json['key']}-#{board.id}.json"
|
|
112
|
-
|
|
113
|
-
@file_system.save_json(json: issue_json, filename: File.join(path, file))
|
|
114
|
-
end
|
|
100
|
+
if @jira_gateway.cloud?
|
|
101
|
+
max_results = 5_000 # The maximum allowed by Jira
|
|
102
|
+
next_page_token = nil
|
|
103
|
+
issue_count = 0
|
|
115
104
|
|
|
116
|
-
|
|
117
|
-
|
|
105
|
+
loop do
|
|
106
|
+
json = @jira_gateway.call_url relative_url: '/rest/api/3/search/jql' \
|
|
107
|
+
"?jql=#{escaped_jql}&maxResults=#{max_results}&" \
|
|
108
|
+
"nextPageToken=#{next_page_token}&expand=changelog&fields=*all"
|
|
109
|
+
next_page_token = json['nextPageToken']
|
|
118
110
|
|
|
119
|
-
|
|
120
|
-
|
|
111
|
+
json['issues'].each do |issue_json|
|
|
112
|
+
issue_json['exporter'] = {
|
|
113
|
+
'in_initial_query' => initial_query
|
|
114
|
+
}
|
|
115
|
+
identify_other_issues_to_be_downloaded raw_issue: issue_json, board: board
|
|
116
|
+
file = "#{issue_json['key']}-#{board.id}.json"
|
|
121
117
|
|
|
122
|
-
|
|
118
|
+
@file_system.save_json(json: issue_json, filename: File.join(path, file))
|
|
119
|
+
issue_count += 1
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
message = " Downloaded #{issue_count} issues"
|
|
123
|
+
log message, both: true
|
|
124
|
+
|
|
125
|
+
break unless next_page_token
|
|
126
|
+
end
|
|
127
|
+
else
|
|
128
|
+
max_results = 100
|
|
129
|
+
start_at = 0
|
|
130
|
+
total = 1
|
|
131
|
+
while start_at < total
|
|
132
|
+
json = @jira_gateway.call_url relative_url: '/rest/api/2/search' \
|
|
133
|
+
"?jql=#{escaped_jql}&maxResults=#{max_results}&startAt=#{start_at}&expand=changelog&fields=*all"
|
|
134
|
+
|
|
135
|
+
json['issues'].each do |issue_json|
|
|
136
|
+
issue_json['exporter'] = {
|
|
137
|
+
'in_initial_query' => initial_query
|
|
138
|
+
}
|
|
139
|
+
identify_other_issues_to_be_downloaded raw_issue: issue_json, board: board
|
|
140
|
+
file = "#{issue_json['key']}-#{board.id}.json"
|
|
141
|
+
|
|
142
|
+
@file_system.save_json(json: issue_json, filename: File.join(path, file))
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
total = json['total'].to_i
|
|
146
|
+
max_results = json['maxResults']
|
|
147
|
+
|
|
148
|
+
message = " Downloaded #{start_at + 1}-#{[start_at + max_results, total].min} of #{total} issues to #{path} "
|
|
149
|
+
log message, both: true
|
|
150
|
+
|
|
151
|
+
start_at += json['issues'].size
|
|
152
|
+
end
|
|
123
153
|
end
|
|
124
154
|
end
|
|
125
155
|
|
|
@@ -147,6 +177,16 @@ class Downloader
|
|
|
147
177
|
)
|
|
148
178
|
end
|
|
149
179
|
|
|
180
|
+
def download_users
|
|
181
|
+
log ' Downloading all users', both: true
|
|
182
|
+
json = @jira_gateway.call_url relative_url: '/rest/api/2/users'
|
|
183
|
+
|
|
184
|
+
@file_system.save_json(
|
|
185
|
+
json: json,
|
|
186
|
+
filename: File.join(@target_path, "#{file_prefix}_users.json")
|
|
187
|
+
)
|
|
188
|
+
end
|
|
189
|
+
|
|
150
190
|
def update_status_history_file
|
|
151
191
|
status_filename = File.join(@target_path, "#{file_prefix}_statuses.json")
|
|
152
192
|
return unless file_system.file_exist? status_filename
|
|
@@ -22,15 +22,18 @@ class EstimateAccuracyChart < ChartBase
|
|
|
22
22
|
</div>
|
|
23
23
|
HTML
|
|
24
24
|
|
|
25
|
-
@y_axis_label = 'Story Point Estimates'
|
|
26
25
|
@y_axis_type = 'linear'
|
|
27
|
-
@y_axis_block = ->(issue, start_time) {
|
|
26
|
+
@y_axis_block = ->(issue, start_time) { estimate_at(issue: issue, start_time: start_time)&.to_f }
|
|
28
27
|
@y_axis_sort_order = nil
|
|
29
28
|
|
|
30
29
|
instance_eval(&configuration_block)
|
|
31
30
|
end
|
|
32
31
|
|
|
33
32
|
def run
|
|
33
|
+
if @y_axis_label.nil?
|
|
34
|
+
text = current_board.estimation_configuration.units == :story_points ? 'Story Points' : 'Days'
|
|
35
|
+
@y_axis_label = "Estimated #{text}"
|
|
36
|
+
end
|
|
34
37
|
data_sets = scan_issues
|
|
35
38
|
|
|
36
39
|
return '' if data_sets.empty?
|
|
@@ -41,6 +44,7 @@ class EstimateAccuracyChart < ChartBase
|
|
|
41
44
|
def scan_issues
|
|
42
45
|
completed_hash, aging_hash = split_into_completed_and_aging issues: issues
|
|
43
46
|
|
|
47
|
+
estimation_units = current_board.estimation_configuration.units
|
|
44
48
|
@has_aging_data = !aging_hash.empty?
|
|
45
49
|
|
|
46
50
|
[
|
|
@@ -53,9 +57,13 @@ class EstimateAccuracyChart < ChartBase
|
|
|
53
57
|
# We sort so that the smaller circles are in front of the bigger circles.
|
|
54
58
|
data = hash.sort(&hash_sorter).collect do |key, values|
|
|
55
59
|
estimate, cycle_time = *key
|
|
56
|
-
|
|
57
|
-
title = [
|
|
58
|
-
|
|
60
|
+
|
|
61
|
+
title = [
|
|
62
|
+
"Estimate: #{estimate_label(estimate: estimate, estimation_units: estimation_units)}, " \
|
|
63
|
+
"Cycletime: #{label_days(cycle_time)}, " \
|
|
64
|
+
"#{values.size} issues"
|
|
65
|
+
] + values.collect { |issue| "#{issue.key}: #{issue.summary}" }
|
|
66
|
+
|
|
59
67
|
{
|
|
60
68
|
'x' => cycle_time,
|
|
61
69
|
'y' => estimate,
|
|
@@ -77,6 +85,18 @@ class EstimateAccuracyChart < ChartBase
|
|
|
77
85
|
end
|
|
78
86
|
end
|
|
79
87
|
|
|
88
|
+
def estimate_label estimate:, estimation_units:
|
|
89
|
+
if @y_axis_type == 'linear'
|
|
90
|
+
if estimation_units == :story_points
|
|
91
|
+
estimate_label = "#{estimate}pts"
|
|
92
|
+
elsif estimation_units == :seconds
|
|
93
|
+
estimate_label = label_days estimate
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
estimate_label = estimate.to_s if estimate_label.nil?
|
|
97
|
+
estimate_label
|
|
98
|
+
end
|
|
99
|
+
|
|
80
100
|
def split_into_completed_and_aging issues:
|
|
81
101
|
aging_hash = {}
|
|
82
102
|
completed_hash = {}
|
|
@@ -126,14 +146,18 @@ class EstimateAccuracyChart < ChartBase
|
|
|
126
146
|
end
|
|
127
147
|
end
|
|
128
148
|
|
|
129
|
-
def
|
|
130
|
-
|
|
149
|
+
def estimate_at issue:, start_time:, estimation_configuration: current_board.estimation_configuration
|
|
150
|
+
estimate = nil
|
|
151
|
+
|
|
131
152
|
issue.changes.each do |change|
|
|
132
|
-
return
|
|
153
|
+
return estimate if change.time >= start_time
|
|
133
154
|
|
|
134
|
-
|
|
155
|
+
if change.field == estimation_configuration.display_name || change.field == estimation_configuration.field_id
|
|
156
|
+
estimate = change.value
|
|
157
|
+
estimate = estimate.to_f / (24 * 60 * 60) if estimation_configuration.units == :seconds
|
|
158
|
+
end
|
|
135
159
|
end
|
|
136
|
-
|
|
160
|
+
estimate
|
|
137
161
|
end
|
|
138
162
|
|
|
139
163
|
def y_axis label:, sort_order: nil, &block
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class EstimationConfiguration
|
|
4
|
+
attr_reader :units, :display_name, :field_id
|
|
5
|
+
|
|
6
|
+
def initialize raw:
|
|
7
|
+
@units = :story_points
|
|
8
|
+
@display_name = 'Story Points'
|
|
9
|
+
|
|
10
|
+
# If there wasn't an estimation section they rely on all defaults
|
|
11
|
+
return if raw.nil?
|
|
12
|
+
|
|
13
|
+
if raw['type'] == 'field'
|
|
14
|
+
@field_id = raw['field']['fieldId']
|
|
15
|
+
@display_name = raw['field']['displayName']
|
|
16
|
+
if @field_id == 'timeoriginalestimate'
|
|
17
|
+
@units = :seconds
|
|
18
|
+
@display_name = 'Original estimate'
|
|
19
|
+
end
|
|
20
|
+
elsif raw['type'] == 'issueCount'
|
|
21
|
+
@display_name = 'Issue Count'
|
|
22
|
+
@units = :issue_count
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
<th title="Age in days">Age</th>
|
|
5
5
|
<th title="Expedited">E</th>
|
|
6
6
|
<th title="Blocked / Stalled">B/S</th>
|
|
7
|
+
<th title="Priority">P</th>
|
|
7
8
|
<th>Issue</th>
|
|
8
9
|
<th>Status</th>
|
|
9
10
|
<th>Forecast</th>
|
|
@@ -29,6 +30,7 @@
|
|
|
29
30
|
<td style="text-align: right;"><%= issue_age || 'Not started' %></td>
|
|
30
31
|
<td><%= expedited_text(issue) %></td>
|
|
31
32
|
<td><%= blocked_text(issue) %></td>
|
|
33
|
+
<td><%= priority_text(issue) %></td>
|
|
32
34
|
<td>
|
|
33
35
|
<% parent_hierarchy(issue).each_with_index do |parent, index| %>
|
|
34
36
|
<% color = parent != issue ? "var(--hierarchy-table-inactive-item-text-color)" : 'var(--default-text-color)' %>
|
|
@@ -67,6 +67,9 @@
|
|
|
67
67
|
--sprint-burndown-sprint-color-4: red;
|
|
68
68
|
--sprint-burndown-sprint-color-5: brown;
|
|
69
69
|
|
|
70
|
+
--daily-view-selected-issue-background: lightgray;
|
|
71
|
+
--daily-view-issue-border: green;
|
|
72
|
+
--daily-view-selected-issue-border: red;
|
|
70
73
|
|
|
71
74
|
}
|
|
72
75
|
|
|
@@ -142,6 +145,69 @@ ul.quality_report {
|
|
|
142
145
|
border-top: 1px solid gray;
|
|
143
146
|
}
|
|
144
147
|
|
|
148
|
+
div.daily_issue:hover {
|
|
149
|
+
background: var(--daily-view-selected-issue-background);
|
|
150
|
+
border-color: var(--daily-view-selected-issue-border);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
div.daily_issue {
|
|
154
|
+
border: 1px solid var(--daily-view-issue-border);
|
|
155
|
+
padding: 0.5em;
|
|
156
|
+
.heading {
|
|
157
|
+
vertical-align: middle;
|
|
158
|
+
display: flex;
|
|
159
|
+
flex-wrap: wrap;
|
|
160
|
+
column-gap: 0.5em;
|
|
161
|
+
align-items: center;
|
|
162
|
+
}
|
|
163
|
+
table {
|
|
164
|
+
margin-left: 1em;
|
|
165
|
+
td {
|
|
166
|
+
vertical-align: top;
|
|
167
|
+
}
|
|
168
|
+
.time {
|
|
169
|
+
white-space: nowrap;
|
|
170
|
+
font-size: 0.8em;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
.icon {
|
|
174
|
+
width: 1em;
|
|
175
|
+
height: 1em;
|
|
176
|
+
}
|
|
177
|
+
.account_id {
|
|
178
|
+
font-weight: bold;
|
|
179
|
+
}
|
|
180
|
+
.field {
|
|
181
|
+
border: 1px solid black;
|
|
182
|
+
color: white;
|
|
183
|
+
background: black;
|
|
184
|
+
padding-left: 0.2em;
|
|
185
|
+
padding-right: 0.2em;
|
|
186
|
+
border-radius: 0.2em;
|
|
187
|
+
}
|
|
188
|
+
.label {
|
|
189
|
+
border: 1px solid black;
|
|
190
|
+
padding-left: 0.2em;
|
|
191
|
+
padding-right: 0.2em;
|
|
192
|
+
border-radius: 0.2em;
|
|
193
|
+
}
|
|
194
|
+
h1 {
|
|
195
|
+
border: none;
|
|
196
|
+
background: none;
|
|
197
|
+
padding-left: 0;
|
|
198
|
+
}
|
|
199
|
+
margin-bottom: 0.5em;
|
|
200
|
+
}
|
|
201
|
+
div.child_issue:hover {
|
|
202
|
+
background: var(--body-background);
|
|
203
|
+
}
|
|
204
|
+
div.child_issue {
|
|
205
|
+
border: 1px dashed green;
|
|
206
|
+
margin: 0.2em;
|
|
207
|
+
margin-left: 1.5em;
|
|
208
|
+
padding: 0.5em;
|
|
209
|
+
}
|
|
210
|
+
|
|
145
211
|
@media screen and (prefers-color-scheme: dark) {
|
|
146
212
|
:root {
|
|
147
213
|
--warning-banner: #9F2B00;
|
|
@@ -174,6 +240,8 @@ ul.quality_report {
|
|
|
174
240
|
--wip-chart-duration-two-weeks-or-less-color: #cf9400;
|
|
175
241
|
--wip-chart-duration-four-weeks-or-less-color: #c25e00;
|
|
176
242
|
--wip-chart-duration-more-than-four-weeks-color: #8e0000;
|
|
243
|
+
|
|
244
|
+
--daily-view-selected-issue-background: #474747;
|
|
177
245
|
}
|
|
178
246
|
|
|
179
247
|
h1 {
|
|
@@ -206,4 +274,12 @@ ul.quality_report {
|
|
|
206
274
|
div.color_block {
|
|
207
275
|
border: 1px solid lightgray;
|
|
208
276
|
}
|
|
277
|
+
|
|
278
|
+
div.daily_issue {
|
|
279
|
+
.field {
|
|
280
|
+
color: var(--default-text-color);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
209
285
|
}
|
|
@@ -18,6 +18,23 @@
|
|
|
18
18
|
document.getElementById(issues_id).style.display = 'none'
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
+
|
|
22
|
+
function toggle_visibility(open_link_id, close_link_id, toggleable_id) {
|
|
23
|
+
let open_link = document.getElementById(open_link_id)
|
|
24
|
+
let close_link = document.getElementById(close_link_id)
|
|
25
|
+
let toggleable_element = document.getElementById(toggleable_id)
|
|
26
|
+
|
|
27
|
+
if(open_link.style.display == 'none') {
|
|
28
|
+
open_link.style.display = 'block'
|
|
29
|
+
close_link.style.display = 'none'
|
|
30
|
+
toggleable_element.style.display = 'none'
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
open_link.style.display = 'none'
|
|
34
|
+
close_link.style.display = 'block'
|
|
35
|
+
toggleable_element.style.display = 'block'
|
|
36
|
+
}
|
|
37
|
+
}
|
|
21
38
|
// If we switch between light/dark mode then force a refresh so all charts will redraw correctly
|
|
22
39
|
// in the other colour scheme.
|
|
23
40
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
|
|
@@ -33,8 +50,8 @@
|
|
|
33
50
|
</head>
|
|
34
51
|
<body>
|
|
35
52
|
<noscript>
|
|
36
|
-
<div style="padding: 1em; background:
|
|
37
|
-
Javascript is currently disabled and that means that almost all of the charts in this report won't render. If you'
|
|
53
|
+
<div style="padding: 1em; background: red; color: white; font-size: 2em;">
|
|
54
|
+
Javascript is currently disabled and that means that almost all of the charts in this report won't render. If you've loaded this from a folder on SharePoint then save it locally and load it again.
|
|
38
55
|
</div>
|
|
39
56
|
</noscript>
|
|
40
57
|
<%= "\n" + @sections.collect { |text, type| text if type == :header }.compact.join("\n\n") %>
|
|
@@ -33,6 +33,7 @@ class HtmlReportConfig
|
|
|
33
33
|
define_chart name: 'estimate_accuracy_chart', classname: 'EstimateAccuracyChart'
|
|
34
34
|
define_chart name: 'hierarchy_table', classname: 'HierarchyTable'
|
|
35
35
|
define_chart name: 'flow_efficiency_scatterplot', classname: 'FlowEfficiencyScatterplot'
|
|
36
|
+
define_chart name: 'daily_view', classname: 'DailyView'
|
|
36
37
|
|
|
37
38
|
define_chart name: 'daily_wip_by_type', classname: 'DailyWipChart',
|
|
38
39
|
deprecated_warning: 'This is the same as daily_wip_chart. Please use that one', deprecated_date: '2024-05-23'
|
|
@@ -159,6 +160,7 @@ class HtmlReportConfig
|
|
|
159
160
|
chart.time_range = project_config.time_range
|
|
160
161
|
chart.timezone_offset = timezone_offset
|
|
161
162
|
chart.settings = settings
|
|
163
|
+
chart.users = project_config.users
|
|
162
164
|
|
|
163
165
|
chart.all_boards = project_config.all_boards
|
|
164
166
|
chart.board_id = find_board_id
|
data/lib/jirametrics/issue.rb
CHANGED
|
@@ -44,9 +44,11 @@ class Issue
|
|
|
44
44
|
def key = @raw['key']
|
|
45
45
|
|
|
46
46
|
def type = @raw['fields']['issuetype']['name']
|
|
47
|
-
|
|
48
47
|
def type_icon_url = @raw['fields']['issuetype']['iconUrl']
|
|
49
48
|
|
|
49
|
+
def priority_name = @raw['fields']['priority']['name']
|
|
50
|
+
def priority_url = @raw['fields']['priority']['iconUrl']
|
|
51
|
+
|
|
50
52
|
def summary = @raw['fields']['summary']
|
|
51
53
|
|
|
52
54
|
def labels = @raw['fields']['labels'] || []
|
|
@@ -205,6 +207,10 @@ class Issue
|
|
|
205
207
|
nil
|
|
206
208
|
end
|
|
207
209
|
|
|
210
|
+
def first_time_visible_on_board
|
|
211
|
+
first_time_in_status(*board.visible_columns.collect(&:status_ids).flatten)
|
|
212
|
+
end
|
|
213
|
+
|
|
208
214
|
def parse_time text
|
|
209
215
|
Time.parse(text).getlocal(@timezone_offset)
|
|
210
216
|
end
|
|
@@ -230,6 +236,10 @@ class Issue
|
|
|
230
236
|
@raw['fields']&.[]('assignee')&.[]('displayName')
|
|
231
237
|
end
|
|
232
238
|
|
|
239
|
+
def assigned_to_icon_url
|
|
240
|
+
@raw['fields']&.[]('assignee')&.[]('avatarUrls')&.[]('16x16')
|
|
241
|
+
end
|
|
242
|
+
|
|
233
243
|
# Many test failures are simply unreadable because the default inspect on this class goes
|
|
234
244
|
# on for pages. Shorten it up.
|
|
235
245
|
def inspect
|
|
@@ -315,7 +325,7 @@ class Issue
|
|
|
315
325
|
|
|
316
326
|
# This mock change is to force the writing of one last entry at the end of the time range.
|
|
317
327
|
# By doing this, we're able to eliminate a lot of duplicated code in charts.
|
|
318
|
-
mock_change = ChangeItem.new time: end_time,
|
|
328
|
+
mock_change = ChangeItem.new time: end_time, artificial: true, raw: { 'field' => '' }, author_raw: nil
|
|
319
329
|
|
|
320
330
|
(changes + [mock_change]).each do |change|
|
|
321
331
|
previous_was_active = false if check_for_stalled(
|
|
@@ -335,7 +345,7 @@ class Issue
|
|
|
335
345
|
end
|
|
336
346
|
elsif change.link?
|
|
337
347
|
# Example: "This issue is satisfied by ANON-30465"
|
|
338
|
-
unless /^This issue (?<link_text>.+) (?<issue_key>.+)$/ =~ (change.value || change.old_value)
|
|
348
|
+
unless /^This (?<_>issue|work item) (?<link_text>.+) (?<issue_key>.+)$/ =~ (change.value || change.old_value)
|
|
339
349
|
puts "Issue(#{key}) Can't parse link text: #{change.value || change.old_value}"
|
|
340
350
|
next
|
|
341
351
|
end
|
|
@@ -462,8 +472,6 @@ class Issue
|
|
|
462
472
|
end
|
|
463
473
|
|
|
464
474
|
def expedited?
|
|
465
|
-
return false unless @board&.project_config
|
|
466
|
-
|
|
467
475
|
names = @board.project_config.settings['expedited_priority_names']
|
|
468
476
|
return false unless names
|
|
469
477
|
|
|
@@ -580,7 +588,7 @@ class Issue
|
|
|
580
588
|
/(?<project_code1>[^-]+)-(?<id1>.+)/ =~ key
|
|
581
589
|
/(?<project_code2>[^-]+)-(?<id2>.+)/ =~ other.key
|
|
582
590
|
comparison = project_code1 <=> project_code2
|
|
583
|
-
comparison = id1 <=> id2 if comparison.zero?
|
|
591
|
+
comparison = id1.to_i <=> id2.to_i if comparison.zero?
|
|
584
592
|
comparison
|
|
585
593
|
end
|
|
586
594
|
|
|
@@ -611,9 +619,13 @@ class Issue
|
|
|
611
619
|
end
|
|
612
620
|
history = [] # time, type, detail
|
|
613
621
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
622
|
+
if board.cycletime
|
|
623
|
+
started_at, stopped_at = board.cycletime.started_stopped_times(self)
|
|
624
|
+
history << [started_at, nil, '↓↓↓↓ Started here ↓↓↓↓', true] if started_at
|
|
625
|
+
history << [stopped_at, nil, '↑↑↑↑ Finished here ↑↑↑↑', true] if stopped_at
|
|
626
|
+
else
|
|
627
|
+
result << " Unable to determine start/end times as board #{board.id} has no cycletime specified\n"
|
|
628
|
+
end
|
|
617
629
|
|
|
618
630
|
@discarded_change_times&.each do |time|
|
|
619
631
|
history << [time, nil, '↑↑↑↑ Changes discarded ↑↑↑↑', true]
|
|
@@ -669,9 +681,8 @@ class Issue
|
|
|
669
681
|
def done?
|
|
670
682
|
if artificial? || board.cycletime.nil?
|
|
671
683
|
# This was probably loaded as a linked issue, which means we don't know what board it really
|
|
672
|
-
# belonged to. The best we can do is look at the status
|
|
673
|
-
|
|
674
|
-
status.category.name == 'Done'
|
|
684
|
+
# belonged to. The best we can do is look at the status key
|
|
685
|
+
status.category.done?
|
|
675
686
|
else
|
|
676
687
|
board.cycletime.done? self
|
|
677
688
|
end
|
|
@@ -681,34 +692,40 @@ class Issue
|
|
|
681
692
|
@changes.select { |change| change.status? }
|
|
682
693
|
end
|
|
683
694
|
|
|
684
|
-
|
|
695
|
+
def sprints
|
|
696
|
+
sprint_ids = []
|
|
685
697
|
|
|
686
|
-
|
|
687
|
-
|
|
698
|
+
changes.each do |change|
|
|
699
|
+
next unless change.sprint?
|
|
700
|
+
|
|
701
|
+
sprint_ids << change.raw['to'].split(/\s*,\s*/).collect { |id| id.to_i }
|
|
702
|
+
end
|
|
703
|
+
sprint_ids.flatten!
|
|
704
|
+
|
|
705
|
+
board.sprints.select { |s| sprint_ids.include? s.id }
|
|
688
706
|
end
|
|
689
707
|
|
|
708
|
+
private
|
|
709
|
+
|
|
690
710
|
def load_history_into_changes
|
|
691
711
|
@raw['changelog']['histories']&.each do |history|
|
|
692
712
|
created = parse_time(history['created'])
|
|
693
713
|
|
|
694
|
-
# It should be impossible to not have an author but we've seen it in production
|
|
695
|
-
author = assemble_author history
|
|
696
714
|
history['items']&.each do |item|
|
|
697
|
-
@changes << ChangeItem.new(raw: item, time: created,
|
|
715
|
+
@changes << ChangeItem.new(raw: item, time: created, author_raw: history['author'])
|
|
698
716
|
end
|
|
699
717
|
end
|
|
700
718
|
end
|
|
701
719
|
|
|
702
720
|
def load_comments_into_changes
|
|
703
721
|
@raw['fields']['comment']['comments']&.each do |comment|
|
|
704
|
-
raw = {
|
|
722
|
+
raw = comment.merge({
|
|
705
723
|
'field' => 'comment',
|
|
706
724
|
'to' => comment['id'],
|
|
707
725
|
'toString' => comment['body']
|
|
708
|
-
}
|
|
709
|
-
author = assemble_author comment
|
|
726
|
+
})
|
|
710
727
|
created = parse_time(comment['created'])
|
|
711
|
-
@changes << ChangeItem.new(raw: raw, time: created,
|
|
728
|
+
@changes << ChangeItem.new(raw: raw, time: created, artificial: true, author_raw: comment['author'])
|
|
712
729
|
end
|
|
713
730
|
end
|
|
714
731
|
|
|
@@ -750,7 +767,9 @@ class Issue
|
|
|
750
767
|
first_status = first_change.old_value
|
|
751
768
|
first_status_id = first_change.old_value_id
|
|
752
769
|
end
|
|
753
|
-
|
|
770
|
+
|
|
771
|
+
creator = raw['fields']['creator']
|
|
772
|
+
ChangeItem.new time: created_time, artificial: true, author_raw: creator, raw: {
|
|
754
773
|
'field' => field_name,
|
|
755
774
|
'to' => first_status_id,
|
|
756
775
|
'toString' => first_status
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class IssueCollection < Array
|
|
4
|
+
attr_reader :hidden
|
|
5
|
+
|
|
6
|
+
def self.[] *issues
|
|
7
|
+
collection = new
|
|
8
|
+
issues.each { |i| collection << i }
|
|
9
|
+
collection
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
super
|
|
14
|
+
@hidden = []
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def reject! &block
|
|
18
|
+
select(&block).each do |issue|
|
|
19
|
+
@hidden << issue
|
|
20
|
+
end
|
|
21
|
+
super
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def find_by_key key:, include_hidden: false
|
|
25
|
+
block = ->(issue) { issue.key == key }
|
|
26
|
+
issue = find(&block)
|
|
27
|
+
issue = hidden.find(&block) if issue.nil? && include_hidden
|
|
28
|
+
issue
|
|
29
|
+
end
|
|
30
|
+
def clone
|
|
31
|
+
raise 'baboom'
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -26,7 +26,10 @@ class JiraGateway
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def call_command command
|
|
29
|
-
|
|
29
|
+
log_entry = " #{command.gsub(/\s+/, ' ')}"
|
|
30
|
+
log_entry = log_entry.gsub(@jira_api_token, '[API_TOKEN]') if @jira_api_token
|
|
31
|
+
@file_system.log log_entry
|
|
32
|
+
|
|
30
33
|
result = `#{command}`
|
|
31
34
|
@file_system.log result unless $CHILD_STATUS.success?
|
|
32
35
|
return result if $CHILD_STATUS.success?
|
|
@@ -74,4 +77,8 @@ class JiraGateway
|
|
|
74
77
|
|
|
75
78
|
true
|
|
76
79
|
end
|
|
80
|
+
|
|
81
|
+
def cloud?
|
|
82
|
+
@jira_url.downcase.end_with? '.atlassian.net'
|
|
83
|
+
end
|
|
77
84
|
end
|