jirametrics 2.0 → 2.11

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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jirametrics/aggregate_config.rb +19 -26
  3. data/lib/jirametrics/aging_work_bar_chart.rb +79 -54
  4. data/lib/jirametrics/aging_work_in_progress_chart.rb +106 -40
  5. data/lib/jirametrics/aging_work_table.rb +78 -43
  6. data/lib/jirametrics/anonymizer.rb +6 -5
  7. data/lib/jirametrics/blocked_stalled_change.rb +24 -4
  8. data/lib/jirametrics/board.rb +44 -15
  9. data/lib/jirametrics/board_config.rb +8 -4
  10. data/lib/jirametrics/board_movement_calculator.rb +147 -0
  11. data/lib/jirametrics/change_item.rb +31 -10
  12. data/lib/jirametrics/chart_base.rb +102 -61
  13. data/lib/jirametrics/columns_config.rb +4 -0
  14. data/lib/jirametrics/css_variable.rb +33 -0
  15. data/lib/jirametrics/cycletime_config.rb +59 -8
  16. data/lib/jirametrics/cycletime_histogram.rb +69 -4
  17. data/lib/jirametrics/cycletime_scatterplot.rb +11 -15
  18. data/lib/jirametrics/daily_wip_by_age_chart.rb +44 -20
  19. data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +37 -35
  20. data/lib/jirametrics/daily_wip_by_parent_chart.rb +38 -0
  21. data/lib/jirametrics/daily_wip_chart.rb +61 -14
  22. data/lib/jirametrics/data_quality_report.rb +222 -41
  23. data/lib/jirametrics/dependency_chart.rb +54 -23
  24. data/lib/jirametrics/download_config.rb +12 -0
  25. data/lib/jirametrics/downloader.rb +76 -57
  26. data/lib/jirametrics/{story_point_accuracy_chart.rb → estimate_accuracy_chart.rb} +48 -33
  27. data/lib/jirametrics/examples/aggregated_project.rb +22 -39
  28. data/lib/jirametrics/examples/standard_project.rb +25 -49
  29. data/lib/jirametrics/expedited_chart.rb +28 -25
  30. data/lib/jirametrics/exporter.rb +59 -32
  31. data/lib/jirametrics/file_config.rb +34 -13
  32. data/lib/jirametrics/file_system.rb +48 -3
  33. data/lib/jirametrics/flow_efficiency_scatterplot.rb +111 -0
  34. data/lib/jirametrics/groupable_issue_chart.rb +2 -6
  35. data/lib/jirametrics/grouping_rules.rb +7 -1
  36. data/lib/jirametrics/hierarchy_table.rb +4 -4
  37. data/lib/jirametrics/html/aging_work_bar_chart.erb +13 -16
  38. data/lib/jirametrics/html/aging_work_in_progress_chart.erb +28 -5
  39. data/lib/jirametrics/html/aging_work_table.erb +19 -25
  40. data/lib/jirametrics/html/cycletime_histogram.erb +83 -3
  41. data/lib/jirametrics/html/cycletime_scatterplot.erb +9 -12
  42. data/lib/jirametrics/html/daily_wip_chart.erb +17 -13
  43. data/lib/jirametrics/html/{story_point_accuracy_chart.erb → estimate_accuracy_chart.erb} +9 -4
  44. data/lib/jirametrics/html/expedited_chart.erb +10 -13
  45. data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +85 -0
  46. data/lib/jirametrics/html/hierarchy_table.erb +2 -2
  47. data/lib/jirametrics/html/index.css +209 -0
  48. data/lib/jirametrics/html/index.erb +16 -39
  49. data/lib/jirametrics/html/sprint_burndown.erb +10 -14
  50. data/lib/jirametrics/html/throughput_chart.erb +10 -13
  51. data/lib/jirametrics/html_report_config.rb +108 -86
  52. data/lib/jirametrics/issue.rb +357 -96
  53. data/lib/jirametrics/jira_gateway.rb +29 -11
  54. data/lib/jirametrics/project_config.rb +256 -144
  55. data/lib/jirametrics/rules.rb +2 -2
  56. data/lib/jirametrics/self_or_issue_dispatcher.rb +2 -0
  57. data/lib/jirametrics/settings.json +10 -0
  58. data/lib/jirametrics/sprint_burndown.rb +24 -7
  59. data/lib/jirametrics/status.rb +84 -19
  60. data/lib/jirametrics/status_collection.rb +80 -39
  61. data/lib/jirametrics/throughput_chart.rb +12 -4
  62. data/lib/jirametrics/value_equality.rb +2 -2
  63. data/lib/jirametrics.rb +25 -7
  64. metadata +16 -17
  65. data/lib/jirametrics/discard_changes_before.rb +0 -37
  66. data/lib/jirametrics/experimental/generator.rb +0 -210
  67. data/lib/jirametrics/experimental/info.rb +0 -77
  68. data/lib/jirametrics/html/data_quality_report.erb +0 -126
