jirametrics 2.6 → 2.12pre9

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jirametrics/aggregate_config.rb +9 -4
  3. data/lib/jirametrics/aging_work_bar_chart.rb +13 -11
  4. data/lib/jirametrics/aging_work_in_progress_chart.rb +105 -41
  5. data/lib/jirametrics/aging_work_table.rb +54 -7
  6. data/lib/jirametrics/blocked_stalled_change.rb +1 -1
  7. data/lib/jirametrics/board.rb +47 -13
  8. data/lib/jirametrics/board_config.rb +7 -2
  9. data/lib/jirametrics/board_movement_calculator.rb +155 -0
  10. data/lib/jirametrics/change_item.rb +19 -8
  11. data/lib/jirametrics/chart_base.rb +63 -27
  12. data/lib/jirametrics/css_variable.rb +1 -1
  13. data/lib/jirametrics/cycletime_config.rb +59 -8
  14. data/lib/jirametrics/cycletime_histogram.rb +68 -3
  15. data/lib/jirametrics/cycletime_scatterplot.rb +3 -6
  16. data/lib/jirametrics/daily_wip_by_age_chart.rb +2 -4
  17. data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +2 -2
  18. data/lib/jirametrics/daily_wip_by_parent_chart.rb +0 -4
  19. data/lib/jirametrics/daily_wip_chart.rb +7 -9
  20. data/lib/jirametrics/data_quality_report.rb +219 -41
  21. data/lib/jirametrics/dependency_chart.rb +3 -4
  22. data/lib/jirametrics/download_config.rb +2 -2
  23. data/lib/jirametrics/downloader.rb +57 -37
  24. data/lib/jirametrics/estimate_accuracy_chart.rb +35 -12
  25. data/lib/jirametrics/estimation_configuration.rb +25 -0
  26. data/lib/jirametrics/examples/aggregated_project.rb +3 -6
  27. data/lib/jirametrics/examples/standard_project.rb +14 -13
  28. data/lib/jirametrics/expedited_chart.rb +7 -8
  29. data/lib/jirametrics/exporter.rb +28 -13
  30. data/lib/jirametrics/file_config.rb +23 -6
  31. data/lib/jirametrics/file_system.rb +41 -4
  32. data/lib/jirametrics/flow_efficiency_scatterplot.rb +111 -0
  33. data/lib/jirametrics/groupable_issue_chart.rb +1 -3
  34. data/lib/jirametrics/html/aging_work_bar_chart.erb +3 -12
  35. data/lib/jirametrics/html/aging_work_in_progress_chart.erb +22 -5
  36. data/lib/jirametrics/html/aging_work_table.erb +6 -4
  37. data/lib/jirametrics/html/cycletime_histogram.erb +74 -0
  38. data/lib/jirametrics/html/cycletime_scatterplot.erb +1 -10
  39. data/lib/jirametrics/html/daily_wip_chart.erb +1 -10
  40. data/lib/jirametrics/html/expedited_chart.erb +1 -10
  41. data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +85 -0
  42. data/lib/jirametrics/html/hierarchy_table.erb +1 -1
  43. data/lib/jirametrics/html/index.css +28 -5
  44. data/lib/jirametrics/html/index.erb +8 -4
  45. data/lib/jirametrics/html/sprint_burndown.erb +1 -10
  46. data/lib/jirametrics/html/throughput_chart.erb +1 -10
  47. data/lib/jirametrics/html_report_config.rb +33 -23
  48. data/lib/jirametrics/issue.rb +170 -54
  49. data/lib/jirametrics/jira_gateway.rb +16 -3
  50. data/lib/jirametrics/project_config.rb +251 -135
  51. data/lib/jirametrics/self_or_issue_dispatcher.rb +2 -0
  52. data/lib/jirametrics/sprint_burndown.rb +38 -36
  53. data/lib/jirametrics/sprint_issue_change_data.rb +3 -3
  54. data/lib/jirametrics/status.rb +81 -26
  55. data/lib/jirametrics/status_collection.rb +77 -39
  56. data/lib/jirametrics/throughput_chart.rb +1 -1
  57. data/lib/jirametrics/value_equality.rb +2 -2
  58. data/lib/jirametrics.rb +23 -6
  59. metadata +11 -13
  60. data/lib/jirametrics/discard_changes_before.rb +0 -37
  61. data/lib/jirametrics/html/data_quality_report.erb +0 -126
@@ -18,7 +18,7 @@ class SprintBurndown < ChartBase
18
18
  attr_accessor :board_id
