jirametrics 2.8 → 2.9

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.
@@ -13,7 +13,12 @@ class Issue
13
13
  @changes = []
14
14
  @board = board
15
15
 
16
+ # We only check for this here because if a board isn't passed in then things will fail much
17
+ # later and be hard to find. Let's find out early.
16
18
  raise "No board for issue #{key}" if board.nil?
19
+
20
+ # There are cases where we create an Issue of fragments like linked issues and those won't have
21
+ # changelogs.
17
22
  return unless @raw['changelog']
18
23
 
19
24
  load_history_into_changes
@@ -93,9 +98,7 @@ class Issue
93
98
 
94
99
  def still_in
95
100
  result = nil
96
- @changes.each do |change|
97
- next unless change.status?
98
-
101
+ status_changes.each do |change|
99
102
  current_status_matched = yield change
100
103
 
101
104
  if current_status_matched && result.nil?
@@ -117,63 +120,71 @@ class Issue
117
120
 
118
121
  # If it ever entered one of these categories and it's still there then what was the last time it entered
119
122
  def still_in_status_category *category_names
123
+ category_ids = find_status_category_ids_by_names category_names
124
+
120
125
  still_in do |change|
121
- status = find_status_by_id change.value_id, name: change.value
122
- category_names.include?(status.category.name) || category_names.include?(status.category.id)
126
+ status = find_or_create_status id: change.value_id, name: change.value
127
+ category_ids.include? status.category.id
123
128
  end
124
129
  end
125
130
 
126
131
  def most_recent_status_change
132
+ # We artificially insert a status change to represent creation so by definition there will always be at least one.
127
133
  changes.reverse.find { |change| change.status? }
128
134
  end
129
135
 
130
136
  # Are we currently in this status? If yes, then return the most recent status change.
131
137
  def currently_in_status *status_names
132
138
  change = most_recent_status_change
133
- return false if change.nil?
134
139
 
135
140
  change if change.current_status_matches(*status_names)
136
141
  end
137
142
 
138
143
  # Are we currently in this status category? If yes, then return the most recent status change.
139
144
  def currently_in_status_category *category_names
145
+ category_ids = find_status_category_ids_by_names category_names
146
+
140
147
  change = most_recent_status_change
141
- return false if change.nil?
142
148
 
143
- status = find_status_by_id change.value_id, name: change.value
144
- change if status && category_names.include?(status.category.name)
149
+ status = find_or_create_status id: change.value_id, name: change.value
150
+ change if status && category_ids.include?(status.category.id)
145
151
  end
146
152
 
147
- def find_status_by_id id, name: nil
153
+ def find_or_create_status id:, name:
148
154
  status = board.possible_statuses.find_by_id(id)
149
- return status if status
150
155
 
151
- status = board.possible_statuses.fabricate_status_for id: id, name: name
156
+ unless status
157
+ # Have to pull this list before the call to fabricate or else the warning will incorrectly
158
+ # list this status as one it actually found
159
+ found_statuses = board.possible_statuses.to_s
160
+
161
+ status = board.possible_statuses.fabricate_status_for id: id, name: name
152
162
 
153
- message = +'The history for issue '
154
- message << key
155
- message << ' references a status ('
156
- message << name.inspect << ':' if name
157
- message << id.to_s
158
- message << ') that can\'t be found in ['
159
- message << board.possible_statuses.collect(&:to_s).join(', ')
160
- message << "]. We are guessing that this belongs to the #{status.category} status category "
161
- message << 'and that may be wrong. See https://jirametrics.org/faq/#q1 for more details'
162
- board.project_config.file_system.warning message
163
+ message = +'The history for issue '
164
+ message << key
165
+ message << ' references the status ('
166
+ message << "#{name.inspect}:#{id.inspect}"
167
+ message << ') that can\'t be found. We are guessing that this belongs to the '
168
+ message << status.category.to_s
169
+ message << ' status category but that may be wrong. See https://jirametrics.org/faq/#q1 for more '
170
+ message << 'details on defining statuses.'
171
+ board.project_config.file_system.warning message, more: "The statuses we did find are: #{found_statuses}"
172
+ end
163
173
 
164
174
  status
165
175
  end
166
176
 
167
177
  def first_status_change_after_created
168
- @changes.find { |change| change.status? && change.artificial? == false }
178
+ status_changes.find { |change| change.artificial? == false }
169
179
  end
170
180
 