@@ -18,11 +18,17 @@ 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'
25
- description_text ''
25
+ description_text <<-TEXT
26
+ <div class="p">
27
+ Burndowns for all sprints in this time period. The different colours are only to
28
+ differentiate one sprint from another as they may overlap time periods.
29
+ </div>
30
+ #{describe_non_working_days}
31
+ TEXT
26
32
  end
27
33
 
28
34
  def options= arg
@@ -55,9 +61,9 @@ class SprintBurndown < ChartBase
55
61
  end
56
62
 
57
63
  result = +''
58
- result << '<h1>Sprint Burndowns</h1>'
64
+ result << render_top_text(binding)
59
65
 
60
- possible_colours = %w[blue orange green red brown]
66
+ possible_colours = (1..5).collect { |i| CssVariable["--sprint-burndown-sprint-color-#{i}"] }
61
67
  charts_to_generate = []
62
68
  charts_to_generate << [:data_set_by_story_points, 'Story Points'] if @use_story_points
63
69
  charts_to_generate << [:data_set_by_story_counts, 'Story Count'] if @use_story_counts
@@ -65,7 +71,7 @@ class SprintBurndown < ChartBase
65
71
  @summary_stats.clear
66
72
  data_sets = []
67
73
  sprints.each_with_index do |sprint, index|
68
- color = possible_colours[index % 5]
74
+ color = possible_colours[index % possible_colours.size]
69
75
  label = sprint.name
70
76
  data = send(data_method, sprint: sprint, change_data_for_sprint: change_data_by_sprint[sprint])
71
77
  data_sets << {
@@ -102,6 +108,17 @@ class SprintBurndown < ChartBase
102
108
  result
103
109
  end
104
110
 
111
+ def sprints_in_time_range board
112
+ board.sprints.select do |sprint|
113
+ sprint_end_time = sprint.completed_time || sprint.end_time
114
+ sprint_start_time = sprint.start_time
115
+ next false if sprint_start_time.nil?
116
+
117
+ time_range.include?(sprint_start_time) || time_range.include?(sprint_end_time) ||
118
+ (sprint_start_time < time_range.begin && sprint_end_time > time_range.end)
119
+ end || []
120
+ end
121
+
105
122
  # select all the changes that are relevant for the sprint. If this issue never appears in this sprint then return [].
106
123
  def changes_for_one_issue issue:, sprint:
107
124
  story_points = 0.0
@@ -109,7 +126,7 @@ class SprintBurndown < ChartBase
109
126
  currently_in_sprint = false
110
127
  change_data = []
111
128
 
112
- issue_completed_time = issue.board.cycletime.stopped_time(issue)
129
+ issue_completed_time = issue.board.cycletime.started_stopped_times(issue).last
113
130
  completed_has_been_tracked = false
114
131
 
115
132
  issue.changes.each do |change|
@@ -155,7 +172,7 @@ class SprintBurndown < ChartBase
155
172
  change_item.raw['to'].split(/\s*,\s*/).any? { |id| id.to_i == sprint.id }
156
173
  end
157
174
 
158
- def data_set_by_story_points sprint:, change_data_for_sprint:
175
+ def data_set_by_story_points sprint:, change_data_for_sprint: # rubocop:disable Metrics/CyclomaticComplexity
159
176
  summary_stats = SprintSummaryStats.new
160
177
  summary_stats.completed = 0.0
161
178
 
@@ -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,8 +73,38 @@ class Status
38
73
  end
39
74
 
40
75
  def to_s
41
- "Status(name=#{@name.inspect}, id=#{@id.inspect}," \
42
- " category_name=#{@category_name.inspect}, category_id=#{@category_id.inspect}, project_id=#{@project_id})"
76
+ "#{name.inspect}:#{id.inspect}"
77
+ end
78
+
79
+ def artificial?
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(', ') << ')'
43
108
  end
44
109
 
45
110
  def value_equality_ignored_variables
@@ -1,68 +1,109 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ class StatusNotFoundError < StandardError
4
+ end
5
+
3
6
  class StatusCollection
7
+ attr_reader :historical_status_mappings
8
+
4
9
  def initialize
5
10
  @list = []
11
+ @historical_status_mappings = {} # 'name:id' => category
6
12
  end
7
13
 
8
- def filter_status_names category_name:, including: nil, excluding: nil
9
- including = expand_statuses including
10
- 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
11
18
 
12
- @list.filter_map do |status|
13
- keep = status.category_name == category_name ||
14
- including.any? { |s| s.name == status.name }
15
- keep = false if excluding.any? { |s| s.name == status.name }
19
+ def find_all_by_name identifier
20
+ name, id = parse_name_id identifier
16
21
 
17
- status.name if keep
18
- end
19
- end
22
+ if id
23
+ status = find_by_id id
24
+ return [] if status.nil?
20
25
 
21
- def expand_statuses names_or_ids
22
- result = []
23
- return result if names_or_ids.nil?
24
-
25
- names_or_ids = [names_or_ids] unless names_or_ids.is_a? Array
26
-
27
- names_or_ids.each do |name_or_id|
28
- status = @list.find { |s| s.name == name_or_id || s.id == name_or_id }
29
- if status.nil?
30
- if block_given?
31
- yield name_or_id
32
- next
33
- else
34
- all_status_names = @list.collect { |s| "#{s.name.inspect}:#{s.id.inspect}" }.uniq.sort.join(', ')
35
- raise "Status not found: #{name_or_id}. Possible statuses are: #{all_status_names}"
36
- 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}."
37
29
  end
