jirametrics 1.4 → 2.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -8,10 +8,10 @@ class ProjectConfig
8
8
 
9
9
  attr_reader :target_path, :jira_config, :all_boards, :possible_statuses,
10
10
  :download_config, :file_configs, :exporter, :data_version, :name, :board_configs,
11
- :settings
12
- attr_accessor :time_range, :jira_url, :project_id
11
+ :settings, :id
12
+ attr_accessor :time_range, :jira_url, :id
13
13
 
14
- def initialize exporter:, jira_config:, block:, target_path: '.', name: ''
14
+ def initialize exporter:, jira_config:, block:, target_path: '.', name: '', id: nil
15
15
  @exporter = exporter
16
16
  @block = block
17
17
  @file_configs = []
@@ -21,6 +21,7 @@ class ProjectConfig
21
21
  @possible_statuses = StatusCollection.new
22
22
  @name = name
23
23
  @board_configs = []
24
+ @all_boards = {}
24
25
  @settings = {
25
26
  'stalled_threshold' => 5,
26
27
  'blocked_statuses' => [],
@@ -32,6 +33,7 @@ class ProjectConfig
32
33
  'blocked' => '#FF7400'
33
34
  }
34
35
  }
36
+ @id = id
35
37
  end
36
38
 
37
39
  def evaluate_next_level
@@ -41,7 +43,7 @@ class ProjectConfig
41
43
  def run
42
44
  unless aggregated_project?
43
45
  load_all_boards
44
- @project_id = @all_boards.first.last&.project_id
46
+ @id = guess_project_id
45
47
  load_status_category_mappings
46
48
  load_project_metadata
47
49
  load_sprints
@@ -54,7 +56,23 @@ class ProjectConfig
54
56
  @file_configs.each do |file_config|
55
57
  file_config.run
56
58
  end
59
+ end
60
+
61
+ def guess_project_id
62
+ return @id if @id
63
+
64
+ previous_id = nil
65
+ @all_boards.each_value do |board|
66
+ project_id = board.project_id
67
+
68
+ # If the id is ambiguous then return nil for now. The user will get an error later
69
+ # in the case where we need it to be unambiguous. Sometimes we don't care and there's
70
+ # no point forcing the user to enter a project id if we don't need it.
71
+ return nil if previous_id && project_id && previous_id != project_id
57
72
 
73
+ previous_id = project_id if project_id
74
+ end
75
+ previous_id
58
76
  end
59
77
 
60
78
  def aggregated_project?
@@ -92,15 +110,7 @@ class ProjectConfig
92
110
  @file_prefix
93
111
  end
94
112
 
95
- def status_category_mapping status:, category:, type: nil
96
- puts "Deprecated: ProjectConfig.status_category_mapping no longer needs a type: #{type.inspect}" if type
97
-
98
- status_object = find_status(name: status)
99
- if status_object
100
- puts "Status/Category mapping was already present. Ignoring redefinition: #{status_object}"
101
- return
102
- end
103
-
113
+ def status_category_mapping status:, category:
104
114
  add_possible_status Status.new(name: status, category_name: category)
105
115
  end
106
116
 
@@ -111,7 +121,7 @@ class ProjectConfig
111
121
  board_id = $1.to_i
112
122
  load_board board_id: board_id, filename: "#{@target_path}#{file}"
113
123
  end
114
- raise "No boards found in #{@target_path.inspect}" if @all_boards.nil?
124
+ raise "No boards found in #{@target_path.inspect}" if @all_boards.empty?
115
125
  end
116
126
 
117
127
  def load_board board_id:, filename:
@@ -119,38 +129,38 @@ class ProjectConfig
119
129
  raw: JSON.parse(File.read(filename)), possible_statuses: @possible_statuses
120
130
  )
121
131
  board.project_config = self
122
- (@all_boards ||= {})[board_id] = board
132
+ @all_boards[board_id] = board
123
133
  end
124
134
 
125
- def raise_with_message_about_missing_category_information
126
- message = String.new
127
- message << 'Could not determine categories for some of the statuses used in this data set.\n\n' \
128
- 'If you specify a project: then we\'ll ask Jira for those mappings. If you\'ve done that' \
129
- ' and we still don\'t have the right mapping, which is possible, then use the' \
130
- " 'status_category_mapping' declaration in your config to manually add one.\n\n" \
131
- ' The mappings we do know about are below:'
135
+ def raise_with_message_about_missing_category_information all_issues = @issues
136
+ message = +''
137
+ message << "Could not determine categories for some of the statuses used in this data set.\n" \
138
+ "Use the 'status_category_mapping' declaration in your config to manually add one.\n" \
139
+ 'The mappings we do know about are below:'
132
140
 