171
181
  def first_time_in_status_category *category_names
172
- @changes.each do |change|
173
- next unless change.status?
182
+ category_ids = find_status_category_ids_by_names category_names
174
183
 
175
- category = find_status_by_id(change.value_id).category.name
176
- return change if category_names.include? category
184
+ status_changes.each do |change|
185
+ to_status = find_or_create_status(id: change.value_id, name: change.value)
186
+ id = to_status.category.id
187
+ return change if category_ids.include? id
177
188
  end
178
189
  nil
179
190
  end
@@ -645,6 +656,10 @@ class Issue
645
656
  end
646
657
  end
647
658
 
659
+ def status_changes
660
+ @changes.select { |change| change.status? }
661
+ end
662
+
648
663
  private
649
664
 
650
665
  def assemble_author raw
@@ -720,4 +735,13 @@ class Issue
720
735
  'toString' => first_status
721
736
  }
722
737
  end
738
+
739
+ def find_status_category_ids_by_names category_names
740
+ category_names.filter_map do |name|
741
+ list = board.possible_statuses.find_all_categories_by_name name
742
+ raise "No status categories found for name: #{name}" if list.empty?
743
+
744
+ list
745
+ end.flatten.collect(&:id)
746
+ end
723
747
  end
@@ -4,11 +4,9 @@ require 'time'
4
4
  require 'jirametrics/status_collection'
5
5
 
6
6
  class ProjectConfig
7
- include DiscardChangesBefore
8
-
9
7
  attr_reader :target_path, :jira_config, :all_boards, :possible_statuses,
10
8
  :download_config, :file_configs, :exporter, :data_version, :name, :board_configs,
11
- :settings, :aggregate_config
9
+ :settings, :aggregate_config, :discarded_changes_data
12
10
  attr_accessor :time_range, :jira_url, :id
13
11
 
14
12
  def initialize exporter:, jira_config:, block:, target_path: '.', name: '', id: nil
@@ -32,14 +30,13 @@ class ProjectConfig
32
30
  end
33
31
 
34
32
  def data_downloaded?
35
- File.exist? File.join(@target_path, "#{get_file_prefix}_meta.json")
33
+ file_system.file_exist? File.join(@target_path, "#{get_file_prefix}_meta.json")
36
34
  end
37
35
 
38
36
  def load_data
39
37
  return if @has_loaded_data
40
38
 
41
39
  @has_loaded_data = true
42
- load_all_boards
43
40
  @id = guess_project_id
44
41
  load_project_metadata
45
42
  load_sprints
@@ -53,16 +50,13 @@ class ProjectConfig
53
50
 
54
51
  return if load_only
55
52
 
56
- @board_configs.each do |board_config|
57
- board_config.run
58
- end
59
53
  @file_configs.each do |file_config|
60
54
  file_config.run
61
55
  end
62
56
  end
63
57
 
64
58
  def load_settings
65
- # This is the wierd exception that we don't ever want mocked out so we skip FileSystem entirely.
59
+ # This is the weird exception that we don't ever want mocked out so we skip FileSystem entirely.
66
60
  JSON.parse(File.read(File.join(__dir__, 'settings.json'), encoding: 'UTF-8'))
67
61
  end
68
62
 
@@ -112,6 +106,7 @@ class ProjectConfig
112
106
 
113
107
  def board id:, &block
114
108
  config = BoardConfig.new(id: id, block: block, project_config: self)
109
+ config.run
115
110
  @board_configs << config
116
111
  end
117
112
 
@@ -128,6 +123,8 @@ class ProjectConfig
128
123
  # is set but before anything inside the project block is run. If only we had made file_prefix an attribute
129
124
  # on project, we wouldn't have this ugliness. 🤷‍♂️
130
125
  load_status_category_mappings
126
+ load_status_history
127
+ load_all_boards
131
128
 
132
129
  @file_prefix
133
130
  end
@@ -138,29 +135,28 @@ class ProjectConfig
138
135
  @file_prefix
139
136
  end
140
137
 
141
- def status_guesses
142
- statuses = {}
138
+ # Walk across all the issues and find any status with that name. Return a list of ids that match.
139
+ def find_ids_by_status_name_across_all_issues name
140
+ ids = Set.new
141
+
143
142
  issues.each do |issue|
144
143
  issue.changes.each do |change|
145
144
  next unless change.status?
146
145
 
