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
@@ -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.5'
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