jirametrics 1.5 → 2.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.
- checksums.yaml +4 -4
- data/bin/jirametrics +1 -0
- data/lib/jirametrics/aggregate_config.rb +2 -1
- data/lib/jirametrics/aging_work_bar_chart.rb +3 -3
- data/lib/jirametrics/aging_work_in_progress_chart.rb +6 -6
- data/lib/jirametrics/anonymizer.rb +3 -3
- data/lib/jirametrics/blocked_stalled_change.rb +5 -10
- data/lib/jirametrics/board.rb +11 -13
- data/lib/jirametrics/chart_base.rb +5 -5
- data/lib/jirametrics/cycletime_histogram.rb +2 -2
- data/lib/jirametrics/cycletime_scatterplot.rb +5 -2
- data/lib/jirametrics/daily_wip_chart.rb +1 -1
- data/lib/jirametrics/data_quality_report.rb +4 -4
- data/lib/jirametrics/dependency_chart.rb +5 -4
- data/lib/jirametrics/download_config.rb +0 -19
- data/lib/jirametrics/downloader.rb +32 -74
- data/lib/jirametrics/examples/aggregated_project.rb +61 -3
- data/lib/jirametrics/examples/standard_project.rb +3 -3
- data/lib/jirametrics/expedited_chart.rb +4 -4
- data/lib/jirametrics/experimental/generator.rb +5 -4
- data/lib/jirametrics/experimental/info.rb +2 -2
- data/lib/jirametrics/exporter.rb +16 -30
- data/lib/jirametrics/file_system.rb +36 -0
- data/lib/jirametrics/groupable_issue_chart.rb +0 -9
- data/lib/jirametrics/hierarchy_table.rb +1 -1
- data/lib/jirametrics/html_report_config.rb +5 -30
- data/lib/jirametrics/issue.rb +1 -7
- data/lib/jirametrics/issue_link.rb +0 -7
- data/lib/jirametrics/jira_gateway.rb +59 -0
- data/lib/jirametrics/project_config.rb +78 -88
- data/lib/jirametrics/rules.rb +1 -20
- data/lib/jirametrics/sprint_burndown.rb +7 -6
- data/lib/jirametrics/sprint_issue_change_data.rb +4 -9
- data/lib/jirametrics/status.rb +24 -20
- data/lib/jirametrics/status_collection.rb +2 -2
- data/lib/jirametrics/story_point_accuracy_chart.rb +2 -7
- data/lib/jirametrics/throughput_chart.rb +2 -2
- data/lib/jirametrics/trend_line_calculator.rb +4 -4
- data/lib/jirametrics/value_equality.rb +23 -0
- data/lib/jirametrics.rb +3 -1
- metadata +5 -17
- data/lib/jirametrics/json_file_loader.rb +0 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: eb163ed079209739994cb9a963533c6afaddc6ca987bf9c428aeee94945ecf40
|
4
|
+
data.tar.gz: 571d3d09cde660cd7038d7d996e0c5e44cb7dfe21fdf892a931ef51db84fb9cf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3212ef8b2d1a2d8eb2be8e614534aa4b614e8f8003f94d0b8cd0239839fbca90c30f8ce062effede748567f53c01c4235ec179d53fd04f01f9f0bd7a288ddb9b
|
7
|
+
data.tar.gz: f25bc331a72956f24dac94205630db7cff9d98a933ccd76f2ddff1132702dfd284c9ad526518c6e91169fb3cc41e604695d1c792f2b2496b870e93a6ff165684
|
data/bin/jirametrics
CHANGED
@@ -48,7 +48,7 @@ class AggregateConfig
|
|
48
48
|
@project_config.jira_url = project.jira_url if @project_config.jira_url.nil?
|
49
49
|
unless @project_config.jira_url == project.jira_url
|
50
50
|
raise 'Not allowed to aggregate projects from different Jira instances: ' \
|
51
|
-
"#{@project_config.jira_url.inspect} and #{project.jira_url.inspect}"
|
51
|
+
"#{@project_config.jira_url.inspect} and #{project.jira_url.inspect}. For project #{project_name}"
|
52
52
|
end
|
53
53
|
|
54
54
|
@included_projects << project
|
@@ -93,6 +93,7 @@ class AggregateConfig
|
|
93
93
|
end
|
94
94
|
|
95
95
|
raise "Can't calculate range" if earliest.nil? || latest.nil?
|
96
|
+
|
96
97
|
earliest..latest
|
97
98
|
end
|
98
99
|
end
|
@@ -185,14 +185,14 @@ class AgingWorkBarChart < ChartBase
|
|
185
185
|
end
|
186
186
|
|
187
187
|
def data_set_by_block(
|
188
|
-
issue:, issue_label:, title_label:, stack:, color:, start_date:, end_date: date_range.end
|
188
|
+
issue:, issue_label:, title_label:, stack:, color:, start_date:, end_date: date_range.end
|
189
189
|
)
|
190
190
|
started = nil
|
191
191
|
ended = nil
|
192
192
|
data = []
|
193
193
|
|
194
194
|
(start_date..end_date).each do |day|
|
195
|
-
if
|
195
|
+
if yield(day)
|
196
196
|
started = day if started.nil?
|
197
197
|
ended = day
|
198
198
|
elsif ended
|
@@ -225,7 +225,7 @@ class AgingWorkBarChart < ChartBase
|
|
225
225
|
end
|
226
226
|
|
227
227
|
def calculate_percent_line percentage: 85
|
228
|
-
days = completed_issues_in_range.
|
228
|
+
days = completed_issues_in_range.filter_map { |issue| issue.board.cycletime.cycletime(issue) }.sort
|
229
229
|
return nil if days.empty?
|
230
230
|
|
231
231
|
days[days.length * percentage / 100]
|
@@ -67,7 +67,7 @@ class AgingWorkInProgressChart < ChartBase
|
|
67
67
|
{
|
68
68
|
'type' => 'line',
|
69
69
|
'label' => rules.label,
|
70
|
-
'data' => rules_to_issues[rules].
|
70
|
+
'data' => rules_to_issues[rules].filter_map do |issue|
|
71
71
|
age = issue.board.cycletime.age(issue, today: date_range.end)
|
72
72
|
column = column_for issue: issue
|
73
73
|
next if column.nil?
|
@@ -76,7 +76,7 @@ class AgingWorkInProgressChart < ChartBase
|
|
76
76
|
'x' => column.name,
|
77
77
|
'title' => ["#{issue.key} : #{issue.summary} (#{label_days age})"]
|
78
78
|
}
|
79
|
-
end
|
79
|
+
end,
|
80
80
|
'fill' => false,
|
81
81
|
'showLine' => false,
|
82
82
|
'backgroundColor' => rules.color
|
@@ -101,16 +101,16 @@ class AgingWorkInProgressChart < ChartBase
|
|
101
101
|
|
102
102
|
def accumulated_status_ids_per_column
|
103
103
|
accumulated_status_ids = []
|
104
|
-
@board_columns.reverse.
|
104
|
+
@board_columns.reverse.filter_map do |column|
|
105
105
|
next if column == @fake_column
|
106
106
|
|
107
107
|
accumulated_status_ids += column.status_ids
|
108
108
|
[column.name, accumulated_status_ids.dup]
|
109
|
-
end.
|
109
|
+
end.reverse
|
110
110
|
end
|
111
111
|
|
112
112
|
def ages_of_issues_that_crossed_column_boundary issues:, status_ids:
|
113
|
-
issues.
|
113
|
+
issues.filter_map do |issue|
|
114
114
|
stop = issue.first_time_in_status(*status_ids)
|
115
115
|
start = issue.board.cycletime.started_time(issue)
|
116
116
|
|
@@ -119,7 +119,7 @@ class AgingWorkInProgressChart < ChartBase
|
|
119
119
|
next if stop < start
|
120
120
|
|
121
121
|
(stop.to_date - start.to_date).to_i + 1
|
122
|
-
end
|
122
|
+
end
|
123
123
|
end
|
124
124
|
|
125
125
|
def column_for issue:
|
@@ -28,7 +28,7 @@ class Anonymizer
|
|
28
28
|
# just try again. In every case we've seen, it's worked on the second attempt, but we'll be
|
29
29
|
# cautious and try five times.
|
30
30
|
5.times do |i|
|
31
|
-
return RandomWord.phrases.next.
|
31
|
+
return RandomWord.phrases.next.tr('_', ' ')
|
32
32
|
rescue # rubocop:disable Style/RescueStandardError We don't care what exception was thrown.
|
33
33
|
puts "Random word blew up on attempt #{i + 1}"
|
34
34
|
end
|
@@ -45,7 +45,7 @@ class Anonymizer
|
|
45
45
|
|
46
46
|
issue.issue_links.each do |link|
|
47
47
|
other_issue = link.other_issue
|
48
|
-
next if other_issue.key
|
48
|
+
next if other_issue.key.match?(/^ANON-\d+$/) # Already anonymized?
|
49
49
|
|
50
50
|
other_issue.raw['key'] = "ANON-#{counter += 1}"
|
51
51
|
other_issue.raw['fields']['summary'] = random_phrase
|
@@ -179,7 +179,7 @@ class Anonymizer
|
|
179
179
|
end
|
180
180
|
|
181
181
|
def anonymize_board_names
|
182
|
-
@all_boards.
|
182
|
+
@all_boards.each_value do |board|
|
183
183
|
board.raw['name'] = "#{random_phrase} board"
|
184
184
|
end
|
185
185
|
end
|
@@ -1,6 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'jirametrics/value_equality'
|
4
|
+
|
3
5
|
class BlockedStalledChange
|
6
|
+
include ValueEquality
|
4
7
|
attr_reader :time, :blocking_issue_keys, :flag, :status, :stalled_days, :status_is_blocking
|
5
8
|
|
6
9
|
def initialize time:, flagged: nil, status: nil, status_is_blocking: true, blocking_issue_keys: nil, stalled_days: nil
|
@@ -16,16 +19,8 @@ class BlockedStalledChange
|
|
16
19
|
def stalled? = @stalled_days || stalled_by_status?
|
17
20
|
def active? = !blocked? && !stalled?
|
18
21
|
|
19
|
-
def blocked_by_status? =
|
20
|
-
def stalled_by_status? =
|
21
|
-
|
22
|
-
def ==(other)
|
23
|
-
(other.class == self.class) && (other.state == state)
|
24
|
-
end
|
25
|
-
|
26
|
-
def state
|
27
|
-
instance_variables.map { |variable| instance_variable_get variable }
|
28
|
-
end
|
22
|
+
def blocked_by_status? = @status && @status_is_blocking
|
23
|
+
def stalled_by_status? = @status && !@status_is_blocking
|
29
24
|
|
30
25
|
def reasons
|
31
26
|
result = []
|
data/lib/jirametrics/board.rb
CHANGED
@@ -17,15 +17,10 @@ class Board
|
|
17
17
|
# visible on the board. If the board is configured to have a kanban backlog then it will have
|
18
18
|
# statuses matched to it and otherwise, there will be no statuses.
|
19
19
|
if kanban?
|
20
|
-
assert_jira_behaviour_true(columns[0]['name'] == 'Backlog') do
|
21
|
-
"Expected first column to be called Backlog: #{raw}"
|
22
|
-
end
|
23
|
-
|
24
20
|
@backlog_statuses = @possible_statuses.expand_statuses(status_ids_from_column columns[0]) do |unknown_status|
|
25
|
-
#
|
26
|
-
|
27
|
-
|
28
|
-
'configuration'
|
21
|
+
# There is a status defined as being 'backlog' that is no longer being returned in statuses.
|
22
|
+
# We used to display a warning for this but honestly, there is nothing that anyone can do about it
|
23
|
+
# so now we just quietly ignore it.
|
29
24
|
end
|
30
25
|
columns = columns[1..]
|
31
26
|
else
|
@@ -33,10 +28,10 @@ class Board
|
|
33
28
|
@backlog_statuses = []
|
34
29
|
end
|
35
30
|
|
36
|
-
@visible_columns = columns.
|
31
|
+
@visible_columns = columns.filter_map do |column|
|
37
32
|
# It's possible for a column to be defined without any statuses and in this case, it won't be visible.
|
38
33
|
BoardColumn.new column unless status_ids_from_column(column).empty?
|
39
|
-
end
|
34
|
+
end
|
40
35
|
end
|
41
36
|
|
42
37
|
def server_url_prefix
|
@@ -51,7 +46,7 @@ class Board
|
|
51
46
|
end
|
52
47
|
|
53
48
|
def status_ids_from_column column
|
54
|
-
column['statuses']
|
49
|
+
column['statuses']&.collect { |status| status['id'].to_i } || []
|
55
50
|
end
|
56
51
|
|
57
52
|
def status_ids_in_or_right_of_column column_name
|
@@ -65,7 +60,7 @@ class Board
|
|
65
60
|
end
|
66
61
|
|
67
62
|
unless found_it
|
68
|
-
column_names = @visible_columns.collect
|
63
|
+
column_names = @visible_columns.collect { |c| c.name.inspect }.join(', ')
|
69
64
|
raise "No visible column with name: #{column_name.inspect} Possible options are: #{column_names}"
|
70
65
|
end
|
71
66
|
status_ids
|
@@ -84,7 +79,10 @@ class Board
|
|
84
79
|
end
|
85
80
|
|
86
81
|
def project_id
|
87
|
-
@raw['location']
|
82
|
+
location = @raw['location']
|
83
|
+
return nil unless location
|
84
|
+
|
85
|
+
location['id'] if location['type'] == 'project'
|
88
86
|
end
|
89
87
|
|
90
88
|
def name
|
@@ -46,7 +46,7 @@ class ChartBase
|
|
46
46
|
|
47
47
|
# Render the file and then wrap it with standard headers and quality checks.
|
48
48
|
def wrap_and_render caller_binding, file
|
49
|
-
result =
|
49
|
+
result = +''
|
50
50
|
result << "<h1>#{@header_text}</h1>" if @header_text
|
51
51
|
result << ERB.new(@description_text).result(caller_binding) if @description_text
|
52
52
|
result << render(caller_binding, file)
|
@@ -74,7 +74,7 @@ class ChartBase
|
|
74
74
|
type: 'bar',
|
75
75
|
label: label,
|
76
76
|
data: date_issues_list.collect do |date, issues|
|
77
|
-
issues.
|
77
|
+
issues.sort_by!(&:key_as_i)
|
78
78
|
title = "#{label} (#{label_issues issues.size})"
|
79
79
|
{
|
80
80
|
x: date,
|
@@ -189,7 +189,7 @@ class ChartBase
|
|
189
189
|
begin
|
190
190
|
statuses = board.possible_statuses.expand_statuses([name_or_id])
|
191
191
|
rescue RuntimeError => e
|
192
|
-
return "<span style='color: red'>#{name_or_id}</span>" if e.message
|
192
|
+
return "<span style='color: red'>#{name_or_id}</span>" if e.message.match?(/^Status not found:/)
|
193
193
|
|
194
194
|
throw e
|
195
195
|
end
|
@@ -212,7 +212,7 @@ class ChartBase
|
|
212
212
|
end
|
213
213
|
|
214
214
|
def random_color
|
215
|
-
"
|
215
|
+
"##{Random.bytes(3).unpack1('H*')}"
|
216
216
|
end
|
217
217
|
|
218
218
|
def canvas width:, height:, responsive: true
|
@@ -233,7 +233,7 @@ class ChartBase
|
|
233
233
|
@issues = issues
|
234
234
|
return unless @filter_issues_block
|
235
235
|
|
236
|
-
@issues = issues.
|
236
|
+
@issues = issues.filter_map { |i| @filter_issues_block.call(i) }.uniq
|
237
237
|
puts @issues.collect(&:key).join(', ')
|
238
238
|
end
|
239
239
|
end
|
@@ -58,7 +58,7 @@ class CycletimeHistogram < ChartBase
|
|
58
58
|
{
|
59
59
|
type: 'bar',
|
60
60
|
label: label,
|
61
|
-
data: keys.sort.
|
61
|
+
data: keys.sort.filter_map do |key|
|
62
62
|
next if histogram_data[key].zero?
|
63
63
|
|
64
64
|
{
|
@@ -66,7 +66,7 @@ class CycletimeHistogram < ChartBase
|
|
66
66
|
y: histogram_data[key],
|
67
67
|
title: "#{histogram_data[key]} items completed in #{label_days key}"
|
68
68
|
}
|
69
|
-
end
|
69
|
+
end,
|
70
70
|
backgroundColor: color,
|
71
71
|
borderRadius: 0
|
72
72
|
}
|
@@ -61,7 +61,7 @@ class CycletimeScatterplot < ChartBase
|
|
61
61
|
label = rules.label
|
62
62
|
color = rules.color
|
63
63
|
percent_line = calculate_percent_line completed_issues_by_type
|
64
|
-
data = completed_issues_by_type.
|
64
|
+
data = completed_issues_by_type.filter_map { |issue| data_for_issue(issue) }
|
65
65
|
data_sets << {
|
66
66
|
label: "#{label} (85% at #{label_days(percent_line)})",
|
67
67
|
data: data,
|
@@ -88,7 +88,10 @@ class CycletimeScatterplot < ChartBase
|
|
88
88
|
|
89
89
|
# The trend calculation works with numbers only so convert Time to an int and back
|
90
90
|
calculator = TrendLineCalculator.new(points)
|
91
|
-
data_points = calculator.chart_datapoints
|
91
|
+
data_points = calculator.chart_datapoints(
|
92
|
+
range: time_range.begin.to_i..time_range.end.to_i,
|
93
|
+
max_y: @highest_cycletime
|
94
|
+
)
|
92
95
|
data_points.each do |point_hash|
|
93
96
|
point_hash[:x] = chart_format Time.at(point_hash[:x])
|
94
97
|
end
|
@@ -50,7 +50,7 @@ class DailyWipChart < ChartBase
|
|
50
50
|
def select_possible_rules issue_rules_by_active_date
|
51
51
|
possible_rules = []
|
52
52
|
issue_rules_by_active_date.each_pair do |_date, issues_rules_list|
|
53
|
-
issues_rules_list.each do |_issue, rules|
|
53
|
+
issues_rules_list.each do |_issue, rules| # rubocop:disable Style/HashEachMethods
|
54
54
|
possible_rules << rules unless possible_rules.any? { |r| r.group == rules.group }
|
55
55
|
end
|
56
56
|
end
|
@@ -83,7 +83,7 @@ class DataQualityReport < ChartBase
|
|
83
83
|
end
|
84
84
|
|
85
85
|
def initialize_entries
|
86
|
-
@entries = @issues.
|
86
|
+
@entries = @issues.filter_map do |issue|
|
87
87
|
cycletime = issue.board.cycletime
|
88
88
|
started = cycletime.started_time(issue)
|
89
89
|
stopped = cycletime.stopped_time(issue)
|
@@ -91,7 +91,7 @@ class DataQualityReport < ChartBase
|
|
91
91
|
next if started && started > time_range.end
|
92
92
|
|
93
93
|
Entry.new started: started, stopped: stopped, issue: issue
|
94
|
-
end
|
94
|
+
end
|
95
95
|
|
96
96
|
@entries.sort! do |a, b|
|
97
97
|
a.issue.key =~ /.+-(\d+)$/
|
@@ -107,11 +107,11 @@ class DataQualityReport < ChartBase
|
|
107
107
|
def scan_for_completed_issues_without_a_start_time entry:
|
108
108
|
return unless entry.stopped && entry.started.nil?
|
109
109
|
|
110
|
-
status_names = entry.issue.changes.
|
110
|
+
status_names = entry.issue.changes.filter_map do |change|
|
111
111
|
next unless change.status?
|
112
112
|
|
113
113
|
format_status change.value, board: entry.issue.board
|
114
|
-
end
|
114
|
+
end
|
115
115
|
|
116
116
|
entry.report(
|
117
117
|
problem_key: :completed_but_not_started,
|
@@ -78,7 +78,7 @@ class DependencyChart < ChartBase
|
|
78
78
|
end
|
79
79
|
|
80
80
|
def make_dot_link issue_link:, link_rules:
|
81
|
-
result =
|
81
|
+
result = +''
|
82
82
|
result << issue_link.origin.key.inspect
|
83
83
|
result << ' -> '
|
84
84
|
result << issue_link.other_issue.key.inspect
|
@@ -91,11 +91,11 @@ class DependencyChart < ChartBase
|
|
91
91
|
end
|
92
92
|
|
93
93
|
def make_dot_issue issue:, issue_rules:
|
94
|
-
result =
|
94
|
+
result = +''
|
95
95
|
result << issue.key.inspect
|
96
96
|
result << '['
|
97
97
|
label = issue_rules.label || "#{issue.key}|#{issue.type}"
|
98
|
-
label = label.inspect unless label
|
98
|
+
label = label.inspect unless label.match?(/^<.+>$/)
|
99
99
|
result << "label=#{label}"
|
100
100
|
result << ',shape=Mrecord'
|
101
101
|
tooltip = "#{issue.key}: #{issue.summary}"
|
@@ -160,6 +160,7 @@ class DependencyChart < ChartBase
|
|
160
160
|
dot_graph << '}'
|
161
161
|
|
162
162
|
return nil if visible_issues.empty?
|
163
|
+
|
163
164
|
dot_graph
|
164
165
|
end
|
165
166
|
|
@@ -205,7 +206,7 @@ class DependencyChart < ChartBase
|
|
205
206
|
line.gsub!(/[{<]/, '[')
|
206
207
|
line.gsub!(/[}>]/, ']')
|
207
208
|
line.gsub!(/\s*&\s*/, ' and ')
|
208
|
-
line.
|
209
|
+
line.delete!('|')
|
209
210
|
|
210
211
|
if line.length > max_width
|
211
212
|
line.gsub(/(.{1,#{max_width}})(\s+|$)/, "\\1#{separator}").strip
|
@@ -15,25 +15,6 @@ class DownloadConfig
|
|
15
15
|
instance_eval(&@block)
|
16
16
|
end
|
17
17
|
|
18
|
-
def project_key _key = nil
|
19
|
-
raise 'project, filter, and jql directives are no longer supported. See ' \
|
20
|
-
'https://github.com/mikebowler/jira-export/wiki/Deprecated#project-filter-and-jql-are-no-longer-supported-in-the-download-section'
|
21
|
-
end
|
22
|
-
|
23
|
-
def board_ids *ids
|
24
|
-
deprecated message: 'board_ids in the download block are deprecated. See https://github.com/mikebowler/jira-export/wiki/Deprecated'
|
25
|
-
@board_ids = ids unless ids.empty?
|
26
|
-
@board_ids
|
27
|
-
end
|
28
|
-
|
29
|
-
def filter_name _filter = nil
|
30
|
-
project_key
|
31
|
-
end
|
32
|
-
|
33
|
-
def jql _query = nil
|
34
|
-
project_key
|
35
|
-
end
|
36
|
-
|
37
18
|
def rolling_date_count count = nil
|
38
19
|
@rolling_date_count = count unless count.nil?
|
39
20
|
@rolling_date_count
|
@@ -2,21 +2,22 @@
|
|
2
2
|
|
3
3
|
require 'cgi'
|
4
4
|
require 'json'
|
5
|
-
require 'English'
|
6
5
|
|
7
6
|
class Downloader
|
8
7
|
CURRENT_METADATA_VERSION = 4
|
9
8
|
|
10
|
-
attr_accessor :metadata, :quiet_mode
|
9
|
+
attr_accessor :metadata, :quiet_mode
|
10
|
+
attr_reader :file_system
|
11
11
|
|
12
12
|
# For testing only
|
13
13
|
attr_reader :start_date_in_query
|
14
14
|
|
15
|
-
def initialize download_config:,
|
15
|
+
def initialize download_config:, file_system:, jira_gateway:
|
16
16
|
@metadata = {}
|
17
17
|
@download_config = download_config
|
18
18
|
@target_path = @download_config.project_config.target_path
|
19
|
-
@
|
19
|
+
@file_system = file_system
|
20
|
+
@jira_gateway = jira_gateway
|
20
21
|
@board_id_to_filter_id = {}
|
21
22
|
|
22
23
|
@issue_keys_downloaded_in_current_run = []
|
@@ -27,7 +28,7 @@ class Downloader
|
|
27
28
|
log '', both: true
|
28
29
|
log @download_config.project_config.name, both: true
|
29
30
|
|
30
|
-
|
31
|
+
init_gateway
|
31
32
|
load_metadata
|
32
33
|
|
33
34
|
if @metadata['no-download']
|
@@ -47,60 +48,23 @@ class Downloader
|
|
47
48
|
save_metadata
|
48
49
|
end
|
49
50
|
|
51
|
+
def init_gateway
|
52
|
+
@jira_gateway.load_jira_config(@download_config.project_config.jira_config)
|
53
|
+
@jira_gateway.ignore_ssl_errors = @download_config.project_config.settings['ignore_ssl_errors']
|
54
|
+
end
|
55
|
+
|
50
56
|
def log text, both: false
|
51
|
-
@
|
57
|
+
@file_system.log text
|
52
58
|
puts text if both
|
53
59
|
end
|
54
60
|
|
55
61
|
def find_board_ids
|
56
62
|
ids = @download_config.project_config.board_configs.collect(&:id)
|
57
|
-
if ids.empty?
|
58
|
-
deprecated message: 'board_ids in the download block have been deprecated. See https://github.com/mikebowler/jira-export/wiki/Deprecated'
|
59
|
-
ids = @download_config.board_ids
|
60
|
-
end
|
61
63
|
raise 'Board ids must be specified' if ids.empty?
|
62
64
|
|
63
65
|
ids
|
64
66
|
end
|
65
67
|
|
66
|
-
def load_jira_config jira_config
|
67
|
-
@jira_url = jira_config['url']
|
68
|
-
@jira_email = jira_config['email']
|
69
|
-
@jira_api_token = jira_config['api_token']
|
70
|
-
@jira_personal_access_token = jira_config['personal_access_token']
|
71
|
-
|
72
|
-
raise 'When specifying an api-token, you must also specify email' if @jira_api_token && !@jira_email
|
73
|
-
|
74
|
-
if @jira_api_token && @jira_personal_access_token
|
75
|
-
raise "You can't specify both an api-token and a personal-access-token. They don't work together."
|
76
|
-
end
|
77
|
-
|
78
|
-
@cookies = (jira_config['cookies'] || []).collect { |key, value| "#{key}=#{value}" }.join(';')
|
79
|
-
end
|
80
|
-
|
81
|
-
def call_command command
|
82
|
-
log " #{command.gsub(/\s+/, ' ')}"
|
83
|
-
result = `#{command}`
|
84
|
-
log result unless $CHILD_STATUS.success?
|
85
|
-
return result if $CHILD_STATUS.success?
|
86
|
-
|
87
|
-
log "Failed call with exit status #{$CHILD_STATUS.exitstatus}. See #{@logfile_name} for details", both: true
|
88
|
-
exit $CHILD_STATUS.exitstatus
|
89
|
-
end
|
90
|
-
|
91
|
-
def make_curl_command url:
|
92
|
-
command = 'curl'
|
93
|
-
command += ' -s'
|
94
|
-
command += ' -k' if @download_config.project_config.settings['ignore_ssl_errors']
|
95
|
-
command += " --cookie #{@cookies.inspect}" unless @cookies.empty?
|
96
|
-
command += " --user #{@jira_email}:#{@jira_api_token}" if @jira_api_token
|
97
|
-
command += " -H \"Authorization: Bearer #{@jira_personal_access_token}\"" if @jira_personal_access_token
|
98
|
-
command += ' --request GET'
|
99
|
-
command += ' --header "Accept: application/json"'
|
100
|
-
command += " --url \"#{url}\""
|
101
|
-
command
|
102
|
-
end
|
103
|
-
|
104
68
|
def download_issues board_id:
|
105
69
|
log " Downloading primary issues for board #{board_id}", both: true
|
106
70
|
path = "#{@target_path}#{@download_config.project_config.file_prefix}_issues/"
|
@@ -136,10 +100,9 @@ class Downloader
|
|
136
100
|
start_at = 0
|
137
101
|
total = 1
|
138
102
|
while start_at < total
|
139
|
-
|
103
|
+
json = @jira_gateway.call_url relative_url: '/rest/api/2/search' \
|
140
104
|
"?jql=#{escaped_jql}&maxResults=#{max_results}&startAt=#{start_at}&expand=changelog&fields=*all"
|
141
105
|
|
142
|
-
json = JSON.parse call_command(command)
|
143
106
|
exit_if_call_failed json
|
144
107
|
|
145
108
|
json['issues'].each do |issue_json|
|
@@ -148,7 +111,8 @@ class Downloader
|
|
148
111
|
}
|
149
112
|
identify_other_issues_to_be_downloaded issue_json
|
150
113
|
file = "#{issue_json['key']}-#{board_id}.json"
|
151
|
-
|
114
|
+
|
115
|
+
@file_system.save_json(json: issue_json, filename: File.join(path, file))
|
152
116
|
end
|
153
117
|
|
154
118
|
total = json['total'].to_i
|
@@ -193,24 +157,24 @@ class Downloader
|
|
193
157
|
|
194
158
|
def download_statuses
|
195
159
|
log ' Downloading all statuses', both: true
|
196
|
-
|
197
|
-
json = JSON.parse call_command(command)
|
160
|
+
json = @jira_gateway.call_url relative_url: "/rest/api/2/status"
|
198
161
|
|
199
|
-
|
162
|
+
@file_system.save_json(
|
163
|
+
json: json,
|
164
|
+
filename: "#{@target_path}#{@download_config.project_config.file_prefix}_statuses.json"
|
165
|
+
)
|
200
166
|
end
|
201
167
|
|
202
168
|
def download_board_configuration board_id:
|
203
169
|
log " Downloading board configuration for board #{board_id}", both: true
|
204
|
-
|
205
|
-
|
206
|
-
json = JSON.parse call_command(command)
|
170
|
+
json = @jira_gateway.call_url relative_url: "/rest/agile/1.0/board/#{board_id}/configuration"
|
207
171
|
exit_if_call_failed json
|
208
172
|
|
209
173
|
@board_id_to_filter_id[board_id] = json['filter']['id'].to_i
|
210
174
|
# @board_configuration = json if @download_config.board_ids.size == 1
|
211
175
|
|
212
176
|
file_prefix = @download_config.project_config.file_prefix
|
213
|
-
|
177
|
+
@file_system.save_json json: json, filename: "#{@target_path}#{file_prefix}_board_#{board_id}_configuration.json"
|
214
178
|
|
215
179
|
download_sprints board_id: board_id if json['type'] == 'scrum'
|
216
180
|
end
|
@@ -223,34 +187,28 @@ class Downloader
|
|
223
187
|
is_last = false
|
224
188
|
|
225
189
|
while is_last == false
|
226
|
-
|
190
|
+
json = @jira_gateway.call_url relative_url: "/rest/agile/1.0/board/#{board_id}/sprint?" \
|
227
191
|
"maxResults=#{max_results}&startAt=#{start_at}"
|
228
|
-
json = JSON.parse call_command(command)
|
229
192
|
exit_if_call_failed json
|
230
193
|
|
231
|
-
|
194
|
+
@file_system.save_json(
|
195
|
+
json: json,
|
196
|
+
filename: "#{@target_path}#{file_prefix}_board_#{board_id}_sprints_#{start_at}.json"
|
197
|
+
)
|
232
198
|
is_last = json['isLast']
|
233
199
|
max_results = json['maxResults']
|
234
200
|
start_at += json['values'].size
|
235
201
|
end
|
236
202
|
end
|
237
203
|
|
238
|
-
def write_json json, filename
|
239
|
-
file_path = File.dirname(filename)
|
240
|
-
FileUtils.mkdir_p file_path unless File.exist?(file_path)
|
241
|
-
|
242
|
-
File.write(filename, JSON.pretty_generate(json))
|
243
|
-
end
|
244
|
-
|
245
204
|
def metadata_pathname
|
246
205
|
"#{@target_path}#{@download_config.project_config.file_prefix}_meta.json"
|
247
206
|
end
|
248
207
|
|
249
208
|
def load_metadata
|
250
209
|
# If we've never done a download before then this file won't be there. That's ok.
|
251
|
-
|
252
|
-
|
253
|
-
hash = JSON.parse(File.read metadata_pathname)
|
210
|
+
hash = file_system.load_json(metadata_pathname, fail_on_error: false)
|
211
|
+
return if hash.nil?
|
254
212
|
|
255
213
|
# Only use the saved metadata if the version number is the same one that we're currently using.
|
256
214
|
# If the cached data is in an older format then we're going to throw most of it away.
|
@@ -283,13 +241,13 @@ class Downloader
|
|
283
241
|
|
284
242
|
@metadata['jira_url'] = @jira_url
|
285
243
|
|
286
|
-
|
244
|
+
@file_system.save_json json: @metadata, filename: metadata_pathname
|
287
245
|
end
|
288
246
|
|
289
247
|
def remove_old_files
|
290
248
|
file_prefix = @download_config.project_config.file_prefix
|
291
249
|
Dir.foreach @target_path do |file|
|
292
|
-
next unless file
|
250
|
+
next unless file.match?(/^#{file_prefix}_\d+\.json$/)
|
293
251
|
|
294
252
|
File.unlink "#{@target_path}#{file}"
|
295
253
|
end
|
@@ -301,7 +259,7 @@ class Downloader
|
|
301
259
|
return unless File.exist? path
|
302
260
|
|
303
261
|
Dir.foreach path do |file|
|
304
|
-
next unless file
|
262
|
+
next unless file.match?(/\.json$/)
|
305
263
|
|
306
264
|
File.unlink File.join(path, file)
|
307
265
|
end
|