147
- (statuses[change.value] ||= Set.new) << change.value_id
148
- (statuses[change.old_value] ||= Set.new) << change.old_value_id if change.old_value
146
+ ids << change.value_id.to_i if change.value == name
147
+ ids << change.old_value_id.to_i if change.old_value == name
149
148
  end
150
149
  end
151
- statuses
150
+ ids.to_a
152
151
  end
153
152
 
154
153
  def status_category_mapping status:, category:
155
- # Status might just be a name like "Review" or it might be a name:id pair like "Review:4"
156
- parse_status = ->(line) { line =~ /^(.+):(\d+)$/ ? [$1, $2.to_i] : [line, nil] }
157
-
158
- status, status_id = parse_status.call status
159
- category, category_id = parse_status.call category
154
+ status, status_id = possible_statuses.parse_name_id status
155
+ category, category_id = possible_statuses.parse_name_id category
160
156
 
161
157
  if status_id.nil?
162
- guesses = status_guesses[status]
163
- if guesses.nil?
158
+ guesses = find_ids_by_status_name_across_all_issues status
159
+ if guesses.empty?
164
160
  file_system.warning "For status_category_mapping status: #{status.inspect}, category: #{category.inspect}\n" \
165
161
  "Cannot guess status id for #{status.inspect} as no statuses found anywhere in the issues " \
166
162
  "histories with that name. Since we can't find it, you probably don't need this mapping anymore so we're " \
@@ -174,21 +170,31 @@ class ProjectConfig
174
170
  end
175
171
 
176
172
  status_id = guesses.first
177
- # TODO: This should be a deprecation instead but we can't currently assert against those
178
173
  file_system.log "status_category_mapping for #{status.inspect} has been mapped to id #{status_id}. " \
179
174
  "If that's incorrect then specify the status_id."
180
175
  end
181
176
 
182
- found_category = possible_statuses.find_category_by_name category
183
- found_category_id = found_category&.id # possible_statuses.find_category_id_by_name category
184
- if category_id && category_id != found_category_id
185
- raise "ID is incorrect for status category #{category.inspect}. Did you mean #{found_category_id}?"
177
+ possible_categories = possible_statuses.find_all_categories_by_name category
178
+ if possible_categories.empty?
179
+ all = possible_statuses.find_all_categories.join(', ')
180
+ raise "No status categories found for name #{category.inspect} in [#{all}]. " \
181
+ 'Either fix the name or add an ID.'
182
+ elsif possible_categories.size > 1
183
+ # Theoretically impossible and yet we've seen wierder things out of Jira so we're prepared.
184
+ raise "More than one status category found with the name #{category.inspect} in " \
185
+ "[#{possible_categories.join(', ')}]. Either fix the name or add an ID"
186
+ end
187
+
188
+ found_category = possible_categories.first
189
+
190
+ if category_id && category_id != found_category.id
191
+ raise "ID is incorrect for status category #{category.inspect}. Did you mean #{found_category.id}?"
186
192
  end
187
193
 
188
194
  add_possible_status(
189
195
  Status.new(
190
196
  name: status, id: status_id,
191
- category_name: category, category_id: found_category_id, category_key: found_category.key
197
+ category_name: category, category_id: found_category.id, category_key: found_category.key
192
198
  )
193
199
  )
194
200
  end
@@ -211,7 +217,14 @@ class ProjectConfig
211
217
  end
212
218
 
213
219
  # We're registering one we already knew about. This may happen if someone specified a status_category_mapping
214
- # for something that was already returned from jira. This isn't problem so just ignore it.
220
+ # for something that was already returned from jira.
221
+ #
222
+ # You may be looking at this code and thinking of changing it to spit out a warning since obviously
223
+ # the user has made a mistake. Unfortunately, they may not have made any mistake. Due to inconsistency with the
224
+ # status API, it's possible for two different people to make a request to the same API at the same time and get
225
+ # back a different set of statuses. So that means that some people might need more status/categories mappings than
226
+ # other people for exactly the same instance. See this article for more on that API:
227
+ # https://agiletechnicalexcellence.com/2024/04/12/jira-api-statuses.html
215
228
  existing_status
216
229
  end
217
230
 
@@ -222,7 +235,6 @@ class ProjectConfig
222
235
  board_id = $1.to_i
223
236
  load_board board_id: board_id, filename: "#{@target_path}#{file}"
224
237
  end
