jirametrics 2.12.1 → 2.14pre2
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/atlassian_document_format.rb +156 -0
- data/lib/jirametrics/board_config.rb +1 -0
- data/lib/jirametrics/change_item.rb +1 -0
- data/lib/jirametrics/chart_base.rb +2 -0
- data/lib/jirametrics/daily_view.rb +58 -38
- data/lib/jirametrics/downloader.rb +52 -23
- data/lib/jirametrics/html/index.css +5 -0
- data/lib/jirametrics/html/index.erb +2 -2
- data/lib/jirametrics/issue.rb +3 -4
- data/lib/jirametrics/jira_gateway.rb +4 -0
- data/lib/jirametrics/project_config.rb +20 -3
- data/lib/jirametrics/status_collection.rb +1 -0
- data/lib/jirametrics.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ddc2265d15bc52422942f009082fd92fea0b296860297b6a6fa5fd0aee1788f4
|
4
|
+
data.tar.gz: e76799a3576b4e1e667268b823c99d6be8068ee196a76766e07fb646a44b17ee
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0d60ce44257fcd466d092963ec7a58e86d9cf9252bc83262ac3c3b3621941df44b5c686b71e57be68bef06352d964f369b3cd9281324e3235cdad2c94e2f1869
|
7
|
+
data.tar.gz: 217c6e8a37ad3b5ae4e25d454a27b77b023ccd87e73ece9a1d3853f55e9c1330e24e64a7578f99289007925c2310482f200ac4f5d5affae6dd0ccb649978269b
|
@@ -0,0 +1,156 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class AtlassianDocumentFormat
|
4
|
+
attr_reader :users
|
5
|
+
|
6
|
+
def initialize users:, timezone_offset:
|
7
|
+
@users = users
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_html input
|
11
|
+
if input.is_a? String
|
12
|
+
input
|
13
|
+
.gsub(/{color:(#\w{6})}([^{]+){color}/, '<span style="color: \1">\2</span>') # Colours
|
14
|
+
.gsub(/\[~accountid:([^\]]+)\]/) { expand_account_id $1 } # Tagged people
|
15
|
+
.gsub(/\[([^\|]+)\|(https?[^\]]+)\]/, '<a href="\2">\1</a>') # URLs
|
16
|
+
.gsub("\n", '<br />')
|
17
|
+
else
|
18
|
+
input['content'].collect { |element| adf_node_to_html element }.join("\n")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# ADF is Atlassian Document Format
|
23
|
+
# https://developer.atlassian.com/cloud/jira/platform/apis/document/structure/
|
24
|
+
def adf_node_to_html node
|
25
|
+
closing_tag = nil
|
26
|
+
node_attrs = node['attrs']
|
27
|
+
|
28
|
+
result = +''
|
29
|
+
case node['type']
|
30
|
+
when 'blockquote'
|
31
|
+
result << '<blockquote>'
|
32
|
+
closing_tag = '</blockquote>'
|
33
|
+
when 'bulletList'
|
34
|
+
result << '<ul>'
|
35
|
+
closing_tag = '</ul>'
|
36
|
+
when 'codeBlock'
|
37
|
+
result << '<code>'
|
38
|
+
closing_tag = '</code>'
|
39
|
+
when 'date'
|
40
|
+
result << Time.at(node_attrs['timestamp'].to_i / 1000, in: @timezone_offset).to_date.to_s
|
41
|
+
when 'decisionItem'
|
42
|
+
result << '<li>'
|
43
|
+
closing_tag = '</li>'
|
44
|
+
when 'decisionList'
|
45
|
+
result << '<div>Decisions<ul>'
|
46
|
+
closing_tag = '</ul></div>'
|
47
|
+
when 'emoji'
|
48
|
+
result << node_attrs['text']
|
49
|
+
when 'expand'
|
50
|
+
# TODO: Maybe, someday, make this actually expandable. For now it's always open
|
51
|
+
result << "<div>#{node_attrs['title']}</div>"
|
52
|
+
when 'hardBreak'
|
53
|
+
result << '<br />'
|
54
|
+
when 'heading'
|
55
|
+
level = node_attrs['level']
|
56
|
+
result << "<h#{level}>"
|
57
|
+
closing_tag = "</h#{level}>"
|
58
|
+
when 'inlineCard'
|
59
|
+
url = node_attrs['url']
|
60
|
+
result << "[Inline card]: <a href='#{url}'>#{url}</a>"
|
61
|
+
when 'listItem'
|
62
|
+
result << '<li>'
|
63
|
+
closing_tag = '</li>'
|
64
|
+
when 'media'
|
65
|
+
text = node_attrs['alt'] || node_attrs['id']
|
66
|
+
result << "Media: #{text}"
|
67
|
+
when 'mediaSingle', 'mediaGroup'
|
68
|
+
result << '<div>'
|
69
|
+
closing_tag = '</div>'
|
70
|
+
when 'mention'
|
71
|
+
user = node_attrs['text']
|
72
|
+
result << "<b>#{user}</b>"
|
73
|
+
when 'orderedList'
|
74
|
+
result << '<ol>'
|
75
|
+
closing_tag = '</ol>'
|
76
|
+
when 'panel'
|
77
|
+
type = node_attrs['panelType']
|
78
|
+
result << "<div>#{type.upcase}</div>"
|
79
|
+
when 'paragraph'
|
80
|
+
result << '<p>'
|
81
|
+
closing_tag = '</p>'
|
82
|
+
when 'rule'
|
83
|
+
result << '<hr />'
|
84
|
+
when 'status'
|
85
|
+
text = node_attrs['text']
|
86
|
+
result << text
|
87
|
+
when 'table'
|
88
|
+
result << '<table>'
|
89
|
+
closing_tag = '</table>'
|
90
|
+
when 'tableCell'
|
91
|
+
result << '<td>'
|
92
|
+
closing_tag = '</td>'
|
93
|
+
when 'tableHeader'
|
94
|
+
result << '<th>'
|
95
|
+
closing_tag = '</th>'
|
96
|
+
when 'tableRow'
|
97
|
+
result << '<tr>'
|
98
|
+
closing_tag = '</tr>'
|
99
|
+
when 'text'
|
100
|
+
marks = adf_marks_to_html node['marks']
|
101
|
+
result << marks.collect(&:first).join
|
102
|
+
result << node['text']
|
103
|
+
result << marks.collect(&:last).join
|
104
|
+
when 'taskItem'
|
105
|
+
state = node_attrs['state'] == 'TODO' ? '☐' : '☑'
|
106
|
+
result << "<li>#{state} "
|
107
|
+
closing_tag = '</li>'
|
108
|
+
when 'taskList'
|
109
|
+
result << "<ul class='taskList'>"
|
110
|
+
closing_tag = '</ul>'
|
111
|
+
else
|
112
|
+
result << "<p>Unparseable section: #{node['type']}</p>"
|
113
|
+
end
|
114
|
+
|
115
|
+
node['content']&.each do |child|
|
116
|
+
result << adf_node_to_html(child)
|
117
|
+
end
|
118
|
+
|
119
|
+
result << closing_tag if closing_tag
|
120
|
+
result
|
121
|
+
end
|
122
|
+
|
123
|
+
def adf_marks_to_html list
|
124
|
+
return [] if list.nil?
|
125
|
+
|
126
|
+
mappings = [
|
127
|
+
['strong', '<b>', '</b>'],
|
128
|
+
['code', '<code>', '</code>'],
|
129
|
+
['em', '<em>', '</em>'],
|
130
|
+
['strike', '<s>', '</s>'],
|
131
|
+
['underline', '<u>', '</u>']
|
132
|
+
]
|
133
|
+
|
134
|
+
list.filter_map do |mark|
|
135
|
+
type = mark['type']
|
136
|
+
if type == 'textColor'
|
137
|
+
color = mark['attrs']['color']
|
138
|
+
["<span style='color: #{color}'>", '</span>']
|
139
|
+
elsif type == 'link'
|
140
|
+
href = mark['attrs']['href']
|
141
|
+
title = mark['attrs']['title']
|
142
|
+
["<a href='#{href}' title='#{title}'>", '</a>']
|
143
|
+
else
|
144
|
+
line = mappings.find { |key, _open, _close| key == type }
|
145
|
+
[line[1], line[2]] if line
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def expand_account_id account_id
|
151
|
+
user = @users.find { |u| u.account_id == account_id }
|
152
|
+
text = account_id
|
153
|
+
text = "@#{user.display_name}" if user
|
154
|
+
"<span class='account_id'>#{text}</span>"
|
155
|
+
end
|
156
|
+
end
|
@@ -11,6 +11,7 @@ class BoardConfig
|
|
11
11
|
|
12
12
|
def run
|
13
13
|
@board = @project_config.all_boards[id]
|
14
|
+
raise "Can't find board #{id.inspect} in #{@project_config.all_boards.keys.inspect}" unless @board
|
14
15
|
|
15
16
|
instance_eval(&@block)
|
16
17
|
raise "Must specify a cycletime for board #{@id}" if @board.cycletime.nil?
|
@@ -30,6 +30,7 @@ class ChangeItem
|
|
30
30
|
def artificial? = @artificial
|
31
31
|
def assignee? = (field == 'assignee')
|
32
32
|
def comment? = (field == 'comment')
|
33
|
+
def description? = (field == 'description')
|
33
34
|
def due_date? = (field == 'duedate')
|
34
35
|
def flagged? = (field == 'Flagged')
|
35
36
|
def issue_type? = field == 'issuetype'
|
@@ -33,6 +33,10 @@ class DailyView < ChartBase
|
|
33
33
|
result
|
34
34
|
end
|
35
35
|
|
36
|
+
def atlassian_document_format
|
37
|
+
@atlassian_document_format ||= AtlassianDocumentFormat.new(users: users, timezone_offset: timezone_offset)
|
38
|
+
end
|
39
|
+
|
36
40
|
def select_aging_issues
|
37
41
|
aging_issues = issues.select do |issue|
|
38
42
|
started_at, stopped_at = issue.board.cycletime.started_stopped_times(issue)
|
@@ -78,7 +82,7 @@ class DailyView < ChartBase
|
|
78
82
|
blocked_stalled = issue.blocked_stalled_by_date(
|
79
83
|
date_range: today..today, chart_end_time: time_range.end, settings: settings
|
80
84
|
)[today]
|
81
|
-
return []
|
85
|
+
return [] if blocked_stalled.active?
|
82
86
|
|
83
87
|
lines = []
|
84
88
|
if blocked_stalled.blocked?
|
@@ -98,15 +102,18 @@ class DailyView < ChartBase
|
|
98
102
|
lines
|
99
103
|
end
|
100
104
|
|
101
|
-
def make_issue_label issue
|
102
|
-
"<img src='#{issue.type_icon_url}' title='#{issue.type}' class='icon' /> "
|
103
|
-
|
105
|
+
def make_issue_label issue:, done:
|
106
|
+
label = "<img src='#{issue.type_icon_url}' title='#{issue.type}' class='icon' /> "
|
107
|
+
label << '<s>' if done
|
108
|
+
label << "<b><a href='#{issue.url}'>#{issue.key}</a></b> <i>#{issue.summary}</i>"
|
109
|
+
label << '</s>' if done
|
110
|
+
label
|
104
111
|
end
|
105
112
|
|
106
|
-
def make_title_line issue
|
113
|
+
def make_title_line issue:, done:
|
107
114
|
title_line = +''
|
108
115
|
title_line << color_block('--expedited-color', title: 'Expedited') if issue.expedited?
|
109
|
-
title_line << make_issue_label(issue)
|
116
|
+
title_line << make_issue_label(issue: issue, done: done)
|
110
117
|
title_line
|
111
118
|
end
|
112
119
|
|
@@ -115,20 +122,25 @@ class DailyView < ChartBase
|
|
115
122
|
parent_key = issue.parent_key
|
116
123
|
if parent_key
|
117
124
|
parent = issues.find_by_key key: parent_key, include_hidden: true
|
118
|
-
text = parent ? make_issue_label(parent) : parent_key
|
125
|
+
text = parent ? make_issue_label(issue: parent, done: parent.done?) : parent_key
|
119
126
|
lines << ["Parent: #{text}"]
|
120
127
|
end
|
121
128
|
lines
|
122
129
|
end
|
123
130
|
|
124
|
-
def make_stats_lines issue
|
131
|
+
def make_stats_lines issue:, done:
|
125
132
|
line = []
|
126
133
|
|
127
134
|
line << "<img src='#{issue.priority_url}' class='icon' /> <b>#{issue.priority_name}</b>"
|
128
135
|
|
129
|
-
|
130
|
-
|
136
|
+
if done
|
137
|
+
cycletime = issue.board.cycletime.cycletime(issue)
|
131
138
|
|
139
|
+
line << "Cycletime: <b>#{label_days cycletime}</b>"
|
140
|
+
else
|
141
|
+
age = issue.board.cycletime.age(issue, today: date_range.end)
|
142
|
+
line << "Age: <b>#{age ? label_days(age) : '(Not Started)'}</b>"
|
143
|
+
end
|
132
144
|
line << "Status: <b>#{format_status issue.status, board: issue.board}</b>"
|
133
145
|
|
134
146
|
column = issue.board.visible_columns.find { |c| c.status_ids.include?(issue.status.id) }
|
@@ -154,29 +166,23 @@ class DailyView < ChartBase
|
|
154
166
|
|
155
167
|
def make_child_lines issue
|
156
168
|
lines = []
|
157
|
-
subtasks = issue.subtasks
|
169
|
+
subtasks = issue.subtasks
|
158
170
|
|
159
|
-
|
160
|
-
icon_urls = subtasks.collect(&:type_icon_url).uniq.collect { |url| "<img src='#{url}' class='icon' />" }
|
161
|
-
lines << (icon_urls << 'Incomplete child issues')
|
162
|
-
lines += subtasks
|
163
|
-
end
|
164
|
-
lines
|
165
|
-
end
|
171
|
+
return lines if subtasks.empty?
|
166
172
|
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
173
|
+
id = next_id
|
174
|
+
lines <<
|
175
|
+
"<a href=\"javascript:toggle_visibility('open#{id}', 'close#{id}', 'section#{id}');\">" \
|
176
|
+
"<span id='open#{id}' style='display: none'>▶ Child issues</span>" \
|
177
|
+
"<span id='close#{id}'>▼ Child issues</span></a>"
|
178
|
+
lines << "<section id='section#{id}'>"
|
179
|
+
|
180
|
+
# icon_urls = subtasks.collect(&:type_icon_url).uniq.collect { |url| "<img src='#{url}' class='icon' />" }
|
181
|
+
# lines << (icon_urls << 'Child issues')
|
182
|
+
lines += subtasks
|
183
|
+
lines << '</section>'
|
174
184
|
|
175
|
-
|
176
|
-
user = @users.find { |u| u.account_id == account_id }
|
177
|
-
text = account_id
|
178
|
-
text = "@#{user.display_name}" if user
|
179
|
-
"<span class='account_id'>#{text}</span>"
|
185
|
+
lines
|
180
186
|
end
|
181
187
|
|
182
188
|
def make_history_lines issue
|
@@ -207,8 +213,8 @@ class DailyView < ChartBase
|
|
207
213
|
end
|
208
214
|
|
209
215
|
def history_text change:, board:
|
210
|
-
if change.comment?
|
211
|
-
|
216
|
+
if change.comment? || change.description?
|
217
|
+
atlassian_document_format.to_html(change.value)
|
212
218
|
elsif change.status?
|
213
219
|
convertor = ->(id) { format_status(board.possible_statuses.find_by_id(id), board: board) }
|
214
220
|
to = convertor.call(change.value_id)
|
@@ -245,15 +251,27 @@ class DailyView < ChartBase
|
|
245
251
|
.join(' ')]]
|
246
252
|
end
|
247
253
|
|
254
|
+
def make_description_lines issue
|
255
|
+
description = issue.raw['fields']['description']
|
256
|
+
result = []
|
257
|
+
result << [atlassian_document_format.to_html(description)] if description
|
258
|
+
result
|
259
|
+
end
|
260
|
+
|
248
261
|
def assemble_issue_lines issue, child:
|
262
|
+
done = issue.done?
|
263
|
+
|
249
264
|
lines = []
|
250
|
-
lines << [make_title_line(issue)]
|
265
|
+
lines << [make_title_line(issue: issue, done: done)]
|
251
266
|
lines += make_parent_lines(issue) unless child
|
252
|
-
lines += make_stats_lines(issue)
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
267
|
+
lines += make_stats_lines(issue: issue, done: done)
|
268
|
+
unless done
|
269
|
+
lines += make_description_lines(issue)
|
270
|
+
lines += make_sprints_lines(issue)
|
271
|
+
lines += make_blocked_stalled_lines(issue)
|
272
|
+
lines += make_child_lines(issue)
|
273
|
+
lines += make_history_lines(issue)
|
274
|
+
end
|
257
275
|
lines
|
258
276
|
end
|
259
277
|
|
@@ -264,6 +282,8 @@ class DailyView < ChartBase
|
|
264
282
|
assemble_issue_lines(issue, child: child).each do |row|
|
265
283
|
if row.is_a? Issue
|
266
284
|
result << render_issue(row, child: true)
|
285
|
+
elsif row.is_a?(String)
|
286
|
+
result << row
|
267
287
|
else
|
268
288
|
result << '<div class="heading">'
|
269
289
|
row.each do |chunk|
|
@@ -97,30 +97,59 @@ class Downloader
|
|
97
97
|
log " JQL: #{jql}"
|
98
98
|
escaped_jql = CGI.escape jql
|
99
99
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
100
|
+
if @jira_gateway.cloud?
|
101
|
+
max_results = 5_000 # The maximum allowed by Jira
|
102
|
+
next_page_token = nil
|
103
|
+
issue_count = 0
|
104
|
+
|
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']
|
110
|
+
|
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"
|
117
|
+
|
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
|
115
152
|
end
|
116
|
-
|
117
|
-
total = json['total'].to_i
|
118
|
-
max_results = json['maxResults']
|
119
|
-
|
120
|
-
message = " Downloaded #{start_at + 1}-#{[start_at + max_results, total].min} of #{total} issues to #{path} "
|
121
|
-
log message, both: true
|
122
|
-
|
123
|
-
start_at += json['issues'].size
|
124
153
|
end
|
125
154
|
end
|
126
155
|
|
@@ -50,8 +50,8 @@
|
|
50
50
|
</head>
|
51
51
|
<body>
|
52
52
|
<noscript>
|
53
|
-
<div style="padding: 1em; background:
|
54
|
-
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.
|
55
55
|
</div>
|
56
56
|
</noscript>
|
57
57
|
<%= "\n" + @sections.collect { |text, type| text if type == :header }.compact.join("\n\n") %>
|
data/lib/jirametrics/issue.rb
CHANGED
@@ -345,7 +345,7 @@ class Issue
|
|
345
345
|
end
|
346
346
|
elsif change.link?
|
347
347
|
# Example: "This issue is satisfied by ANON-30465"
|
348
|
-
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)
|
349
349
|
puts "Issue(#{key}) Can't parse link text: #{change.value || change.old_value}"
|
350
350
|
next
|
351
351
|
end
|
@@ -681,9 +681,8 @@ class Issue
|
|
681
681
|
def done?
|
682
682
|
if artificial? || board.cycletime.nil?
|
683
683
|
# This was probably loaded as a linked issue, which means we don't know what board it really
|
684
|
-
# belonged to. The best we can do is look at the status
|
685
|
-
|
686
|
-
status.category.name == 'Done'
|
684
|
+
# belonged to. The best we can do is look at the status key
|
685
|
+
status.category.done?
|
687
686
|
else
|
688
687
|
board.cycletime.done? self
|
689
688
|
end
|
@@ -114,10 +114,14 @@ class ProjectConfig
|
|
114
114
|
def file_prefix prefix
|
115
115
|
# The file_prefix has to be set before almost everything else. It really should have been an attribute
|
116
116
|
# on the project declaration itself. Hindsight is 20/20.
|
117
|
+
|
118
|
+
# There can only be one of these
|
117
119
|
if @file_prefix
|
118
|
-
raise "file_prefix
|
120
|
+
raise "file_prefix can only be set once. Was #{@file_prefix.inspect} and now changed to #{prefix.inspect}."
|
119
121
|
end
|
120
122
|
|
123
|
+
raise_if_prefix_already_used(prefix)
|
124
|
+
|
121
125
|
@file_prefix = prefix
|
122
126
|
|
123
127
|
# Yes, this is a wierd place to be initializing this. Unfortunately, it has to happen after the file_prefix
|
@@ -130,8 +134,21 @@ class ProjectConfig
|
|
130
134
|
@file_prefix
|
131
135
|
end
|
132
136
|
|
133
|
-
def
|
134
|
-
|
137
|
+
def raise_if_prefix_already_used prefix
|
138
|
+
@exporter.project_configs.each do |project|
|
139
|
+
next unless project.get_file_prefix(raise_if_not_set: false) == prefix && project.target_path == target_path
|
140
|
+
|
141
|
+
raise "Project #{name.inspect} specifies file prefix #{prefix.inspect}, " \
|
142
|
+
"but that is already used by project #{project.name.inspect} in the same target path #{target_path.inspect}. " \
|
143
|
+
'This is almost guaranteed to be too much copy and paste in your configuration. ' \
|
144
|
+
'File prefixes must be unique within a directory.'
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def get_file_prefix raise_if_not_set: true
|
149
|
+
if @file_prefix.nil? && raise_if_not_set
|
150
|
+
raise 'file_prefix has not been set yet. Move it to the top of the project declaration.'
|
151
|
+
end
|
135
152
|
|
136
153
|
@file_prefix
|
137
154
|
end
|
data/lib/jirametrics.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jirametrics
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.14pre2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mike Bowler
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-
|
10
|
+
date: 2025-08-11 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: random-word
|
@@ -65,6 +65,7 @@ files:
|
|
65
65
|
- lib/jirametrics/aging_work_in_progress_chart.rb
|
66
66
|
- lib/jirametrics/aging_work_table.rb
|
67
67
|
- lib/jirametrics/anonymizer.rb
|
68
|
+
- lib/jirametrics/atlassian_document_format.rb
|
68
69
|
- lib/jirametrics/blocked_stalled_change.rb
|
69
70
|
- lib/jirametrics/board.rb
|
70
71
|
- lib/jirametrics/board_column.rb
|