133
141
  @possible_statuses.each do |status|
134
- message << "\n type: #{status.type.inspect}, status: #{status.name.inspect}, " \
135
- "category: #{status.category_name.inspect}'"
142
+ message << "\n status: #{status.name.inspect}, category: #{status.category_name.inspect}"
136
143
  end
137
144
 
138
145
  message << "\n\nThe ones we're missing are the following:"
139
146
 
147
+ find_statuses_with_no_category_information(all_issues).each do |status_name|
148
+ message << "\n status: #{status_name.inspect}, category: <unknown>"
149
+ end
150
+
151
+ raise message
152
+ end
153
+
154
+ def find_statuses_with_no_category_information all_issues
140
155
  missing_statuses = []
141
- issues.each do |issue|
156
+ all_issues.each do |issue|
142
157
  issue.changes.each do |change|
143
158
  next unless change.status?
144
159
 
145
160
  missing_statuses << change.value unless find_status(name: change.value)
146
161
  end
147
162
  end
148
-
149
- missing_statuses.uniq.each do |status_name|
150
- message << "\n status: #{status_name.inspect}, category: <unknown>"
151
- end
152
-
153
- raise message
163
+ missing_statuses.uniq
154
164
  end
155
165
 
156
166
  def load_status_category_mappings
@@ -158,22 +168,14 @@ class ProjectConfig
158
168
  # We may not always have this file. Load it if we can.
159
169
  return unless File.exist? filename
160
170
 
161
- status_json_snippets = []
162
-
163
- json = JSON.parse(File.read(filename))
164
- if json[0]['statuses']
165
- # Response from /api/2/{project_code}/status
166
- json.each do |type_config|
167
- status_json_snippets += type_config['statuses']
168
- end
169
- else
170
- # Response from /api/2/status
171
- status_json_snippets = json
172
- end
173
-
174
- status_json_snippets.each do |snippet|
175
- add_possible_status Status.new(raw: snippet)
176
- end
171
+ statuses = JSON.parse(File.read(filename))
172
+ .map { |snippet| Status.new(raw: snippet) }
173
+ statuses
174
+ .find_all { |status| status.global? }
175
+ .each { |status| add_possible_status status }
176
+ statuses
177
+ .find_all { |status| status.project_scoped? }
178
+ .each { |status| add_possible_status status }
177
179
  end
178
180
 
179
181
  def load_sprints
@@ -193,33 +195,40 @@ class ProjectConfig
193
195
  end
194
196
 
195
197
  def add_possible_status status
196
- # If it's project scoped and it's not this project, just ignore it.
197
- return if status.project_id && (@project_id.nil? || status.project_id != @project_id)
198
-
199
198
  existing_status = find_status(name: status.name)
200
199
 
201
- # If it isn't there, add it and go.
202
- return @possible_statuses << status unless existing_status
200
+ if status.project_scoped?
201
+ # If the project specific status doesn't change anything then we don't care whether it's
202
+ # our project or not.
203
+ return if existing_status && existing_status.category_name == status.category_name
203
204
 
204
- # If the existing one has a project id then it's already the most precise. Ignore the new one.
205
- # No need to check categories as status_category_mapping can't add a project_id so by definition
206
- # this data came from Jira.
207
- return if existing_status && existing_status.project_id
205
+ raise_ambiguous_project_id if @id.nil?
208
206
 
209
- # If the new one has a project_id then it's more precise so replace the old one with this,
210
- # regardless of whether the categories match.
211
- if status.project_id
207
+ # Not our project, ignore it.
208
+ return unless status.project_id == @id
209
+
210
+ # Replace the old one with this
212
211
  @possible_statuses.delete(existing_status)
213
212
  @possible_statuses << status
214
213
  return
215
214
  end
216
215
 
217
- # This new status may have come from status_category_mapping so verify that categories match.
218
- if existing_status.category_name != status.category_name
219
- raise "Redefining status category #{status} with #{existing_status}. Was one set in the config?"
220
- end
216
+ # If it isn't there, add it and go.
217
+ return @possible_statuses << status unless existing_status
218
+
219
+ # We're registering the same one twice. Shouldn't be possible with the new status API but it
220
+ # did happen with the project specific one.
221
+ return if status.category_name == existing_status.category_name
221
222
 
