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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/bin/jirametrics +1 -0
  3. data/lib/jirametrics/aggregate_config.rb +2 -1
  4. data/lib/jirametrics/aging_work_bar_chart.rb +3 -3
  5. data/lib/jirametrics/aging_work_in_progress_chart.rb +6 -6
  6. data/lib/jirametrics/anonymizer.rb +3 -3
  7. data/lib/jirametrics/blocked_stalled_change.rb +5 -10
  8. data/lib/jirametrics/board.rb +11 -13
  9. data/lib/jirametrics/chart_base.rb +5 -5
  10. data/lib/jirametrics/cycletime_histogram.rb +2 -2
  11. data/lib/jirametrics/cycletime_scatterplot.rb +5 -2
  12. data/lib/jirametrics/daily_wip_chart.rb +1 -1
  13. data/lib/jirametrics/data_quality_report.rb +4 -4
  14. data/lib/jirametrics/dependency_chart.rb +5 -4
  15. data/lib/jirametrics/download_config.rb +0 -19
  16. data/lib/jirametrics/downloader.rb +32 -74
  17. data/lib/jirametrics/examples/aggregated_project.rb +61 -3
  18. data/lib/jirametrics/examples/standard_project.rb +3 -3
  19. data/lib/jirametrics/expedited_chart.rb +4 -4
  20. data/lib/jirametrics/experimental/generator.rb +5 -4
  21. data/lib/jirametrics/experimental/info.rb +2 -2
  22. data/lib/jirametrics/exporter.rb +16 -30
  23. data/lib/jirametrics/file_system.rb +36 -0
  24. data/lib/jirametrics/groupable_issue_chart.rb +0 -9
  25. data/lib/jirametrics/hierarchy_table.rb +1 -1
  26. data/lib/jirametrics/html_report_config.rb +5 -30
  27. data/lib/jirametrics/issue.rb +1 -7
  28. data/lib/jirametrics/issue_link.rb +0 -7
  29. data/lib/jirametrics/jira_gateway.rb +59 -0
  30. data/lib/jirametrics/project_config.rb +78 -88
  31. data/lib/jirametrics/rules.rb +1 -20
  32. data/lib/jirametrics/sprint_burndown.rb +7 -6
  33. data/lib/jirametrics/sprint_issue_change_data.rb +4 -9
  34. data/lib/jirametrics/status.rb +24 -20
  35. data/lib/jirametrics/status_collection.rb +2 -2
  36. data/lib/jirametrics/story_point_accuracy_chart.rb +2 -7
  37. data/lib/jirametrics/throughput_chart.rb +2 -2
  38. data/lib/jirametrics/trend_line_calculator.rb +4 -4
  39. data/lib/jirametrics/value_equality.rb +23 -0
  40. data/lib/jirametrics.rb +3 -1
  41. metadata +5 -17
  42. data/lib/jirametrics/json_file_loader.rb +0 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bc425ec4e1efcb78c2e12facdbfb0109383c379c1e08a83f46e3587dceabda7a
4
- data.tar.gz: aa98dca9360bec6250a423790f7e1d54d5d3c102804b514468aad220c4913cad
3
+ metadata.gz: eb163ed079209739994cb9a963533c6afaddc6ca987bf9c428aeee94945ecf40
4
+ data.tar.gz: 571d3d09cde660cd7038d7d996e0c5e44cb7dfe21fdf892a931ef51db84fb9cf
5
5
  SHA512:
6
- metadata.gz: 7ad6615bf254963dabe4557e907f6bacb0340562d9120c9fc3d5ca722b88709531b3e58148d1957707931e88512950053be04c49ace5d531c2990e99cb6e4254
7
- data.tar.gz: 06b0af0ce2512d028bb0d11771610c75d88246113a0bc3cb789a348f4d3e24c745d419802248f4ad25caebf845e682ecabd7aa24b1aa39a911164fe0332d111a
6
+ metadata.gz: 3212ef8b2d1a2d8eb2be8e614534aa4b614e8f8003f94d0b8cd0239839fbca90c30f8ce062effede748567f53c01c4235ec179d53fd04f01f9f0bd7a288ddb9b
7
+ data.tar.gz: f25bc331a72956f24dac94205630db7cff9d98a933ccd76f2ddff1132702dfd284c9ad526518c6e91169fb3cc41e604695d1c792f2b2496b870e93a6ff165684
data/bin/jirametrics CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'jirametrics'
4
5
  JiraMetrics.start(ARGV)
@@ -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, &block
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 block.call(day)
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.collect { |issue| issue.board.cycletime.cycletime(issue) }.compact.sort
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].collect do |issue|
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.compact,
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.collect do |column|
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.compact.reverse
109
+ end.reverse
110
110
  end
111
111
 
112
112
  def ages_of_issues_that_crossed_column_boundary issues:, status_ids:
113
- issues.collect do |issue|
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.compact
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.gsub(/_/, ' ')
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 =~ /^ANON-\d+$/ # Already anonymized?
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.values.each do |board|
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? = (@status && @status_is_blocking)
20
- def stalled_by_status? = (@status && !@status_is_blocking)
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 = []
@@ -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
- # Yet another "theoretically impossible and yet we've seen it in production" moment
26
- puts "Status #{unknown_status.inspect} is defined as being in the backlog for board #{name.inspect}:#{id} " \
27
- 'and yet it\'s not defined in the list of possible statuses available to the project. Check your Jira ' \
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.collect do |column|
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.compact
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'].collect { |status| status['id'].to_i }
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(&:name).collect(&:inspect).join(', ')
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']&.[]('id')
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 = String.new
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.sort! { |a, b| a.key_as_i <=> b.key_as_i }
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 =~ /^Status not found:/
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
- "\##{Random.bytes(3).unpack1('H*')}"
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.collect { |i| @filter_issues_block.call(i) }.compact.uniq
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.collect do |key|
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.compact,
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.collect { |issue| data_for_issue(issue) }.compact
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 range: time_range.begin.to_i..time_range.end.to_i, max_y: @highest_cycletime
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.collect do |issue|
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.compact
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.collect do |change|
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.compact
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 = String.new
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 = String.new
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.gsub!('|', '')
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, :logfile, :logfile_name
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:, json_file_loader: JsonFileLoader.new
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
- @json_file_loader = json_file_loader
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
- load_jira_config(@download_config.project_config.jira_config)
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
- @logfile&.puts text
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
- command = make_curl_command url: "#{@jira_url}/rest/api/2/search" \
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
- write_json(issue_json, File.join(path, file))
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
- command = make_curl_command url: "\"#{@jira_url}/rest/api/2/status\""
197
- json = JSON.parse call_command(command)
160
+ json = @jira_gateway.call_url relative_url: "/rest/api/2/status"
198
161
 
199
- write_json json, "#{@target_path}#{@download_config.project_config.file_prefix}_statuses.json"
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
- command = make_curl_command url: "#{@jira_url}/rest/agile/1.0/board/#{board_id}/configuration"
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
- write_json json, "#{@target_path}#{file_prefix}_board_#{board_id}_configuration.json"
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
- command = make_curl_command url: "#{@jira_url}/rest/agile/1.0/board/#{board_id}/sprint?" \
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
- write_json json, "#{@target_path}#{file_prefix}_board_#{board_id}_sprints_#{start_at}.json"
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
- return unless File.exist? metadata_pathname
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
- write_json @metadata, metadata_pathname
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 =~ /^#{file_prefix}_\d+\.json$/
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 =~ /\.json$/
262
+ next unless file.match?(/\.json$/)
305
263
 
306
264
  File.unlink File.join(path, file)
307
265
  end