225
- raise "No boards found for #{get_file_prefix.inspect} in #{@target_path.inspect}" if @all_boards.empty?
226
238
  end
227
239
 
228
240
  def load_board board_id:, filename:
@@ -235,9 +247,7 @@ class ProjectConfig
235
247
 
236
248
  def load_status_category_mappings
237
249
  filename = File.join @target_path, "#{get_file_prefix}_statuses.json"
238
-
239
- # We may not always have this file. Load it if we can.
240
- return unless File.exist? filename
250
+ return unless file_system.file_exist? filename
241
251
 
242
252
  file_system
243
253
  .load_json(filename)
@@ -245,6 +255,22 @@ class ProjectConfig
245
255
  .each { |status| add_possible_status status }
246
256
  end
247
257
 
258
+ def load_status_history
259
+ filename = File.join @target_path, "#{get_file_prefix}_status_history.json"
260
+ return unless file_system.file_exist? filename
261
+
262
+ file_system.log ' Loading historical statuses', also_write_to_stderr: true
263
+ file_system
264
+ .load_json(filename)
265
+ .map { |snippet| Status.from_raw(snippet) }
266
+ .each { |status| possible_statuses.historical_status_mappings[status.to_s] = status.category }
267
+
268
+ possible_statuses
269
+ rescue => e # rubocop:disable Style/RescueStandardError
270
+ file_system.warning "Unable to load status history due to #{e.message.inspect}. If this is because of a " \
271
+ 'malformed file then it should be fixed on the next download.'
272
+ end
273
+
248
274
  def load_sprints
249
275
  file_system.foreach(@target_path) do |file|
250
276
  next unless file =~ /^#{get_file_prefix}_board_(\d+)_sprints_\d+.json$/
@@ -478,21 +504,52 @@ class ProjectConfig
478
504
  Anonymizer.new(project_config: self).run
479
505
  end
480
506
 
481
- def discard_changes_before_hook issues_cutoff_times
482
- issues_cutoff_times.each do |issue, cutoff_time|
483
- days = (cutoff_time.to_date - issue.changes.first.time.to_date).to_i + 1
484
- message = "#{issue.key}(#{issue.type}) discarding #{days} "
485
- if days == 1
486
- message << "day of data on #{cutoff_time.to_date}"
487
- else
488
- message << "days of data from #{issue.changes.first.time.to_date} to #{cutoff_time.to_date}"
507
+ def file_system
508
+ @exporter.file_system
509
+ end
510
+
511
+ def discard_changes_before status_becomes: nil, &block
512
+ if status_becomes
513
+ status_becomes = [status_becomes] unless status_becomes.is_a? Array
514
+
515
+ block = lambda do |issue|
516
+ trigger_statuses = status_becomes.collect do |status_name|
517
+ if status_name == :backlog
518
+ issue.board.backlog_statuses
519
+ else
520
+ possible_statuses.find_all_by_name status_name
521
+ end
522
+ end.flatten
523
+
524
+ next if trigger_statuses.empty?
525
+
526
+ trigger_status_ids = trigger_statuses.collect(&:id)
527
+
528
+ time = nil
529
+ issue.status_changes.each do |change|
530
+ time = change.time if trigger_status_ids.include?(change.value_id) # && change.artificial? == false
531
+ end
532
+ time
489
533
  end
490
- exporter.file_system.log message
491
534
  end
492
- exporter.file_system.log "Discarded data from #{issues_cutoff_times.count} issues out of a total #{issues.size}"
493
- end
494
535
 
495
- def file_system
496
- @exporter.file_system
536
+ issues.each do |issue|
537
+ cutoff_time = block.call(issue)
538
+ next if cutoff_time.nil?
539
+
540
+ original_start_time = issue.board.cycletime.started_stopped_times(issue).first
541
+ next if original_start_time.nil?
542
+
543
+ issue.discard_changes_before cutoff_time
544
+
545
+ next unless cutoff_time
546
+ next if original_start_time > cutoff_time # ie the cutoff would have made no difference.
547
+
548
+ (@discarded_changes_data ||= []) << {
549
+ cutoff_time: cutoff_time,
550
+ original_start_time: original_start_time,
551
+ issue: issue
552
+ }
553
+ end
497
554
  end
498
555
  end
@@ -3,7 +3,6 @@
3
3
  require 'jirametrics/value_equality'
4
4
 
5
5
  class Status
6
- include ValueEquality
7
6
  attr_reader :id, :project_id, :category