222
- @possible_statuses << status
223
+ # If we got this far then someone has called status_category_mapping and is attempting to
224
+ # change the category.
225
+ raise "Redefining status category #{status} with #{existing_status}. Was one set in the config?"
226
+ end
227
+
228
+ def raise_ambiguous_project_id
229
+ raise 'Ambiguous project id: There is a project specific status that could affect out calculations. ' \
230
+ 'We are unable to automatically detect the id of the project so you will have to set it manually ' \
231
+ 'in the configuration like: "project id: 5"'
223
232
  end
224
233
 
225
234
  def find_status name:
@@ -243,7 +252,7 @@ class ProjectConfig
243
252
  end
244
253
 
245
254
  def to_time string
246
- string = "#{string}T00:00:00#{@timezone_offset}" if string =~ /^\d{4}-\d{2}\d{2}$/
255
+ string = "#{string}T00:00:00#{@timezone_offset}" if string.match?(/^\d{4}-\d{2}\d{2}$/)
247
256
  Time.parse string
248
257
  end
249
258
 
@@ -275,7 +284,7 @@ class ProjectConfig
275
284
  # To be used by the aggregate_config only. Not intended to be part of the public API
276
285
  def add_issues issues_list
277
286
  @issues = [] if @issues.nil?
278
- @all_boards = {} if @all_boards.nil?
287
+ @all_boards = {}
279
288
 
280
289
  issues_list.each do |issue|
281
290
  @issues << issue
@@ -298,10 +307,8 @@ class ProjectConfig
298
307
  issues_path = "#{@target_path}#{file_prefix}_issues/"
299
308
  if File.exist?(issues_path) && File.directory?(issues_path)
300
309
  issues = load_issues_from_issues_directory path: issues_path, timezone_offset: timezone_offset
301
- elsif File.exist?(@target_path) && File.directory?(@target_path)
302
- issues = load_issues_from_target_directory path: @target_path, timezone_offset: timezone_offset
303
310
  else
304
- puts "Can't find issues in either #{path} or #{@target_path}"
311
+ puts "Can't find issues in #{path}. Has a download been done?"
305
312
  end
306
313
 
307
314
  # Attach related issues
@@ -347,29 +354,12 @@ class ProjectConfig
347
354
  raise "No boards found for project #{name.inspect}" if all_boards.empty?
348
355
 
349
356
  if all_boards.size != 1
350
- puts "Multiple boards are in use for project #{name.inspect}. Picked #{(default_board.name).inspect} to attach issues to."
357
+ puts "Multiple boards are in use for project #{name.inspect}. " \
358
+ "Picked #{default_board.name.inspect} to attach issues to."
351
359
  end
352
360
  default_board
353
361
  end
354
362
 
355
- def load_issues_from_target_directory path:, timezone_offset:
356
- puts "Deprecated: issues in the target directory for project #{@name}. " \
357
- 'Download again and this should fix itself.'
358
-
359
- default_board = find_default_board
360
-
361
- issues = []
362
- Dir.foreach(path) do |filename|
363
- if filename =~ /#{file_prefix}_\d+\.json/
364
- content = JSON.parse File.read("#{path}#{filename}")
365
- content['issues'].each do |issue|
366
- issues << Issue.new(raw: issue, timezone_offset: timezone_offset, board: default_board)
367
- end
368
- end
369
- end
370
- issues
371
- end
372
-
373
363
  def load_issues_from_issues_directory path:, timezone_offset:
374
364
  issues = []
375
365
  default_board = nil
@@ -9,26 +9,7 @@ class Rules
9
9
  @ignore == true
10
10
  end
11
11
 
12
- def eql?(other)
13
- (other.class == self.class) && (other.state == state)
14
- end
15
-
16
- def state
17
- instance_variables.map { |variable| instance_variable_get variable }
18
- end
19
-
20
12
  def hash
21
- 2 # TODO: While this work, it's not performant
13
+ 2 # TODO: While this works, it's not performant
22
14
  end
23
-
24
- def inspect
25
- result = String.new
26
- result << "#{self.class}("
27
- result << instance_variables.collect do |variable|
28
- "#{variable}=#{instance_variable_get(variable).inspect}"
29
- end.join(', ')
30
- result << ')'
31
- result
32
- end
33
-
34
15
  end