19
19
 
20
20
  def initialize
21
- super()
21
+ super
22
22
 
23
23
  @summary_stats = {}
24
24
  header_text 'Sprint burndown'
@@ -121,12 +121,14 @@ class SprintBurndown < ChartBase
121
121
 
122
122
  # select all the changes that are relevant for the sprint. If this issue never appears in this sprint then return [].
123
123
  def changes_for_one_issue issue:, sprint:
124
- story_points = 0.0
124
+ estimate = 0.0
125
125
  ever_in_sprint = false
126
126
  currently_in_sprint = false
127
127
  change_data = []
128
128
 
129
- issue_completed_time = issue.board.cycletime.stopped_time(issue)
129
+ estimate_display_name = current_board.estimation_configuration.display_name
130
+
131
+ issue_completed_time = issue.board.cycletime.started_stopped_times(issue).last
130
132
  completed_has_been_tracked = false
131
133
 
132
134
  issue.changes.each do |change|
@@ -140,26 +142,26 @@ class SprintBurndown < ChartBase
140
142
  if currently_in_sprint == false && in_change_item
141
143
  action = :enter_sprint
142
144
  ever_in_sprint = true
143
- value = story_points
145
+ value = estimate
144
146
  elsif currently_in_sprint && in_change_item == false
145
147
  action = :leave_sprint
146
- value = -story_points
148
+ value = -estimate
147
149
  end
148
150
  currently_in_sprint = in_change_item
149
- elsif change.story_points? && (issue_completed_time.nil? || change.time < issue_completed_time)
151
+ elsif change.field == estimate_display_name && (issue_completed_time.nil? || change.time < issue_completed_time)
150
152
  action = :story_points
151
- story_points = change.value.to_f
152
- value = story_points - change.old_value.to_f
153
+ estimate = change.value.to_f
154
+ value = estimate - change.old_value.to_f
153
155
  elsif completed_has_been_tracked == false && change.time == issue_completed_time
154
156
  completed_has_been_tracked = true
155
157
  action = :issue_stopped
156
- value = -story_points
158
+ value = -estimate
157
159
  end
158
160
 
159
161
  next unless action
160
162
 
161
163
  change_data << SprintIssueChangeData.new(
162
- time: change.time, issue: issue, action: action, value: value, story_points: story_points
164
+ time: change.time, issue: issue, action: action, value: value, estimate: estimate
163
165
  )
164
166
  end
165
167
 
@@ -172,11 +174,11 @@ class SprintBurndown < ChartBase
172
174
  change_item.raw['to'].split(/\s*,\s*/).any? { |id| id.to_i == sprint.id }
173
175
  end
174
176
 
175
- def data_set_by_story_points sprint:, change_data_for_sprint:
177
+ def data_set_by_story_points sprint:, change_data_for_sprint: # rubocop:disable Metrics/CyclomaticComplexity
176
178
  summary_stats = SprintSummaryStats.new
177
179
  summary_stats.completed = 0.0
178
180
 
179
- story_points = 0.0
181
+ estimate = 0.0
180
182
  start_data_written = false
181
183
  data_set = []
182
184
 
@@ -185,11 +187,11 @@ class SprintBurndown < ChartBase
185
187
  change_data_for_sprint.each do |change_data|
186
188
  if start_data_written == false && change_data.time >= sprint.start_time
187
189
  data_set << {
188
- y: story_points,
190
+ y: estimate,
189
191
  x: chart_format(sprint.start_time),
190
- title: "Sprint started with #{story_points} points"
192
+ title: "Sprint started with #{estimate} points"
191
193
  }
192
- summary_stats.started = story_points
194
+ summary_stats.started = estimate
193
195
  start_data_written = true
194
196
  end
195
197
 
@@ -198,12 +200,12 @@ class SprintBurndown < ChartBase
198
200
  case change_data.action
199
201
  when :enter_sprint
200
202
  issues_currently_in_sprint << change_data.issue.key
201
- story_points += change_data.story_points
203
+ estimate += change_data.estimate
202
204
  when :leave_sprint
203
205
  issues_currently_in_sprint.delete change_data.issue.key
204
- story_points -= change_data.story_points
206
+ estimate -= change_data.estimate
205
207
  when :story_points
206
- story_points += change_data.value if issues_currently_in_sprint.include? change_data.issue.key
208
+ estimate += change_data.value if issues_currently_in_sprint.include? change_data.issue.key
207
209
  end
208
210
 
