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.
- checksums.yaml +4 -4
- data/lib/jirametrics/aggregate_config.rb +19 -26
- data/lib/jirametrics/aging_work_bar_chart.rb +79 -54
- data/lib/jirametrics/aging_work_in_progress_chart.rb +106 -40
- data/lib/jirametrics/aging_work_table.rb +78 -43
- data/lib/jirametrics/anonymizer.rb +6 -5
- data/lib/jirametrics/blocked_stalled_change.rb +24 -4
- data/lib/jirametrics/board.rb +44 -15
- data/lib/jirametrics/board_config.rb +8 -4
- data/lib/jirametrics/board_movement_calculator.rb +147 -0
- data/lib/jirametrics/change_item.rb +31 -10
- data/lib/jirametrics/chart_base.rb +102 -61
- data/lib/jirametrics/columns_config.rb +4 -0
- data/lib/jirametrics/css_variable.rb +33 -0
- data/lib/jirametrics/cycletime_config.rb +59 -8
- data/lib/jirametrics/cycletime_histogram.rb +69 -4
- data/lib/jirametrics/cycletime_scatterplot.rb +11 -15
- data/lib/jirametrics/daily_wip_by_age_chart.rb +44 -20
- data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +37 -35
- data/lib/jirametrics/daily_wip_by_parent_chart.rb +38 -0
- data/lib/jirametrics/daily_wip_chart.rb +61 -14
- data/lib/jirametrics/data_quality_report.rb +222 -41
- data/lib/jirametrics/dependency_chart.rb +54 -23
- data/lib/jirametrics/download_config.rb +12 -0
- data/lib/jirametrics/downloader.rb +76 -57
- data/lib/jirametrics/{story_point_accuracy_chart.rb → estimate_accuracy_chart.rb} +48 -33
- data/lib/jirametrics/examples/aggregated_project.rb +22 -39
- data/lib/jirametrics/examples/standard_project.rb +25 -49
- data/lib/jirametrics/expedited_chart.rb +28 -25
- data/lib/jirametrics/exporter.rb +59 -32
- data/lib/jirametrics/file_config.rb +34 -13
- data/lib/jirametrics/file_system.rb +48 -3
- data/lib/jirametrics/flow_efficiency_scatterplot.rb +111 -0
- data/lib/jirametrics/groupable_issue_chart.rb +2 -6
- data/lib/jirametrics/grouping_rules.rb +7 -1
- data/lib/jirametrics/hierarchy_table.rb +4 -4
- data/lib/jirametrics/html/aging_work_bar_chart.erb +13 -16
- data/lib/jirametrics/html/aging_work_in_progress_chart.erb +28 -5
- data/lib/jirametrics/html/aging_work_table.erb +19 -25
- data/lib/jirametrics/html/cycletime_histogram.erb +83 -3
- data/lib/jirametrics/html/cycletime_scatterplot.erb +9 -12
- data/lib/jirametrics/html/daily_wip_chart.erb +17 -13
- data/lib/jirametrics/html/{story_point_accuracy_chart.erb → estimate_accuracy_chart.erb} +9 -4
- data/lib/jirametrics/html/expedited_chart.erb +10 -13
- data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +85 -0
- data/lib/jirametrics/html/hierarchy_table.erb +2 -2
- data/lib/jirametrics/html/index.css +209 -0
- data/lib/jirametrics/html/index.erb +16 -39
- data/lib/jirametrics/html/sprint_burndown.erb +10 -14
- data/lib/jirametrics/html/throughput_chart.erb +10 -13
- data/lib/jirametrics/html_report_config.rb +108 -86
- data/lib/jirametrics/issue.rb +357 -96
- data/lib/jirametrics/jira_gateway.rb +29 -11
- data/lib/jirametrics/project_config.rb +256 -144
- data/lib/jirametrics/rules.rb +2 -2
- data/lib/jirametrics/self_or_issue_dispatcher.rb +2 -0
- data/lib/jirametrics/settings.json +10 -0
- data/lib/jirametrics/sprint_burndown.rb +24 -7
- data/lib/jirametrics/status.rb +84 -19
- data/lib/jirametrics/status_collection.rb +80 -39
- data/lib/jirametrics/throughput_chart.rb +12 -4
- data/lib/jirametrics/value_equality.rb +2 -2
- data/lib/jirametrics.rb +25 -7
- metadata +16 -17
- data/lib/jirametrics/discard_changes_before.rb +0 -37
- data/lib/jirametrics/experimental/generator.rb +0 -210
- data/lib/jirametrics/experimental/info.rb +0 -77
- 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 <<
|
|
64
|
+
result << render_top_text(binding)
|
|
59
65
|
|
|
60
|
-
possible_colours =
|
|
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 %
|
|
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.
|
|
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
|
|
data/lib/jirametrics/status.rb
CHANGED
|
@@ -3,30 +3,65 @@
|
|
|
3
3
|
require 'jirametrics/value_equality'
|
|
4
4
|
|
|
5
5
|
class Status
|
|
6
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
18
|
+
def to_s
|
|
19
|
+
"#{name.inspect}:#{id.inspect}"
|
|
20
|
+
end
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
|
|
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
|
+
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
@
|
|
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
|
-
"
|
|
42
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
if id
|
|
23
|
+
status = find_by_id id
|
|
24
|
+
return [] if status.nil?
|
|
20
25
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
30
|
+
[status]
|
|
31
|
+
else
|
|
32
|
+
@list.select { |status| status.name == name }
|
|
40
33
|
end
|
|
41
|
-
result
|
|
42
34
|
end
|
|
43
35
|
|
|
44
|
-
def
|
|
45
|
-
|
|
36
|
+
def find_all_categories
|
|
37
|
+
@list
|
|
38
|
+
.collect(&:category)
|
|
39
|
+
.uniq
|
|
40
|
+
.sort_by(&:id)
|
|
46
41
|
end
|
|
47
42
|
|
|
48
|
-
def
|
|
49
|
-
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
|
8
|
+
def initialize block
|
|
9
9
|
super()
|
|
10
10
|
|
|
11
11
|
header_text 'Throughput Chart'
|
|
12
|
-
description_text
|
|
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,
|
|
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.
|
|
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
|
|
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
|
|
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 '
|
|
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/
|
|
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/
|
|
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.
|
|
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:
|
|
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:
|
|
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/
|
|
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://
|
|
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://
|
|
138
|
-
documentation_uri: https://
|
|
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.
|
|
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
|