@@ -54,19 +54,20 @@ class SprintBurndown < ChartBase
54
54
  change_data_by_sprint[sprint] = change_data.sort_by(&:time)
55
55
  end
56
56
 
57
- result = String.new
57
+ result = +''
58
58
  result << '<h1>Sprint Burndowns</h1>'
59
59
 
60
+ possible_colours = %w[blue orange green red brown]
60
61
  charts_to_generate = []
61
62
  charts_to_generate << [:data_set_by_story_points, 'Story Points'] if @use_story_points
62
63
  charts_to_generate << [:data_set_by_story_counts, 'Story Count'] if @use_story_counts
63
- charts_to_generate.each do |data_method, y_axis_title|
64
+ charts_to_generate.each do |data_method, y_axis_title| # rubocop:disable Style/HashEachMethods
64
65
  @summary_stats.clear
65
66
  data_sets = []
66
67
  sprints.each_with_index do |sprint, index|
67
- color = %w[blue orange green red brown][index % 5]
68
+ color = possible_colours[index % 5]
68
69
  label = sprint.name
69
- data = send(data_method, **{ sprint: sprint, change_data_for_sprint: change_data_by_sprint[sprint] })
70
+ data = send(data_method, sprint: sprint, change_data_for_sprint: change_data_by_sprint[sprint])
70
71
  data_sets << {
71
72
  label: label,
72
73
  data: data,
@@ -130,8 +131,8 @@ class SprintBurndown < ChartBase
130
131
  currently_in_sprint = in_change_item
131
132
  elsif change.story_points? && (issue_completed_time.nil? || change.time < issue_completed_time)
132
133
  action = :story_points
133
- story_points = change.value&.to_f || 0.0
134
- value = story_points - (change.old_value&.to_f || 0.0)
134
+ story_points = change.value.to_f
135
+ value = story_points - change.old_value.to_f
135
136
  elsif completed_has_been_tracked == false && change.time == issue_completed_time
136
137
  completed_has_been_tracked = true
137
138
  action = :issue_stopped
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'jirametrics/value_equality'
4
+
3
5
  class SprintIssueChangeData
6
+ include ValueEquality
4
7
  attr_reader :time, :action, :value, :issue, :story_points
5
8
 
6
9
  def initialize time:, action:, value:, issue:, story_points:
@@ -11,16 +14,8 @@ class SprintIssueChangeData
11
14
  @story_points = story_points
12
15
  end
13
16
 
14
- def eql?(other)
15
- (other.class == self.class) && (other.state == state)
16
- end
17
-
18
- def state
19
- instance_variables.map { |variable| instance_variable_get variable }
20
- end
21
-
22
17
  def inspect
23
- result = String.new
18
+ result = +''
24
19
  result << 'SprintIssueChangeData('
25
20
  result << instance_variables.collect do |variable|
26
21
  "#{variable}=#{instance_variable_get(variable).inspect}"
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'jirametrics/value_equality'
4
+
3
5
  class Status
4
- attr_reader :id, :type, :category_name, :category_id, :project_id
6
+ include ValueEquality
7
+ attr_reader :id, :category_name, :category_id, :project_id
5
8
  attr_accessor :name
6
9
 
7
10
  def initialize name: nil, id: nil, category_name: nil, category_id: nil, project_id: nil, raw: nil
@@ -11,20 +14,27 @@ class Status
11
14
  @category_id = category_id
12
15
  @project_id = project_id
13
16
 
14
- if raw
15
- @raw = raw
16
- @name = raw['name']
17
- @id = raw['id'].to_i
17
+ return unless raw
18
+
19
+ @raw = raw
20
+ @name = raw['name']
21
+ @id = raw['id'].to_i
18
22
 
19
- category_config = raw['statusCategory']
20
- @category_name = category_config['name']
21
- @category_id = category_config['id'].to_i
23
+ category_config = raw['statusCategory']
24
+ @category_name = category_config['name']
25
+ @category_id = category_config['id'].to_i
22
26
 
23
- # If this is a NextGen project then this status may be project specific. When this field is
24
- # nil then the status is global.
25
- @project_id = raw['scope']&.[]('project')&.[]('id')
26
- end
27
+ # If this is a NextGen project then this status may be project specific. When this field is
28
+ # nil then the status is global.
29
+ @project_id = raw['scope']&.[]('project')&.[]('id')
30
+ end
27
31
 
32
+ def project_scoped?
33
+ !!@project_id
34
+ end
35
+
36
+ def global?
37
+ !project_scoped?
28
38
  end
29
39
 
30
40
  def to_s
@@ -32,13 +42,7 @@ class Status
32
42
  " category_name=#{@category_name.inspect}, category_id=#{@category_id.inspect}, project_id=#{@project_id})"
33
43
  end
34
44
 
35
- def eql?(other)
36
- (other.class == self.class) && (other.state == state)
37
- end
38
-
39
- def state
40
- instance_variables
41
- .reject {|variable| variable == :@raw}
42
- .map { |variable| instance_variable_get variable }
45
+ def value_equality_ignored_variables
46
+ [:@raw]
43
47
  end
44
48
  end
@@ -9,13 +9,13 @@ class StatusCollection
9
9
  including = expand_statuses including
10
10
  excluding = expand_statuses excluding
11
11
 
12
- @list.collect do |status|
12
+ @list.filter_map do |status|
13
13
  keep = status.category_name == category_name ||
14
14
  including.any? { |s| s.name == status.name }
15
15
  keep = false if excluding.any? { |s| s.name == status.name }
16
16
 
17
17
  status.name if keep
18
- end.compact
18
+ end
19
19
  end
20
20
 
21
21
  def expand_statuses names_or_ids
@@ -58,7 +58,7 @@ class StoryPointAccuracyChart < ChartBase
58
58
  [
59
59
  [completed_hash, 'Completed', '#66FF99', 'green', false],
60
60
  [aging_hash, 'Still in progress', '#FFCCCB', 'red', true]
61
- ].collect do |hash, label, fill_color, border_color, starts_hidden|
61
+ ].filter_map do |hash, label, fill_color, border_color, starts_hidden|
62
62
  # We sort so that the smaller circles are in front of the bigger circles.