209
211
  next unless change_data.time >= sprint.start_time
@@ -213,26 +215,26 @@ class SprintBurndown < ChartBase
213
215
  when :story_points
214
216
  next unless issues_currently_in_sprint.include? change_data.issue.key
215
217
 
216
- old_story_points = change_data.story_points - change_data.value
217
- message = "Story points changed from #{old_story_points} points to #{change_data.story_points} points"
218
+ old_estimate = change_data.estimate - change_data.value
219
+ message = "Story points changed from #{old_estimate} points to #{change_data.estimate} points"
218
220
  summary_stats.points_values_changed = true
219
221
  when :enter_sprint
220
- message = "Added to sprint with #{change_data.story_points || 'no'} points"
221
- summary_stats.added += change_data.story_points
222
+ message = "Added to sprint with #{change_data.estimate || 'no'} points"
223
+ summary_stats.added += change_data.estimate
222
224
  when :issue_stopped
223
- story_points -= change_data.story_points
224
- message = "Completed with #{change_data.story_points || 'no'} points"
225
+ estimate -= change_data.estimate
226
+ message = "Completed with #{change_data.estimate || 'no'} points"
225
227
  issues_currently_in_sprint.delete change_data.issue.key
226
- summary_stats.completed += change_data.story_points
228
+ summary_stats.completed += change_data.estimate
227
229
  when :leave_sprint
228
- message = "Removed from sprint with #{change_data.story_points || 'no'} points"
229
- summary_stats.removed += change_data.story_points
230
+ message = "Removed from sprint with #{change_data.estimate || 'no'} points"
231
+ summary_stats.removed += change_data.estimate
230
232
  else
231
233
  raise "Unexpected action: #{change_data.action}"
232
234
  end
233
235
 
234
236
  data_set << {
235
- y: story_points,
237
+ y: estimate,
236
238
  x: chart_format(change_data.time),
237
239
  title: "#{change_data.issue.key} #{message}"
238
240
  }
@@ -241,27 +243,27 @@ class SprintBurndown < ChartBase
241
243
  unless start_data_written
242
244
  # There was nothing that triggered us to write the sprint started block so do it now.
243
245
  data_set << {
244
- y: story_points,
246
+ y: estimate,
245
247
  x: chart_format(sprint.start_time),
246
- title: "Sprint started with #{story_points} points"
248
+ title: "Sprint started with #{estimate} points"
247
249
  }
248
- summary_stats.started = story_points
250
+ summary_stats.started = estimate
249
251
  end
250
252
 
251
253
  if sprint.completed_time
252
254
  data_set << {
253
- y: story_points,
255
+ y: estimate,
254
256
  x: chart_format(sprint.completed_time),
255
- title: "Sprint ended with #{story_points} points unfinished"
257
+ title: "Sprint ended with #{estimate} points unfinished"
256
258
  }
257
- summary_stats.remaining = story_points
259
+ summary_stats.remaining = estimate
258
260
  end
259
261
 
260
262
  unless sprint.completed_at?(time_range.end)
261
263
  data_set << {
262
- y: story_points,
264
+ y: estimate,
263
265
  x: chart_format(time_range.end),
264
- title: "Sprint still active. #{story_points} points still in progress."
266
+ title: "Sprint still active. #{estimate} points still in progress."
265
267
  }
266
268
  end
267
269
 
@@ -4,14 +4,14 @@ require 'jirametrics/value_equality'
4
4
 
5
5
  class SprintIssueChangeData
6
6
  include ValueEquality
7
- attr_reader :time, :action, :value, :issue, :story_points
7
+ attr_reader :time, :action, :value, :issue, :estimate
8
8
 
9
- def initialize time:, action:, value:, issue:, story_points:
9
+ def initialize time:, action:, value:, issue:, estimate:
10
10
  @time = time
11
11
  @action = action
12
12
  @value = value
13
13
  @issue = issue
14
- @story_points = story_points
14
+ @estimate = estimate
15
15
  end
16
16
 
17
17
  def inspect
@@ -3,30 +3,65 @@
3
3
  require 'jirametrics/value_equality'
4
4
 
5
5
  class Status
6
- include ValueEquality
7
- attr_reader :id, :category_name, :category_id, :project_id
6
+ attr_reader :id, :project_id, :category
8
7
  attr_accessor :name
9
8
 