38
-
39
- result << status
30
+ [status]
31
+ else
32
+ @list.select { |status| status.name == name }
40
33
  end
41
- result
42
34
  end
43
35
 
44
- def todo including: nil, excluding: nil
45
- 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)
46
41
  end
47
42
 
48
- def in_progress including: nil, excluding: nil
49
- 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
50
52
  end
51
53
 
52
- def done including: nil, excluding: nil
53
- filter_status_names category_name: 'Done', including: including, excluding: excluding
54
- end
54
+ def find_all_categories_by_name identifier
55
+ key = nil
56
+ id = nil
57
+
58
+ if identifier.is_a? Symbol
59
+ key = identifier.to_s
60
+ else
61
+ name, id = parse_name_id identifier
62
+ end
55
63
 
56
- def find_by_name name
57
- find { |status| status.name == name }
64
+ find_all_categories.select { |c| c.id == id || c.name == name || c.key == key }
58
65
  end
59
66
 
60
- def find(&block)= @list.find(&block)
61
67
  def collect(&block) = @list.collect(&block)
68
+ def find(&block) = @list.find(&block)
62
69
  def each(&block) = @list.each(&block)
63
70
  def select(&block) = @list.select(&block)
64
71
  def <<(arg) = @list << arg
65
72
  def empty? = @list.empty?
66
73
  def clear = @list.clear
67
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
68
109
  end
@@ -5,11 +5,16 @@ class ThroughputChart < ChartBase
5
5
 
6
6
  attr_accessor :possible_statuses
7
7
 
8
- def initialize block = nil
8
+ def initialize block
9
9
  super()
10
10
 
11
11
  header_text 'Throughput Chart'
12
- description_text 'This chart shows how many items we completed per unit of time'
12
+ description_text <<-TEXT
13
+ <div class="p">
14
+ This chart shows how many items we completed per week
15
+ </div>
16
+ #{describe_non_working_days}
17
+ TEXT
13
18
 
14
19
  init_configuration_block(block) do
15
20
  grouping_rules do |issue, rule|
@@ -25,7 +30,10 @@ class ThroughputChart < ChartBase
25
30
  data_sets = []
26
31
  if rules_to_issues.size > 1