63
63
  data = hash.sort(&hash_sorter).collect do |key, values|
64
64
  estimate, cycle_time = *key
@@ -71,7 +71,7 @@ class StoryPointAccuracyChart < ChartBase
71
71
  'r' => values.size * 2,
72
72
  'title' => title
73
73
  }
74
- end.compact
74
+ end
75
75
  next if data.empty?
76
76
 
77
77
  {
@@ -121,11 +121,6 @@ class StoryPointAccuracyChart < ChartBase
121
121
  story_points
122
122
  end
123
123
 
124
- def grouping range:, color: # rubocop:disable Lint/UnusedMethodArgument
125
- deprecated message: 'The grouping declaration is no longer supported on the StoryPointEstimateChart ' \
126
- 'as we now use a bubble chart rather than colors'
127
- end
128
-
129
124
  def y_axis label:, sort_order: nil, &block
130
125
  @y_axis_sort_order = sort_order
131
126
  @y_axis_label = label
@@ -73,10 +73,10 @@ class ThroughputChart < ChartBase
73
73
 
74
74
  def throughput_dataset periods:, completed_issues:
75
75
  periods.collect do |period|
76
- closed_issues = completed_issues.collect do |issue|
76
+ closed_issues = completed_issues.filter_map do |issue|
77
77
  stop_date = issue.board.cycletime.stopped_time(issue)&.to_date
78
78
  [stop_date, issue] if stop_date && period.include?(stop_date)
79
- end.compact
79
+ end
80
80
 
81
81
  date_label = "on #{period.end}"
82
82
  date_label = "between #{period.begin} and #{period.end}" unless period.begin == period.end
@@ -8,10 +8,10 @@ class TrendLineCalculator
8
8
  @valid = points.size >= 2
9
9
  return unless valid?
10
10
 
11
- sum_of_x = points.collect { |x, _y| x }.sum
12
- sum_of_y = points.collect { |_x, y| y }.sum
13
- sum_of_xy = points.collect { |x, y| x * y }.sum
14
- sum_of_x2 = points.collect { |x, _y| x * x }.sum
11
+ sum_of_x = points.sum { |x, _y| x }
12
+ sum_of_y = points.sum { |_x, y| y }
13
+ sum_of_xy = points.sum { |x, y| x * y }
14
+ sum_of_x2 = points.sum { |x, _y| x * x }
15
15
  n = points.size.to_f
16
16
 
17
17
  @slope = ((n * sum_of_xy) - (sum_of_x * sum_of_y)) / ((n * sum_of_x2) - (sum_of_x * sum_of_x))
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Perform an equality check based on whether the two objects have the same values
4
+ module ValueEquality
5
+ def ==(other)
6
+ return false unless other.class == self.class
7
+
8
+ code = lambda do |object|
9
+ names = object.instance_variables
10
+ if object.respond_to? :value_equality_ignored_variables
11
+ ignored_variables = object.value_equality_ignored_variables
12
+ names.reject! { |n| ignored_variables.include? n }
13
+ end
14
+ names.map { |variable| instance_variable_get variable }
15
+ end
16
+
17
+ code.call(self) == code.call(other)
18
+ end
19
+
20
+ def eql?(other)
21
+ self == other
22
+ end
23
+ end
data/lib/jirametrics.rb CHANGED
@@ -44,6 +44,7 @@ class JiraMetrics < Thor
44
44
  exit 1
45
45
  end
46
46
 
47
+ require 'jirametrics/value_equality'
47
48
  require 'jirametrics/chart_base'
48
49
  require 'jirametrics/rules'
49
50
  require 'jirametrics/grouping_rules'
@@ -55,6 +56,7 @@ class JiraMetrics < Thor
55
56
  require 'jirametrics/expedited_chart'
56
57
  require 'jirametrics/board_config'
57
58
  require 'jirametrics/file_config'
59
+ require 'jirametrics/jira_gateway'
58
60
  require 'jirametrics/trend_line_calculator'
59
61
  require 'jirametrics/status'
60
62
  require 'jirametrics/issue_link'
@@ -81,7 +83,7 @@ class JiraMetrics < Thor
81
83
  require 'jirametrics/self_or_issue_dispatcher'
82
84
  require 'jirametrics/throughput_chart'
83
85
  require 'jirametrics/exporter'
84
- require 'jirametrics/json_file_loader'
86
+ require 'jirametrics/file_system'
85
87
  require 'jirametrics/blocked_stalled_change'
86
88
  require 'jirametrics/board_column'
87
89
  require 'jirametrics/anonymizer'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jirametrics
3
3
  version: !ruby/object:Gem::Version
4
- version: '1.4'
4
+ version: '2.0'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Bowler
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-04-16 00:00:00.000000000 Z
11
+ date: 2024-04-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: random-word
@@ -52,20 +52,6 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: 1.2.2
55
- - !ruby/object:Gem::Dependency
56
- name: rspec
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "~>"
60
- - !ruby/object:Gem::Version
61
- version: '3.4'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - "~>"
67
- - !ruby/object:Gem::Version
68
- version: '3.4'
69
55
  description: Tool to extract metrics from Jira and export to either a report or to
70
56
  CSV files
71
57
  email: mbowler@gargoylesoftware.com
@@ -106,6 +92,7 @@ files:
106
92
  - lib/jirametrics/experimental/info.rb
107
93
  - lib/jirametrics/exporter.rb
108
94
  - lib/jirametrics/file_config.rb
95
+ - lib/jirametrics/file_system.rb
109
96
  - lib/jirametrics/fix_version.rb
110
97
  - lib/jirametrics/groupable_issue_chart.rb
111
98
  - lib/jirametrics/grouping_rules.rb
@@ -127,7 +114,7 @@ files:
127
114
  - lib/jirametrics/html_report_config.rb
128
115
  - lib/jirametrics/issue.rb
129
116
  - lib/jirametrics/issue_link.rb
130
- - lib/jirametrics/json_file_loader.rb
117
+ - lib/jirametrics/jira_gateway.rb
131
118
  - lib/jirametrics/project_config.rb
132
119
  - lib/jirametrics/rules.rb
133
120
  - lib/jirametrics/self_or_issue_dispatcher.rb
@@ -140,6 +127,7 @@ files:
140
127
  - lib/jirametrics/throughput_chart.rb
141
128
  - lib/jirametrics/tree_organizer.rb
142
129
  - lib/jirametrics/trend_line_calculator.rb
130
+ - lib/jirametrics/value_equality.rb
143
131
  homepage: https://github.com/mikebowler/jirametrics
144
132
  licenses:
145
133
  - Apache-2.0
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'json'
4
-
5
- class JsonFileLoader
6
- def load filename
7
- JSON.parse File.read(filename)
8
- end
9
- end