10
- def initialize name: nil, id: nil, category_name: nil, category_id: nil, project_id: nil, raw: nil
11
- @name = name
12
- @id = id
13
- @category_name = category_name
14
- @category_id = category_id
15
- @project_id = project_id
9
+ class Category
10
+ attr_reader :id, :name, :key
11
+
12
+ def initialize id:, name:, key:
13
+ @id = id
14
+ @name = name
15
+ @key = key
16
+ end
16
17
 
17
- return unless raw
18
+ def to_s
19
+ "#{name.inspect}:#{id.inspect}"
20
+ end
18
21
 
19
- @raw = raw
20
- @name = raw['name']
21
- @id = raw['id'].to_i
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
+
33
+ def new? = (@key == 'new')
34
+ def indeterminate? = (@key == 'indeterminate')
35
+ def done? = (@key == 'done')
36
+ end
37
+
38
+ def self.from_raw raw
39
+ raise "raw cannot be nil" if raw.nil?
22
40
 
23
41
  category_config = raw['statusCategory']
24
- @category_name = category_config['name']
25
- @category_id = category_config['id'].to_i
42
+ raise "statusCategory can't be nil in #{category_config.inspect}" if category_config.nil?
43
+
44
+ Status.new(
45
+ name: raw['name'],
46
+ id: raw['id'].to_i,
47
+ category_name: category_config['name'],
48
+ category_id: category_config['id'].to_i,
49
+ category_key: category_config['key'],
50
+ project_id: raw['scope']&.[]('project')&.[]('id'),
51
+ artificial: false
52
+ )
53
+ end
54
+
55
+ def initialize name:, id:, category_name:, category_id:, category_key:, project_id: nil, artificial: true
56
+ # These checks are needed because nils used to be possible and now they aren't.
57
+ raise 'id cannot be nil' if id.nil?
58
+ raise 'category_id cannot be nil' if category_id.nil?
26
59
 
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')
60
+ @name = name
61
+ @id = id
62
+ @category = Category.new id: category_id, name: category_name, key: category_key
63
+ @project_id = project_id
64
+ @artificial = artificial
30
65
  end
31
66
 
32
67
  def project_scoped?
@@ -38,18 +73,38 @@ class Status
38
73
  end
39
74
 
40
75
  def to_s
41
- result = +"Status(name=#{@name.inspect}," \
42
- " id=#{@id.inspect}," \
43
- " category_name=#{@category_name.inspect}," \
44
- " category_id=#{@category_id.inspect}," \
45
- " project_id=#{@project_id}"
46
- result << ' artificial' if artificial?
47
- result << ')'
48
- result
76
+ "#{name.inspect}:#{id.inspect}"
49
77
  end
50
78
 
51
79
  def artificial?
52
- @raw.nil?
80
+ @artificial
81
+ end
82
+
83
+ def == other
84
+ return false unless other.is_a? Status
85
+
86
+ @id == other.id && @name == other.name && @category.id == other.category.id && @category.name == other.category.name
87
+ end
88
+
89
+ def eql?(other)
90
+ self == other
91
+ end
92
+
93
+ def <=> other
94
+ result = @name.casecmp(other.name)
95
+ result = @id <=> other.id if result.zero?
96
+ result
97
+ end
98
+
99
+ def inspect
100
+ result = []
101
+ result << "Status(name: #{@name.inspect}"
102
+ result << "id: #{@id.inspect}"
103
+ result << "project_id: #{@project_id}" if @project_id
104
+ category = self.category
105
+ result << "category: {name:#{category.name.inspect}, id: #{category.id.inspect}, key: #{category.key.inspect}}"
106
+ result << 'artificial' if artificial?
107
+ result.join(', ') << ')'
53
108
  end
54
109
 
55
110
  def value_equality_ignored_variables
@@ -4,68 +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
- def find_by_name name
60
- find { |status| status.name == name }
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 }
61
65
  end
62
66
 
63
- def find(&block)= @list.find(&block)
64
67
  def collect(&block) = @list.collect(&block)
68
+ def find(&block) = @list.find(&block)
65
69
  def each(&block) = @list.each(&block)
66
70
  def select(&block) = @list.select(&block)
67
71
  def <<(arg) = @list << arg
68
72
  def empty? = @list.empty?
69
73
  def clear = @list.clear
70
74
  def delete(object) = @list.delete(object)