8
7
  attr_accessor :name
9
8
 
@@ -20,6 +19,17 @@ class Status
20
19
  "#{name.inspect}:#{id.inspect}"
21
20
  end
22
21
 
22
+ def <=> other
23
+ id <=> other.id
24
+ end
25
+
26
+ def == other
27
+ id == other.id
28
+ end
29
+
30
+ def eql?(other) = id.eql?(other.id)
31
+ def hash = id.hash
32
+
23
33
  def new? = (@key == 'new')
24
34
  def indeterminate? = (@key == 'indeterminate')
25
35
  def done? = (@key == 'done')
@@ -74,9 +84,21 @@ class Status
74
84
  end
75
85
 
76
86
  def == other
87
+ return false unless other.is_a? Status
88
+
77
89
  @id == other.id && @name == other.name && @category.id == other.category.id && @category.name == other.category.name
78
90
  end
79
91
 
92
+ def eql?(other)
93
+ self == other
94
+ end
95
+
96
+ def <=> other
97
+ result = @name.casecmp(other.name)
98
+ result = @id <=> other.id if result.zero?
99
+ result
100
+ end
101
+
80
102
  def inspect
81
103
  result = []
82
104
  result << "Status(name: #{@name.inspect}"
@@ -4,105 +4,106 @@ class StatusNotFoundError < StandardError
4
4
  end
5
5
 
6
6
  class StatusCollection
7
+ attr_reader :historical_status_mappings
8
+
7
9
  def initialize
8
10
  @list = []
11
+ @historical_status_mappings = {} # 'name:id' => category
9
12
  end
10
13
 
11
- def filter_status_names category_name:, including: nil, excluding: nil
12
- including = expand_statuses including
13
- excluding = expand_statuses excluding
14
+ # Return the status matching this id or nil if it can't be found.
15
+ def find_by_id id
16
+ @list.find { |status| status.id == id }
17
+ end
14
18
 
15
- @list.filter_map do |status|
16
- keep = status.category.name == category_name ||
17
- including.any? { |s| s.name == status.name }
18
- keep = false if excluding.any? { |s| s.name == status.name }
19
+ def find_all_by_name identifier
20
+ name, id = parse_name_id identifier
19
21
 
20
- status.name if keep
21
- end
22
- end
22
+ if id
23
+ status = find_by_id id
24
+ return [] if status.nil?
23
25
 
24
- def expand_statuses names_or_ids
25
- result = []
26
- return result if names_or_ids.nil?
27
-
28
- names_or_ids = [names_or_ids] unless names_or_ids.is_a? Array
29
-
30
- names_or_ids.each do |name_or_id|
31
- status = @list.find { |s| s.name == name_or_id || s.id == name_or_id }
32
- if status.nil?
33
- if block_given?
34
- yield name_or_id
35
- next
36
- else
37
- all_status_names = @list.collect { |s| "#{s.name.inspect}:#{s.id.inspect}" }.uniq.sort.join(', ')
38
- raise StatusNotFoundError, "Status not found: \"#{name_or_id}\". Possible statuses are: #{all_status_names}"
39
- end
26
+ if name && status.name != name
27
+ raise "Specified status ID of #{id} does not match specified name #{name.inspect}. " \
28
+ "You might have meant one of these: #{self}."
40
29
  end
41
-
42
- result << status
30
+ [status]
31
+ else
32
+ @list.select { |status| status.name == name }
43
33
  end
44
- result
45
34
  end
46
35
 
47
- def todo including: nil, excluding: nil
48
- filter_status_names category_name: 'To Do', including: including, excluding: excluding
36
+ def find_all_categories
37
+ @list
38
+ .collect(&:category)
39
+ .uniq
40
+ .sort_by(&:id)
49
41
  end
50
42
 
51
- def in_progress including: nil, excluding: nil
52
- filter_status_names category_name: 'In Progress', including: including, excluding: excluding
43
+ def parse_name_id name
44
+ # Names could arrive in one of the following formats: "Done:3", "3", "Done"
45
+ if name =~ /^(.*):(\d+)$/
46
+ [$1, $2.to_i]
47
+ elsif name.match?(/^\d+$/)
48
+ [nil, name.to_i]
49
+ else
50
+ [name, nil]
51
+ end
53
52
  end
54
53
 
