jirametrics 1.4 → 2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|