27
32
  data_sets << weekly_throughput_dataset(
28
- completed_issues: completed_issues, label: 'Totals', color: 'gray', dashed: true
33
+ completed_issues: completed_issues,
34
+ label: 'Totals',
35
+ color: CssVariable['--throughput_chart_total_line_color'],
36
+ dashed: true
29
37
  )
30
38
  end
31
39
 
@@ -74,7 +82,7 @@ class ThroughputChart < ChartBase
74
82
  def throughput_dataset periods:, completed_issues:
75
83
  periods.collect do |period|
76
84
  closed_issues = completed_issues.filter_map do |issue|
77
- stop_date = issue.board.cycletime.stopped_time(issue)&.to_date
85
+ stop_date = issue.board.cycletime.started_stopped_dates(issue).last
78
86
  [stop_date, issue] if stop_date && period.include?(stop_date)
79
87
  end
80
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,7 @@ 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'
71
+ require 'jirametrics/css_variable'
54
72
 
55
73
  require 'jirametrics/aggregate_config'
56
74
  require 'jirametrics/expedited_chart'
@@ -60,13 +78,15 @@ class JiraMetrics < Thor
60
78
  require 'jirametrics/trend_line_calculator'
61
79
  require 'jirametrics/status'
62
80
  require 'jirametrics/issue_link'
63
- require 'jirametrics/story_point_accuracy_chart'
81
+ require 'jirametrics/estimate_accuracy_chart'
64
82
  require 'jirametrics/status_collection'
65
83
  require 'jirametrics/sprint'
66
84
  require 'jirametrics/issue'
67
85
  require 'jirametrics/daily_wip_by_age_chart'
86
+ require 'jirametrics/daily_wip_by_parent_chart'
68
87
  require 'jirametrics/aging_work_in_progress_chart'
69
88
  require 'jirametrics/cycletime_scatterplot'
89
+ require 'jirametrics/flow_efficiency_scatterplot'
70
90
  require 'jirametrics/sprint_issue_change_data'
71
91
  require 'jirametrics/cycletime_histogram'
72
92
  require 'jirametrics/daily_wip_by_blocked_stalled_chart'
@@ -95,6 +115,4 @@ class JiraMetrics < Thor
95
115
  require 'jirametrics/board'
96
116
  load config_file
97
117
  end
98
-
99
- # Dir.foreach('lib/jirametrics') {|file| puts "require 'jirametrics/#{$1}'" if file =~ /^(.+)\.rb$/}
100
118
  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.0'
4
+ version: '2.11'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Bowler
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-04-22 00:00:00.000000000 Z
10
+ date: 2025-03-11 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,29 +69,31 @@ 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
76
+ - lib/jirametrics/css_variable.rb
77
77
  - lib/jirametrics/cycletime_config.rb
78
78
  - lib/jirametrics/cycletime_histogram.rb
79
79
  - lib/jirametrics/cycletime_scatterplot.rb
80
80
  - lib/jirametrics/daily_wip_by_age_chart.rb
81
81
  - lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb
82
+ - lib/jirametrics/daily_wip_by_parent_chart.rb
82
83
  - lib/jirametrics/daily_wip_chart.rb
83
84
  - lib/jirametrics/data_quality_report.rb
84
85
  - lib/jirametrics/dependency_chart.rb
85
- - lib/jirametrics/discard_changes_before.rb
86
86
  - lib/jirametrics/download_config.rb
87
87
  - lib/jirametrics/downloader.rb
88
+ - lib/jirametrics/estimate_accuracy_chart.rb
88
89
  - lib/jirametrics/examples/aggregated_project.rb
89
90
  - lib/jirametrics/examples/standard_project.rb
90
91
  - lib/jirametrics/expedited_chart.rb
91
- - lib/jirametrics/experimental/generator.rb
92
- - lib/jirametrics/experimental/info.rb
93
92
  - lib/jirametrics/exporter.rb
94
93
  - lib/jirametrics/file_config.rb
95
94
  - lib/jirametrics/file_system.rb
96
95
  - lib/jirametrics/fix_version.rb
96
+ - lib/jirametrics/flow_efficiency_scatterplot.rb
97
97
  - lib/jirametrics/groupable_issue_chart.rb
98
98
  - lib/jirametrics/grouping_rules.rb
99
99
  - lib/jirametrics/hierarchy_table.rb