55
- def done including: nil, excluding: nil
56
- filter_status_names category_name: 'Done', including: including, excluding: excluding
57
- end
54
+ def find_all_categories_by_name identifier
55
+ key = nil
56
+ id = nil
58
57
 
59
- # Return the status matching this id or nil if it can't be found.
60
- def find_by_id id
61
- @list.find { |status| status.id == id }
58
+ if identifier.is_a? Symbol
59
+ key = identifier.to_s
60
+ else
61
+ name, id = parse_name_id identifier
62
+ end
63
+
64
+ find_all_categories.select { |c| c.id == id || c.name == name || c.key == key }
62
65
  end
63
66
 
64
- def find_all_by_name name
65
- @list.select { |status| status.name == name }
67
+ def collect(&block) = @list.collect(&block)
68
+ def find(&block) = @list.find(&block)
69
+ def each(&block) = @list.each(&block)
70
+ def select(&block) = @list.select(&block)
71
+ def <<(arg) = @list << arg
72
+ def empty? = @list.empty?
73
+ def clear = @list.clear
74
+ def delete(object) = @list.delete(object)
75
+
76
+ def to_s
77
+ "[#{@list.sort.join(', ')}]"
66
78
  end
67
79
 
68
- def find_category_by_name name
69
- category = @list.find { |status| status.category.name == name }&.category
70
- unless category
71
- set = Set.new
72
- @list.each do |status|
73
- set << status.category.to_s
74
- end
75
- raise "Unable to find status category #{name.inspect} in [#{set.to_a.sort.join(', ')}]"
76
- end
77
- category
80
+ def inspect
81
+ "StatusCollection#{self}"
78
82
  end
79
83
 
80
- # This is used to create a status that was found in the history but has since been deleted.
81
84
  def fabricate_status_for id:, name:
82
- first_in_progress_status = @list.find { |s| s.category.indeterminate? }
83
- raise "Can't find even one in-progress status in [#{set.to_a.sort.join(', ')}]" unless first_in_progress_status
85
+ category = @historical_status_mappings["#{name.inspect}:#{id.inspect}"]
86
+ category = in_progress_category if category.nil?
84
87
 
85
88
  status = Status.new(
86
89
  name: name,
87
90
  id: id,
88
- category_name: first_in_progress_status.category.name,
89
- category_id: first_in_progress_status.category.id,
90
- category_key: first_in_progress_status.category.key
91
+ category_name: category.name,
92
+ category_id: category.id,
93
+ category_key: category.key,
94
+ artificial: true
91
95
  )
92
- self << status
96
+ @list << status
93
97
  status
94
98
  end
95
99
 
96
- def collect(&block) = @list.collect(&block)
97
- def find(&block) = @list.find(&block)
98
- def each(&block) = @list.each(&block)
99
- def select(&block) = @list.select(&block)
100
- def <<(arg) = @list << arg
101
- def empty? = @list.empty?
102
- def clear = @list.clear
103
- def delete(object) = @list.delete(object)
100
+ private
104
101
 
105
- def inspect
106
- "StatusCollection(#{@list.join(', ')})"
102
+ # Return the in-progress category or raise an error if we can't find one.
103
+ def in_progress_category
104
+ first_in_progress_status = find { |s| s.category.indeterminate? }
105
+ raise "Can't find even one in-progress status in #{self}" unless first_in_progress_status
106
+
107
+ first_in_progress_status.category
107
108
  end
108
109
  end
@@ -9,9 +9,9 @@ module ValueEquality
9
9
  names = object.instance_variables
10
10
  if object.respond_to? :value_equality_ignored_variables
11
11
  ignored_variables = object.value_equality_ignored_variables
12
- names.reject! { |n| ignored_variables.include? n }
12
+ names.reject! { |n| ignored_variables.include? n.to_sym }
13
13
  end
14
- names.map { |variable| instance_variable_get variable }
14
+ names.map { |variable| object.instance_variable_get variable }
15
15
  end
16
16
 
17
17
  code.call(self) == code.call(other)
data/lib/jirametrics.rb CHANGED
@@ -68,7 +68,6 @@ class JiraMetrics < Thor
68
68
  require 'jirametrics/grouping_rules'
69
69
  require 'jirametrics/daily_wip_chart'
70
70
  require 'jirametrics/groupable_issue_chart'
71
- require 'jirametrics/discard_changes_before'
72
71
  require 'jirametrics/css_variable'
73
72
 
74
73
  require 'jirametrics/aggregate_config'