75
+
76
+ def to_s
77
+ "[#{@list.sort.join(', ')}]"
78
+ end
79
+
80
+ def inspect
81
+ "StatusCollection#{self}"
82
+ end
83
+
84
+ def fabricate_status_for id:, name:
85
+ category = @historical_status_mappings["#{name.inspect}:#{id.inspect}"]
86
+ category = in_progress_category if category.nil?
87
+
88
+ status = Status.new(
89
+ name: name,
90
+ id: id,
91
+ category_name: category.name,
92
+ category_id: category.id,
93
+ category_key: category.key,
94
+ artificial: true
95
+ )
96
+ @list << status
97
+ status
98
+ end
99
+
100
+ private
101
+
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
108
+ end
71
109
  end
@@ -82,7 +82,7 @@ class ThroughputChart < ChartBase
82
82
  def throughput_dataset periods:, completed_issues:
83
83
  periods.collect do |period|
84
84
  closed_issues = completed_issues.filter_map do |issue|
85
- stop_date = issue.board.cycletime.stopped_time(issue)&.to_date
85
+ stop_date = issue.board.cycletime.started_stopped_dates(issue).last
86
86
  [stop_date, issue] if stop_date && period.include?(stop_date)
87
87
  end
88
88
 
@@ -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
@@ -3,9 +3,20 @@
3
3
  require 'thor'
4
4
 
5
5
  class JiraMetrics < Thor
6
+ def self.exit_on_failure?
7
+ true
8
+ end
9
+
10
+ map %w[--version -v] => :__print_version
11
+
12
+ desc '--version, -v', 'print the version'
13
+ def __print_version
14
+ puts Gem.loaded_specs['jirametrics'].version
15
+ end
16
+
6
17
  option :config
7
18
  option :name
8
- desc 'export only', "Export data into either reports or CSV's as per the configuration"
19
+ desc 'export', "Export data into either reports or CSV's as per the configuration"
9
20
  def export
10
21
  load_config options[:config]
11
22
  Exporter.instance.export(name_filter: options[:name] || '*')
@@ -13,7 +24,7 @@ class JiraMetrics < Thor
13
24
 
14
25
  option :config
15
26
  option :name
16
- desc 'download only', 'Download data from Jira'
27
+ desc 'download', 'Download data from Jira'
17
28
  def download
18
29
  load_config options[:config]
19
30
  Exporter.instance.download(name_filter: options[:name] || '*')
@@ -21,7 +32,7 @@ class JiraMetrics < Thor
21
32
 
22
33
  option :config
23
34
  option :name
24
- desc 'download and export', 'Same as running download, followed by export'
35
+ desc 'go', 'Same as running download, followed by export'
25
36
  def go
26
37
  load_config options[:config]
27
38
  Exporter.instance.download(name_filter: options[:name] || '*')
@@ -30,6 +41,13 @@ class JiraMetrics < Thor
30
41
  Exporter.instance.export(name_filter: options[:name] || '*')
31
42
  end
32
43
 
44
+ option :config
45
+ desc 'info', 'Dump information about one issue'
46
+ def info keys
47
+ load_config options[:config]
48
+ Exporter.instance.info(keys, name_filter: options[:name] || '*')
49
+ end
50
+
33
51
  private
34
52
 
35
53
  def load_config config_file
@@ -50,7 +68,6 @@ class JiraMetrics < Thor
50
68
  require 'jirametrics/grouping_rules'
51
69
  require 'jirametrics/daily_wip_chart'
52
70
  require 'jirametrics/groupable_issue_chart'
53
- require 'jirametrics/discard_changes_before'
54
71
  require 'jirametrics/css_variable'
55
72
 
56
73
  require 'jirametrics/aggregate_config'
@@ -69,6 +86,7 @@ class JiraMetrics < Thor
69
86
  require 'jirametrics/daily_wip_by_parent_chart'
70
87
  require 'jirametrics/aging_work_in_progress_chart'
71
88
  require 'jirametrics/cycletime_scatterplot'
89
+ require 'jirametrics/flow_efficiency_scatterplot'
72
90
  require 'jirametrics/sprint_issue_change_data'
73
91
  require 'jirametrics/cycletime_histogram'
74
92
  require 'jirametrics/daily_wip_by_blocked_stalled_chart'
@@ -94,9 +112,8 @@ class JiraMetrics < Thor
94
112
  require 'jirametrics/download_config'
95
113
  require 'jirametrics/columns_config'
96
114
  require 'jirametrics/hierarchy_table'
115
+ require 'jirametrics/estimation_configuration'
97
116
  require 'jirametrics/board'
98
117
  load config_file
99
118
  end
100
-
101
- # Dir.foreach('lib/jirametrics') {|file| puts "require 'jirametrics/#{$1}'" if file =~ /^(.+)\.rb$/}
102
119
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jirametrics
3
3
  version: !ruby/object:Gem::Version