@@ -104,12 +104,13 @@ files:
104
104
  - lib/jirametrics/html/cycletime_histogram.erb
105
105
  - lib/jirametrics/html/cycletime_scatterplot.erb
106
106
  - lib/jirametrics/html/daily_wip_chart.erb
107
- - lib/jirametrics/html/data_quality_report.erb
107
+ - lib/jirametrics/html/estimate_accuracy_chart.erb
108
108
  - lib/jirametrics/html/expedited_chart.erb
109
+ - lib/jirametrics/html/flow_efficiency_scatterplot.erb
109
110
  - lib/jirametrics/html/hierarchy_table.erb
111
+ - lib/jirametrics/html/index.css
110
112
  - lib/jirametrics/html/index.erb
111
113
  - lib/jirametrics/html/sprint_burndown.erb
112
- - lib/jirametrics/html/story_point_accuracy_chart.erb
113
114
  - lib/jirametrics/html/throughput_chart.erb
114
115
  - lib/jirametrics/html_report_config.rb
115
116
  - lib/jirametrics/issue.rb
@@ -118,25 +119,24 @@ files:
118
119
  - lib/jirametrics/project_config.rb
119
120
  - lib/jirametrics/rules.rb
120
121
  - lib/jirametrics/self_or_issue_dispatcher.rb
122
+ - lib/jirametrics/settings.json
121
123
  - lib/jirametrics/sprint.rb
122
124
  - lib/jirametrics/sprint_burndown.rb
123
125
  - lib/jirametrics/sprint_issue_change_data.rb
124
126
  - lib/jirametrics/status.rb
125
127
  - lib/jirametrics/status_collection.rb
126
- - lib/jirametrics/story_point_accuracy_chart.rb
127
128
  - lib/jirametrics/throughput_chart.rb
128
129
  - lib/jirametrics/tree_organizer.rb
129
130
  - lib/jirametrics/trend_line_calculator.rb
130
131
  - lib/jirametrics/value_equality.rb
131
- homepage: https://github.com/mikebowler/jirametrics
132
+ homepage: https://jirametrics.org
132
133
  licenses:
133
134
  - Apache-2.0
134
135
  metadata:
135
136
  rubygems_mfa_required: 'true'
136
137
  bug_tracker_uri: https://github.com/mikebowler/jirametrics/issues
137
- changelog_uri: https://github.com/mikebowler/jirametrics/wiki/Changes
138
- documentation_uri: https://github.com/mikebowler/jirametrics/wiki
139
- post_install_message:
138
+ changelog_uri: https://jirametrics.org/changes
139
+ documentation_uri: https://jirametrics.org
140
140
  rdoc_options: []
141
141
  require_paths:
142
142
  - lib
@@ -151,8 +151,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
151
151
  - !ruby/object:Gem::Version
152
152
  version: '0'
153
153
  requirements: []
154
- rubygems_version: 3.5.7
155
- signing_key:
154
+ rubygems_version: 3.6.2
156
155
  specification_version: 4
157
156
  summary: Extract Jira metrics
158
157
  test_files: []
@@ -1,37 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module DiscardChangesBefore
4
- def discard_changes_before status_becomes: nil, &block
5
- if status_becomes
6
- status_becomes = [status_becomes] unless status_becomes.is_a? Array
7
-
8
- block = lambda do |issue|
9
- trigger_statuses = status_becomes.collect do |status_name|
10
- if status_name == :backlog
11
- issue.board.backlog_statuses.collect(&:name)
12
- else
13
- status_name
14
- end
15
- end.flatten
16
-
17
- time = nil
18
- issue.changes.each do |change|
19
- time = change.time if change.status? && trigger_statuses.include?(change.value) && change.artificial? == false
20
- end
21
- time
22
- end
23
- end
24
-
25
- issues_cutoff_times = []
26
- issues.each do |issue|
27
- cutoff_time = block.call(issue)
28
- issues_cutoff_times << [issue, cutoff_time] if cutoff_time
29
- end
30
-
31
- discard_changes_before_hook issues_cutoff_times
32
-
33
- issues_cutoff_times.each do |issue, cutoff_time|
34
- issue.changes.reject! { |change| change.status? && change.time <= cutoff_time && change.artificial? == false }
35
- end
36
- end
37
- end