jirametrics 2.11 → 2.14
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/aging_work_table.rb +6 -11
- data/lib/jirametrics/atlassian_document_format.rb +160 -0
- data/lib/jirametrics/board.rb +8 -9
- data/lib/jirametrics/board_config.rb +2 -0
- data/lib/jirametrics/board_movement_calculator.rb +8 -0
- data/lib/jirametrics/change_item.rb +30 -15
- data/lib/jirametrics/chart_base.rb +5 -4
- data/lib/jirametrics/cycletime_config.rb +1 -1
- data/lib/jirametrics/daily_view.rb +295 -0
- data/lib/jirametrics/downloader.rb +61 -21
- data/lib/jirametrics/estimate_accuracy_chart.rb +34 -10
- data/lib/jirametrics/estimation_configuration.rb +25 -0
- data/lib/jirametrics/examples/standard_project.rb +2 -0
- data/lib/jirametrics/file_config.rb +1 -1
- data/lib/jirametrics/html/aging_work_table.erb +2 -0
- data/lib/jirametrics/html/index.css +76 -0
- data/lib/jirametrics/html/index.erb +19 -2
- data/lib/jirametrics/html_report_config.rb +2 -0
- data/lib/jirametrics/issue.rb +42 -23
- data/lib/jirametrics/issue_collection.rb +33 -0
- data/lib/jirametrics/jira_gateway.rb +8 -1
- data/lib/jirametrics/project_config.rb +44 -10
- data/lib/jirametrics/settings.json +2 -1
- data/lib/jirametrics/sprint.rb +1 -0
- data/lib/jirametrics/sprint_burndown.rb +35 -33
- data/lib/jirametrics/sprint_issue_change_data.rb +3 -3
- data/lib/jirametrics/status_collection.rb +7 -0
- data/lib/jirametrics/user.rb +12 -0
- data/lib/jirametrics.rb +5 -0
- metadata +7 -2
|
@@ -6,7 +6,7 @@ require 'jirametrics/status_collection'
|
|
|
6
6
|
class ProjectConfig
|
|
7
7
|
attr_reader :target_path, :jira_config, :all_boards, :possible_statuses,
|
|
8
8
|
:download_config, :file_configs, :exporter, :data_version, :name, :board_configs,
|
|
9
|
-
:settings, :aggregate_config, :discarded_changes_data
|
|
9
|
+
:settings, :aggregate_config, :discarded_changes_data, :users
|
|
10
10
|
attr_accessor :time_range, :jira_url, :id
|
|
11
11
|
|
|
12
12
|
def initialize exporter:, jira_config:, block:, target_path: '.', name: '', id: nil
|
|
@@ -40,6 +40,7 @@ class ProjectConfig
|
|
|
40
40
|
@id = guess_project_id
|
|
41
41
|
load_project_metadata
|
|
42
42
|
load_sprints
|
|
43
|
+
load_users
|
|
43
44
|
end
|
|
44
45
|
|
|
45
46
|
def run load_only: false
|
|
@@ -113,10 +114,14 @@ class ProjectConfig
|
|
|
113
114
|
def file_prefix prefix
|
|
114
115
|
# The file_prefix has to be set before almost everything else. It really should have been an attribute
|
|
115
116
|
# on the project declaration itself. Hindsight is 20/20.
|
|
117
|
+
|
|
118
|
+
# There can only be one of these
|
|
116
119
|
if @file_prefix
|
|
117
|
-
raise "file_prefix
|
|
120
|
+
raise "file_prefix can only be set once. Was #{@file_prefix.inspect} and now changed to #{prefix.inspect}."
|
|
118
121
|
end
|
|
119
122
|
|
|
123
|
+
raise_if_prefix_already_used(prefix)
|
|
124
|
+
|
|
120
125
|
@file_prefix = prefix
|
|
121
126
|
|
|
122
127
|
# Yes, this is a wierd place to be initializing this. Unfortunately, it has to happen after the file_prefix
|
|
@@ -129,8 +134,21 @@ class ProjectConfig
|
|
|
129
134
|
@file_prefix
|
|
130
135
|
end
|
|
131
136
|
|
|
132
|
-
def
|
|
133
|
-
|
|
137
|
+
def raise_if_prefix_already_used prefix
|
|
138
|
+
@exporter.project_configs.each do |project|
|
|
139
|
+
next unless project.get_file_prefix(raise_if_not_set: false) == prefix && project.target_path == target_path
|
|
140
|
+
|
|
141
|
+
raise "Project #{name.inspect} specifies file prefix #{prefix.inspect}, " \
|
|
142
|
+
"but that is already used by project #{project.name.inspect} in the same target path #{target_path.inspect}. " \
|
|
143
|
+
'This is almost guaranteed to be too much copy and paste in your configuration. ' \
|
|
144
|
+
'File prefixes must be unique within a directory.'
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def get_file_prefix raise_if_not_set: true
|
|
149
|
+
if @file_prefix.nil? && raise_if_not_set
|
|
150
|
+
raise 'file_prefix has not been set yet. Move it to the top of the project declaration.'
|
|
151
|
+
end
|
|
134
152
|
|
|
135
153
|
@file_prefix
|
|
136
154
|
end
|
|
@@ -151,6 +169,8 @@ class ProjectConfig
|
|
|
151
169
|
end
|
|
152
170
|
|
|
153
171
|
def status_category_mapping status:, category:
|
|
172
|
+
return if @exporter.downloading?
|
|
173
|
+
|
|
154
174
|
status, status_id = possible_statuses.parse_name_id status
|
|
155
175
|
category, category_id = possible_statuses.parse_name_id category
|
|
156
176
|
|
|
@@ -323,6 +343,15 @@ class ProjectConfig
|
|
|
323
343
|
raise
|
|
324
344
|
end
|
|
325
345
|
|
|
346
|
+
def load_users
|
|
347
|
+
@users = []
|
|
348
|
+
filename = File.join @target_path, "#{get_file_prefix}_users.json"
|
|
349
|
+
return unless File.exist? filename
|
|
350
|
+
|
|
351
|
+
json = file_system.load_json(filename)
|
|
352
|
+
json.each { |user_data| @users << User.new(raw: user_data) }
|
|
353
|
+
end
|
|
354
|
+
|
|
326
355
|
def to_time string, end_of_day: false
|
|
327
356
|
time = end_of_day ? '23:59:59' : '00:00:00'
|
|
328
357
|
string = "#{string}T#{time}#{exporter.timezone_offset}" if string.match?(/^\d{4}-\d{2}-\d{2}$/)
|
|
@@ -356,7 +385,7 @@ class ProjectConfig
|
|
|
356
385
|
|
|
357
386
|
# To be used by the aggregate_config only. Not intended to be part of the public API
|
|
358
387
|
def add_issues issues_list
|
|
359
|
-
@issues =
|
|
388
|
+
@issues = IssueCollection.new if @issues.nil?
|
|
360
389
|
@all_boards = {}
|
|
361
390
|
|
|
362
391
|
issues_list.each do |issue|
|
|
@@ -373,7 +402,7 @@ class ProjectConfig
|
|
|
373
402
|
'declaration but none are here. Check your config.'
|
|
374
403
|
end
|
|
375
404
|
|
|
376
|
-
return @issues =
|
|
405
|
+
return @issues = IssueCollection.new if @exporter.downloading?
|
|
377
406
|
raise 'No data found. Must do a download before an export' unless data_downloaded?
|
|
378
407
|
|
|
379
408
|
load_data if all_boards.empty?
|
|
@@ -385,7 +414,7 @@ class ProjectConfig
|
|
|
385
414
|
issues = load_issues_from_issues_directory path: issues_path, timezone_offset: timezone_offset
|
|
386
415
|
else
|
|
387
416
|
file_system.log "Can't find directory #{issues_path}. Has a download been done?", also_write_to_stderr: true
|
|
388
|
-
return
|
|
417
|
+
return IssueCollection.new
|
|
389
418
|
end
|
|
390
419
|
|
|
391
420
|
# Attach related issues
|
|
@@ -397,7 +426,8 @@ class ProjectConfig
|
|
|
397
426
|
|
|
398
427
|
# We'll have some issues that are in the list that weren't part of the initial query. Once we've
|
|
399
428
|
# attached them in the appropriate places, remove any that aren't part of that initial set.
|
|
400
|
-
|
|
429
|
+
issues.reject! { |i| !i.in_initial_query? } # rubocop:disable Style/InverseMethods
|
|
430
|
+
@issues = issues
|
|
401
431
|
end
|
|
402
432
|
|
|
403
433
|
@issues
|
|
@@ -438,7 +468,7 @@ class ProjectConfig
|
|
|
438
468
|
end
|
|
439
469
|
|
|
440
470
|
def load_issues_from_issues_directory path:, timezone_offset:
|
|
441
|
-
issues =
|
|
471
|
+
issues = IssueCollection.new
|
|
442
472
|
default_board = nil
|
|
443
473
|
|
|
444
474
|
group_filenames_and_board_ids(path: path).each do |filename, board_ids|
|
|
@@ -450,6 +480,10 @@ class ProjectConfig
|
|
|
450
480
|
end
|
|
451
481
|
|
|
452
482
|
boards.each do |board|
|
|
483
|
+
if board.cycletime.nil?
|
|
484
|
+
raise "The board declaration for board #{board.id} must come before the " \
|
|
485
|
+
"first usage of 'issues' in the configuration"
|
|
486
|
+
end
|
|
453
487
|
issues << Issue.new(raw: content, timezone_offset: timezone_offset, board: board)
|
|
454
488
|
end
|
|
455
489
|
end
|
|
@@ -462,7 +496,7 @@ class ProjectConfig
|
|
|
462
496
|
# board ids appropriately.
|
|
463
497
|
def group_filenames_and_board_ids path:
|
|
464
498
|
hash = {}
|
|
465
|
-
|
|
499
|
+
file_system.foreach(path) do |filename|
|
|
466
500
|
# Matches either FAKE-123.json or FAKE-123-456.json
|
|
467
501
|
if /^(?<key>[^-]+-\d+)(?<_>-(?<board_id>\d+))?\.json$/ =~ filename
|
|
468
502
|
(hash[key] ||= []) << [filename, board_id&.to_i || :unknown]
|
data/lib/jirametrics/sprint.rb
CHANGED
|
@@ -121,11 +121,13 @@ 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
|
-
|
|
124
|
+
estimate = 0.0
|
|
125
125
|
ever_in_sprint = false
|
|
126
126
|
currently_in_sprint = false
|
|
127
127
|
change_data = []
|
|
128
128
|
|
|
129
|
+
estimate_display_name = current_board.estimation_configuration.display_name
|
|
130
|
+
|
|
129
131
|
issue_completed_time = issue.board.cycletime.started_stopped_times(issue).last
|
|
130
132
|
completed_has_been_tracked = false
|
|
131
133
|
|
|
@@ -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 =
|
|
145
|
+
value = estimate
|
|
144
146
|
elsif currently_in_sprint && in_change_item == false
|
|
145
147
|
action = :leave_sprint
|
|
146
|
-
value = -
|
|
148
|
+
value = -estimate
|
|
147
149
|
end
|
|
148
150
|
currently_in_sprint = in_change_item
|
|
149
|
-
elsif change.
|
|
151
|
+
elsif change.field == estimate_display_name && (issue_completed_time.nil? || change.time < issue_completed_time)
|
|
150
152
|
action = :story_points
|
|
151
|
-
|
|
152
|
-
value =
|
|
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 = -
|
|
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,
|
|
164
|
+
time: change.time, issue: issue, action: action, value: value, estimate: estimate
|
|
163
165
|
)
|
|
164
166
|
end
|
|
165
167
|
|
|
@@ -176,7 +178,7 @@ class SprintBurndown < ChartBase
|
|
|
176
178
|
summary_stats = SprintSummaryStats.new
|
|
177
179
|
summary_stats.completed = 0.0
|
|
178
180
|
|
|
179
|
-
|
|
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:
|
|
190
|
+
y: estimate,
|
|
189
191
|
x: chart_format(sprint.start_time),
|
|
190
|
-
title: "Sprint started with #{
|
|
192
|
+
title: "Sprint started with #{estimate} points"
|
|
191
193
|
}
|
|
192
|
-
summary_stats.started =
|
|
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
|
-
|
|
203
|
+
estimate += change_data.estimate
|
|
202
204
|
when :leave_sprint
|
|
203
205
|
issues_currently_in_sprint.delete change_data.issue.key
|
|
204
|
-
|
|
206
|
+
estimate -= change_data.estimate
|
|
205
207
|
when :story_points
|
|
206
|
-
|
|
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
|
-
|
|
217
|
-
message = "Story points changed from #{
|
|
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.
|
|
221
|
-
summary_stats.added += change_data.
|
|
222
|
+
message = "Added to sprint with #{change_data.estimate || 'no'} points"
|
|
223
|
+
summary_stats.added += change_data.estimate
|
|
222
224
|
when :issue_stopped
|
|
223
|
-
|
|
224
|
-
message = "Completed with #{change_data.
|
|
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.
|
|
228
|
+
summary_stats.completed += change_data.estimate
|
|
227
229
|
when :leave_sprint
|
|
228
|
-
message = "Removed from sprint with #{change_data.
|
|
229
|
-
summary_stats.removed += change_data.
|
|
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:
|
|
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:
|
|
246
|
+
y: estimate,
|
|
245
247
|
x: chart_format(sprint.start_time),
|
|
246
|
-
title: "Sprint started with #{
|
|
248
|
+
title: "Sprint started with #{estimate} points"
|
|
247
249
|
}
|
|
248
|
-
summary_stats.started =
|
|
250
|
+
summary_stats.started = estimate
|
|
249
251
|
end
|
|
250
252
|
|
|
251
253
|
if sprint.completed_time
|
|
252
254
|
data_set << {
|
|
253
|
-
y:
|
|
255
|
+
y: estimate,
|
|
254
256
|
x: chart_format(sprint.completed_time),
|
|
255
|
-
title: "Sprint ended with #{
|
|
257
|
+
title: "Sprint ended with #{estimate} points unfinished"
|
|
256
258
|
}
|
|
257
|
-
summary_stats.remaining =
|
|
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:
|
|
264
|
+
y: estimate,
|
|
263
265
|
x: chart_format(time_range.end),
|
|
264
|
-
title: "Sprint still active. #{
|
|
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, :
|
|
7
|
+
attr_reader :time, :action, :value, :issue, :estimate
|
|
8
8
|
|
|
9
|
-
def initialize time:, action:, value:, issue:,
|
|
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
|
-
@
|
|
14
|
+
@estimate = estimate
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def inspect
|
|
@@ -16,6 +16,13 @@ class StatusCollection
|
|
|
16
16
|
@list.find { |status| status.id == id }
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
+
def find_by_id! id
|
|
20
|
+
status = @list.find { |status| status.id == id }
|
|
21
|
+
raise "Can't find any status for id #{id} in #{self}" unless status
|
|
22
|
+
|
|
23
|
+
status
|
|
24
|
+
end
|
|
25
|
+
|
|
19
26
|
def find_all_by_name identifier
|
|
20
27
|
name, id = parse_name_id identifier
|
|
21
28
|
|
data/lib/jirametrics.rb
CHANGED
|
@@ -69,6 +69,7 @@ class JiraMetrics < Thor
|
|
|
69
69
|
require 'jirametrics/daily_wip_chart'
|
|
70
70
|
require 'jirametrics/groupable_issue_chart'
|
|
71
71
|
require 'jirametrics/css_variable'
|
|
72
|
+
require 'jirametrics/issue_collection'
|
|
72
73
|
|
|
73
74
|
require 'jirametrics/aggregate_config'
|
|
74
75
|
require 'jirametrics/expedited_chart'
|
|
@@ -112,7 +113,11 @@ class JiraMetrics < Thor
|
|
|
112
113
|
require 'jirametrics/download_config'
|
|
113
114
|
require 'jirametrics/columns_config'
|
|
114
115
|
require 'jirametrics/hierarchy_table'
|
|
116
|
+
require 'jirametrics/estimation_configuration'
|
|
115
117
|
require 'jirametrics/board'
|
|
118
|
+
require 'jirametrics/daily_view'
|
|
119
|
+
require 'jirametrics/user'
|
|
120
|
+
require 'jirametrics/atlassian_document_format'
|
|
116
121
|
load config_file
|
|
117
122
|
end
|
|
118
123
|
end
|
metadata
CHANGED
|
@@ -1,13 +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.14'
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mike Bowler
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2025-
|
|
10
|
+
date: 2025-08-18 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: random-word
|
|
@@ -65,6 +65,7 @@ files:
|
|
|
65
65
|
- lib/jirametrics/aging_work_in_progress_chart.rb
|
|
66
66
|
- lib/jirametrics/aging_work_table.rb
|
|
67
67
|
- lib/jirametrics/anonymizer.rb
|
|
68
|
+
- lib/jirametrics/atlassian_document_format.rb
|
|
68
69
|
- lib/jirametrics/blocked_stalled_change.rb
|
|
69
70
|
- lib/jirametrics/board.rb
|
|
70
71
|
- lib/jirametrics/board_column.rb
|
|
@@ -77,6 +78,7 @@ files:
|
|
|
77
78
|
- lib/jirametrics/cycletime_config.rb
|
|
78
79
|
- lib/jirametrics/cycletime_histogram.rb
|
|
79
80
|
- lib/jirametrics/cycletime_scatterplot.rb
|
|
81
|
+
- lib/jirametrics/daily_view.rb
|
|
80
82
|
- lib/jirametrics/daily_wip_by_age_chart.rb
|
|
81
83
|
- lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb
|
|
82
84
|
- lib/jirametrics/daily_wip_by_parent_chart.rb
|
|
@@ -86,6 +88,7 @@ files:
|
|
|
86
88
|
- lib/jirametrics/download_config.rb
|
|
87
89
|
- lib/jirametrics/downloader.rb
|
|
88
90
|
- lib/jirametrics/estimate_accuracy_chart.rb
|
|
91
|
+
- lib/jirametrics/estimation_configuration.rb
|
|
89
92
|
- lib/jirametrics/examples/aggregated_project.rb
|
|
90
93
|
- lib/jirametrics/examples/standard_project.rb
|
|
91
94
|
- lib/jirametrics/expedited_chart.rb
|
|
@@ -114,6 +117,7 @@ files:
|
|
|
114
117
|
- lib/jirametrics/html/throughput_chart.erb
|
|
115
118
|
- lib/jirametrics/html_report_config.rb
|
|
116
119
|
- lib/jirametrics/issue.rb
|
|
120
|
+
- lib/jirametrics/issue_collection.rb
|
|
117
121
|
- lib/jirametrics/issue_link.rb
|
|
118
122
|
- lib/jirametrics/jira_gateway.rb
|
|
119
123
|
- lib/jirametrics/project_config.rb
|
|
@@ -128,6 +132,7 @@ files:
|
|
|
128
132
|
- lib/jirametrics/throughput_chart.rb
|
|
129
133
|
- lib/jirametrics/tree_organizer.rb
|
|
130
134
|
- lib/jirametrics/trend_line_calculator.rb
|
|
135
|
+
- lib/jirametrics/user.rb
|
|
131
136
|
- lib/jirametrics/value_equality.rb
|
|
132
137
|
homepage: https://jirametrics.org
|
|
133
138
|
licenses:
|