4
- version: '2.6'
4
+ version: 2.12pre9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Bowler
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-10-02 00:00:00.000000000 Z
10
+ date: 2025-04-16 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: random-word
@@ -52,8 +51,7 @@ dependencies:
52
51
  - - "~>"
53
52
  - !ruby/object:Gem::Version
54
53
  version: 1.2.2
55
- description: Tool to extract metrics from Jira and export to either a report or to
56
- CSV files
54
+ description: Extract metrics from Jira and export to either a report or to CSV files
57
55
  email: mbowler@gargoylesoftware.com
58
56
  executables:
59
57
  - jirametrics
@@ -71,6 +69,7 @@ files:
71
69
  - lib/jirametrics/board.rb
72
70
  - lib/jirametrics/board_column.rb
73
71
  - lib/jirametrics/board_config.rb
72
+ - lib/jirametrics/board_movement_calculator.rb
74
73
  - lib/jirametrics/change_item.rb
75
74
  - lib/jirametrics/chart_base.rb
76
75
  - lib/jirametrics/columns_config.rb
@@ -84,10 +83,10 @@ files:
84
83
  - lib/jirametrics/daily_wip_chart.rb
85
84
  - lib/jirametrics/data_quality_report.rb
86
85
  - lib/jirametrics/dependency_chart.rb
87
- - lib/jirametrics/discard_changes_before.rb
88
86
  - lib/jirametrics/download_config.rb
89
87
  - lib/jirametrics/downloader.rb
90
88
  - lib/jirametrics/estimate_accuracy_chart.rb
89
+ - lib/jirametrics/estimation_configuration.rb
91
90
  - lib/jirametrics/examples/aggregated_project.rb
92
91
  - lib/jirametrics/examples/standard_project.rb
93
92
  - lib/jirametrics/expedited_chart.rb
@@ -95,6 +94,7 @@ files:
95
94
  - lib/jirametrics/file_config.rb
96
95
  - lib/jirametrics/file_system.rb
97
96
  - lib/jirametrics/fix_version.rb
97
+ - lib/jirametrics/flow_efficiency_scatterplot.rb
98
98
  - lib/jirametrics/groupable_issue_chart.rb
99
99
  - lib/jirametrics/grouping_rules.rb
100
100
  - lib/jirametrics/hierarchy_table.rb
@@ -105,9 +105,9 @@ files:
105
105
  - lib/jirametrics/html/cycletime_histogram.erb
106
106
  - lib/jirametrics/html/cycletime_scatterplot.erb
107
107
  - lib/jirametrics/html/daily_wip_chart.erb
108
- - lib/jirametrics/html/data_quality_report.erb
109
108
  - lib/jirametrics/html/estimate_accuracy_chart.erb
110
109
  - lib/jirametrics/html/expedited_chart.erb
110
+ - lib/jirametrics/html/flow_efficiency_scatterplot.erb
111
111
  - lib/jirametrics/html/hierarchy_table.erb
112
112
  - lib/jirametrics/html/index.css
113
113
  - lib/jirametrics/html/index.erb
@@ -130,15 +130,14 @@ files:
130
130
  - lib/jirametrics/tree_organizer.rb
131
131
  - lib/jirametrics/trend_line_calculator.rb
132
132
  - lib/jirametrics/value_equality.rb
133
- homepage: https://github.com/mikebowler/jirametrics
133
+ homepage: https://jirametrics.org
134
134
  licenses:
135
135
  - Apache-2.0
136
136
  metadata:
137
137
  rubygems_mfa_required: 'true'
138
138
  bug_tracker_uri: https://github.com/mikebowler/jirametrics/issues
139
- changelog_uri: https://github.com/mikebowler/jirametrics/wiki/Changes
140
- documentation_uri: https://github.com/mikebowler/jirametrics/wiki
141
- post_install_message:
139
+ changelog_uri: https://jirametrics.org/changes
140
+ documentation_uri: https://jirametrics.org
142
141
  rdoc_options: []
143
142
  require_paths:
144
143
  - lib
@@ -153,8 +152,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
153
152
  - !ruby/object:Gem::Version
154
153
  version: '0'
155
154
  requirements: []
156
- rubygems_version: 3.5.18
157
- signing_key:
155
+ rubygems_version: 3.6.2
158
156
  specification_version: 4
159
157
  summary: Extract Jira metrics
160
158
  test_files: []