jirametrics 2.22 → 2.27
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 +10 -2
- data/lib/jirametrics/aging_work_bar_chart.rb +20 -6
- data/lib/jirametrics/aging_work_table.rb +4 -5
- data/lib/jirametrics/anonymizer.rb +74 -1
- data/lib/jirametrics/atlassian_document_format.rb +93 -93
- data/lib/jirametrics/blocked_stalled_change.rb +5 -3
- data/lib/jirametrics/board.rb +20 -8
- data/lib/jirametrics/board_feature.rb +14 -0
- data/lib/jirametrics/board_movement_calculator.rb +2 -2
- data/lib/jirametrics/cfd_data_builder.rb +108 -0
- data/lib/jirametrics/change_item.rb +4 -3
- data/lib/jirametrics/chart_base.rb +94 -2
- data/lib/jirametrics/css_variable.rb +1 -1
- data/lib/jirametrics/cumulative_flow_diagram.rb +208 -0
- data/lib/jirametrics/{cycletime_config.rb → cycle_time_config.rb} +1 -2
- data/lib/jirametrics/cycletime_histogram.rb +15 -103
- data/lib/jirametrics/cycletime_scatterplot.rb +13 -98
- data/lib/jirametrics/daily_view.rb +36 -12
- data/lib/jirametrics/daily_wip_by_age_chart.rb +1 -1
- data/lib/jirametrics/daily_wip_by_blocked_stalled_chart.rb +1 -1
- data/lib/jirametrics/daily_wip_by_parent_chart.rb +4 -2
- data/lib/jirametrics/daily_wip_chart.rb +29 -7
- data/lib/jirametrics/data_quality_report.rb +38 -12
- data/lib/jirametrics/dependency_chart.rb +2 -2
- data/lib/jirametrics/download_config.rb +15 -0
- data/lib/jirametrics/downloader.rb +87 -5
- data/lib/jirametrics/downloader_for_cloud.rb +52 -10
- data/lib/jirametrics/downloader_for_data_center.rb +2 -1
- data/lib/jirametrics/estimate_accuracy_chart.rb +42 -4
- data/lib/jirametrics/examples/aggregated_project.rb +2 -2
- data/lib/jirametrics/examples/standard_project.rb +29 -19
- data/lib/jirametrics/expedited_chart.rb +3 -1
- data/lib/jirametrics/exporter.rb +3 -1
- data/lib/jirametrics/file_system.rb +35 -2
- data/lib/jirametrics/flow_efficiency_scatterplot.rb +5 -1
- data/lib/jirametrics/github_gateway.rb +115 -0
- data/lib/jirametrics/groupable_issue_chart.rb +4 -0
- data/lib/jirametrics/grouping_rules.rb +26 -4
- data/lib/jirametrics/html/aging_work_bar_chart.erb +3 -4
- data/lib/jirametrics/html/aging_work_table.erb +3 -0
- data/lib/jirametrics/html/cumulative_flow_diagram.erb +503 -0
- data/lib/jirametrics/html/daily_wip_chart.erb +38 -5
- data/lib/jirametrics/html/estimate_accuracy_chart.erb +2 -12
- data/lib/jirametrics/html/expedited_chart.erb +3 -13
- data/lib/jirametrics/html/flow_efficiency_scatterplot.erb +2 -8
- data/lib/jirametrics/html/index.css +117 -0
- data/lib/jirametrics/html/index.erb +6 -0
- data/lib/jirametrics/html/index.js +52 -2
- data/lib/jirametrics/html/sprint_burndown.erb +7 -13
- data/lib/jirametrics/html/throughput_chart.erb +40 -9
- data/lib/jirametrics/html/{cycletime_histogram.erb → time_based_histogram.erb} +59 -59
- data/lib/jirametrics/html/{cycletime_scatterplot.erb → time_based_scatterplot.erb} +11 -7
- data/lib/jirametrics/html_generator.rb +2 -1
- data/lib/jirametrics/html_report_config.rb +23 -16
- data/lib/jirametrics/issue.rb +101 -96
- data/lib/jirametrics/issue_printer.rb +97 -0
- data/lib/jirametrics/jira_gateway.rb +6 -3
- data/lib/jirametrics/mcp_server.rb +305 -0
- data/lib/jirametrics/project_config.rb +80 -7
- data/lib/jirametrics/pull_request.rb +30 -0
- data/lib/jirametrics/pull_request_cycle_time_histogram.rb +77 -0
- data/lib/jirametrics/pull_request_cycle_time_scatterplot.rb +88 -0
- data/lib/jirametrics/pull_request_review.rb +13 -0
- data/lib/jirametrics/raw_javascript.rb +4 -0
- data/lib/jirametrics/settings.json +3 -1
- data/lib/jirametrics/sprint_burndown.rb +3 -1
- data/lib/jirametrics/status.rb +1 -1
- data/lib/jirametrics/stitcher.rb +7 -1
- data/lib/jirametrics/throughput_by_completed_resolution_chart.rb +22 -0
- data/lib/jirametrics/throughput_chart.rb +73 -23
- data/lib/jirametrics/time_based_histogram.rb +139 -0
- data/lib/jirametrics/time_based_scatterplot.rb +107 -0
- data/lib/jirametrics.rb +28 -0
- metadata +47 -5
data/lib/jirametrics/issue.rb
CHANGED
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
require 'time'
|
|
4
4
|
|
|
5
5
|
class Issue
|
|
6
|
-
attr_reader :changes, :raw, :subtasks, :board
|
|
7
|
-
attr_accessor :parent
|
|
6
|
+
attr_reader :changes, :raw, :subtasks, :board, :discarded_changes, :discarded_change_times
|
|
7
|
+
attr_accessor :parent, :github_prs
|
|
8
8
|
|
|
9
9
|
def initialize raw:, board:, timezone_offset: '+00:00'
|
|
10
10
|
@raw = raw
|
|
11
11
|
@timezone_offset = timezone_offset
|
|
12
12
|
@subtasks = []
|
|
13
13
|
@changes = []
|
|
14
|
+
@github_prs = []
|
|
14
15
|
@board = board
|
|
15
16
|
|
|
16
17
|
# We only check for this here because if a board isn't passed in then things will fail much
|
|
@@ -212,6 +213,19 @@ class Issue
|
|
|
212
213
|
first_time_in_status(*board.visible_columns.collect(&:status_ids).flatten)
|
|
213
214
|
end
|
|
214
215
|
|
|
216
|
+
def reasons_not_visible_on_board
|
|
217
|
+
reasons = []
|
|
218
|
+
reasons << 'Not in an active sprint' if board.scrum? && sprints.none?(&:active?)
|
|
219
|
+
unless board.visible_columns.any? { |c| c.status_ids.include?(status.id) }
|
|
220
|
+
reasons << 'Status is not configured for any visible column on the board'
|
|
221
|
+
end
|
|
222
|
+
reasons
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def visible_on_board?
|
|
226
|
+
reasons_not_visible_on_board.empty?
|
|
227
|
+
end
|
|
228
|
+
|
|
215
229
|
# If this issue will ever be in an active sprint then return the time that it
|
|
216
230
|
# was first added to that sprint, whether or not the sprint was active at that
|
|
217
231
|
# time. Although it seems like an odd thing to calculate, it's a reasonable proxy
|
|
@@ -270,13 +284,19 @@ class Issue
|
|
|
270
284
|
# First look in the actual sprints json. If any issues are in this sprint then it should
|
|
271
285
|
# be here.
|
|
272
286
|
sprint = board.sprints.find { |s| s.id == sprint_id }
|
|
273
|
-
|
|
287
|
+
if sprint
|
|
288
|
+
return [nil, nil] if sprint.future?
|
|
289
|
+
|
|
290
|
+
return [sprint.start_time, sprint.completed_time]
|
|
291
|
+
end
|
|
274
292
|
|
|
275
293
|
# Then look at the sprints inside the issue. Even though the field id may be specified,
|
|
276
294
|
# that custom field may not be present. This happens if it was in that sprint but was
|
|
277
295
|
# then removed, whether or not that sprint had ever started.
|
|
278
296
|
sprint_data = raw['fields'][change.field_id]&.find { |sd| sd['id'].to_i == sprint_id }
|
|
279
297
|
if sprint_data
|
|
298
|
+
return [nil, nil] if sprint_data['state'] == 'future'
|
|
299
|
+
|
|
280
300
|
start = parse_time(sprint_data['startDate'])
|
|
281
301
|
stop = parse_time(sprint_data['completeDate'])
|
|
282
302
|
return [start, stop]
|
|
@@ -388,21 +408,11 @@ class Issue
|
|
|
388
408
|
results
|
|
389
409
|
end
|
|
390
410
|
|
|
391
|
-
def blocked_stalled_statuses settings
|
|
392
|
-
blocked_statuses = settings['blocked_statuses']
|
|
393
|
-
stalled_statuses = settings['stalled_statuses']
|
|
394
|
-
unless blocked_statuses.is_a?(Array) && stalled_statuses.is_a?(Array)
|
|
395
|
-
raise "blocked_statuses(#{blocked_statuses.inspect}) and " \
|
|
396
|
-
"stalled_statuses(#{stalled_statuses.inspect}) must both be arrays"
|
|
397
|
-
end
|
|
398
|
-
|
|
399
|
-
[blocked_statuses, stalled_statuses]
|
|
400
|
-
end
|
|
401
|
-
|
|
402
411
|
def blocked_stalled_changes end_time:, settings: nil
|
|
403
412
|
settings ||= @board.project_config.settings
|
|
404
413
|
|
|
405
|
-
blocked_statuses
|
|
414
|
+
blocked_statuses = settings['blocked_statuses']
|
|
415
|
+
stalled_statuses = settings['stalled_statuses']
|
|
406
416
|
|
|
407
417
|
blocked_link_texts = settings['blocked_link_text']
|
|
408
418
|
stalled_threshold = settings['stalled_threshold_days']
|
|
@@ -415,7 +425,9 @@ class Issue
|
|
|
415
425
|
previous_change_time = created
|
|
416
426
|
|
|
417
427
|
blocking_status = nil
|
|
428
|
+
blocking_is_blocked = false
|
|
418
429
|
flag = nil
|
|
430
|
+
flag_reason = nil
|
|
419
431
|
|
|
420
432
|
# This mock change is to force the writing of one last entry at the end of the time range.
|
|
421
433
|
# By doing this, we're able to eliminate a lot of duplicated code in charts.
|
|
@@ -430,11 +442,14 @@ class Issue
|
|
|
430
442
|
)
|
|
431
443
|
|
|
432
444
|
if change.flagged? && flagged_means_blocked
|
|
433
|
-
flag = change
|
|
434
|
-
flag = nil if change.value == ''
|
|
445
|
+
flag, flag_reason = blocked_stalled_changes_flag_logic change
|
|
435
446
|
elsif change.status?
|
|
436
447
|
blocking_status = nil
|
|
437
|
-
|
|
448
|
+
blocking_is_blocked = false
|
|
449
|
+
if blocked_statuses.find_by_id(change.value_id)
|
|
450
|
+
blocking_status = change.value
|
|
451
|
+
blocking_is_blocked = true
|
|
452
|
+
elsif stalled_statuses.find_by_id(change.value_id)
|
|
438
453
|
blocking_status = change.value
|
|
439
454
|
end
|
|
440
455
|
elsif change.link?
|
|
@@ -455,8 +470,9 @@ class Issue
|
|
|
455
470
|
|
|
456
471
|
new_change = BlockedStalledChange.new(
|
|
457
472
|
flagged: flag,
|
|
473
|
+
flag_reason: flag_reason,
|
|
458
474
|
status: blocking_status,
|
|
459
|
-
status_is_blocking: blocking_status.nil? ||
|
|
475
|
+
status_is_blocking: blocking_status.nil? || blocking_is_blocked,
|
|
460
476
|
blocking_issue_keys: (blocking_issue_keys.empty? ? nil : blocking_issue_keys.dup),
|
|
461
477
|
time: change.time
|
|
462
478
|
)
|
|
@@ -475,6 +491,7 @@ class Issue
|
|
|
475
491
|
hack = result.pop
|
|
476
492
|
result << BlockedStalledChange.new(
|
|
477
493
|
flagged: hack.flag,
|
|
494
|
+
flag_reason: hack.flag_reason,
|
|
478
495
|
status: hack.status,
|
|
479
496
|
status_is_blocking: hack.status_is_blocking,
|
|
480
497
|
blocking_issue_keys: hack.blocking_issue_keys,
|
|
@@ -486,6 +503,28 @@ class Issue
|
|
|
486
503
|
result
|
|
487
504
|
end
|
|
488
505
|
|
|
506
|
+
def blocked_stalled_changes_flag_logic change
|
|
507
|
+
flag = change.value
|
|
508
|
+
flag = nil if change.value == ''
|
|
509
|
+
if flag
|
|
510
|
+
# When the user is adding a comment to explain why a flag was set, the flag is set immediately
|
|
511
|
+
# and the comment is inserted after the user hits enter, which means that there is some time
|
|
512
|
+
# gap. If a comment happened shortly after the flag was set, we assume they're linked. This
|
|
513
|
+
# won't always be true and so there will be false positives, but it's a reasonable assumption.
|
|
514
|
+
max_seconds_between_flag_and_comment = 30
|
|
515
|
+
comment_change = changes.find do |c|
|
|
516
|
+
c.comment? && c.time >= change.time && (c.time - change.time) <= max_seconds_between_flag_and_comment
|
|
517
|
+
end
|
|
518
|
+
flag_reason = comment_change && @board.project_config.atlassian_document_format.to_text(comment_change.value)
|
|
519
|
+
# Newer Jira instances may add this extra text but older instances did not. Strip it out if found.
|
|
520
|
+
flag_reason = flag_reason&.sub(/\A:flag_on: Flag added\s*/m, '')&.strip
|
|
521
|
+
flag_reason = nil if flag_reason&.empty?
|
|
522
|
+
else
|
|
523
|
+
flag_reason = nil
|
|
524
|
+
end
|
|
525
|
+
[flag, flag_reason]
|
|
526
|
+
end
|
|
527
|
+
|
|
489
528
|
def check_for_stalled change_time:, previous_change_time:, stalled_threshold:, blocking_stalled_changes:
|
|
490
529
|
stalled_threshold_seconds = stalled_threshold * 60 * 60 * 24
|
|
491
530
|
|
|
@@ -521,7 +560,7 @@ class Issue
|
|
|
521
560
|
# return [number of active seconds, total seconds] that this issue had up to the end_time.
|
|
522
561
|
# It does not include data before issue start or after issue end
|
|
523
562
|
def flow_efficiency_numbers end_time:, settings: @board.project_config.settings
|
|
524
|
-
issue_start, issue_stop =
|
|
563
|
+
issue_start, issue_stop = started_stopped_times
|
|
525
564
|
return [0.0, 0.0] if !issue_start || issue_start > end_time
|
|
526
565
|
|
|
527
566
|
value_add_time = 0.0
|
|
@@ -701,75 +740,7 @@ class Issue
|
|
|
701
740
|
end
|
|
702
741
|
|
|
703
742
|
def dump
|
|
704
|
-
|
|
705
|
-
result << "#{key} (#{type}): #{compact_text summary, max: 200}\n"
|
|
706
|
-
|
|
707
|
-
assignee = raw['fields']['assignee']
|
|
708
|
-
result << " [assignee] #{assignee['name'].inspect} <#{assignee['emailAddress']}>\n" unless assignee.nil?
|
|
709
|
-
|
|
710
|
-
raw['fields']['issuelinks']&.each do |link|
|
|
711
|
-
result << " [link] #{link['type']['outward']} #{link['outwardIssue']['key']}\n" if link['outwardIssue']
|
|
712
|
-
result << " [link] #{link['type']['inward']} #{link['inwardIssue']['key']}\n" if link['inwardIssue']
|
|
713
|
-
end
|
|
714
|
-
history = [] # time, type, detail
|
|
715
|
-
|
|
716
|
-
if board.cycletime
|
|
717
|
-
started_at, stopped_at = board.cycletime.started_stopped_times(self)
|
|
718
|
-
history << [started_at, nil, '↓↓↓↓ Started here ↓↓↓↓', true] if started_at
|
|
719
|
-
history << [stopped_at, nil, '↑↑↑↑ Finished here ↑↑↑↑', true] if stopped_at
|
|
720
|
-
else
|
|
721
|
-
result << " Unable to determine start/end times as board #{board.id} has no cycletime specified\n"
|
|
722
|
-
end
|
|
723
|
-
|
|
724
|
-
@discarded_change_times&.each do |time|
|
|
725
|
-
history << [time, nil, '↑↑↑↑ Changes discarded ↑↑↑↑', true]
|
|
726
|
-
end
|
|
727
|
-
|
|
728
|
-
(changes + (@discarded_changes || [])).each do |change|
|
|
729
|
-
if change.status?
|
|
730
|
-
value = "#{change.value.inspect}:#{change.value_id.inspect}"
|
|
731
|
-
old_value = change.old_value ? "#{change.old_value.inspect}:#{change.old_value_id.inspect}" : nil
|
|
732
|
-
else
|
|
733
|
-
value = compact_text(change.value).inspect
|
|
734
|
-
old_value = change.old_value ? compact_text(change.old_value).inspect : nil
|
|
735
|
-
end
|
|
736
|
-
|
|
737
|
-
message = +''
|
|
738
|
-
message << "#{old_value} -> " unless old_value.nil? || old_value.empty?
|
|
739
|
-
message << value
|
|
740
|
-
if change.artificial?
|
|
741
|
-
message << ' (Artificial entry)' if change.artificial?
|
|
742
|
-
else
|
|
743
|
-
message << " (Author: #{change.author})"
|
|
744
|
-
end
|
|
745
|
-
history << [change.time, change.field, message, change.artificial?]
|
|
746
|
-
end
|
|
747
|
-
|
|
748
|
-
result << " History:\n"
|
|
749
|
-
type_width = history.collect { |_time, type, _detail, _artificial| type&.length || 0 }.max
|
|
750
|
-
history.sort! do |a, b|
|
|
751
|
-
if a[0] == b[0]
|
|
752
|
-
if a[1].nil?
|
|
753
|
-
1
|
|
754
|
-
elsif b[1].nil?
|
|
755
|
-
-1
|
|
756
|
-
else
|
|
757
|
-
a[1] <=> b[1]
|
|
758
|
-
end
|
|
759
|
-
else
|
|
760
|
-
a[0] <=> b[0]
|
|
761
|
-
end
|
|
762
|
-
end
|
|
763
|
-
history.each do |time, type, detail, _artificial|
|
|
764
|
-
if type.nil?
|
|
765
|
-
type = '-' * type_width
|
|
766
|
-
else
|
|
767
|
-
type = (' ' * (type_width - type.length)) << type
|
|
768
|
-
end
|
|
769
|
-
result << " #{time.strftime '%Y-%m-%d %H:%M:%S %z'} [#{type}] #{detail}\n"
|
|
770
|
-
end
|
|
771
|
-
|
|
772
|
-
result
|
|
743
|
+
IssuePrinter.new(self).to_s
|
|
773
744
|
end
|
|
774
745
|
|
|
775
746
|
def done?
|
|
@@ -782,10 +753,36 @@ class Issue
|
|
|
782
753
|
end
|
|
783
754
|
end
|
|
784
755
|
|
|
756
|
+
def started_stopped_times
|
|
757
|
+
board.cycletime.started_stopped_times(self)
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
def started_stopped_dates
|
|
761
|
+
board.cycletime.started_stopped_dates(self)
|
|
762
|
+
end
|
|
763
|
+
|
|
785
764
|
def status_changes
|
|
786
765
|
@changes.select { |change| change.status? }
|
|
787
766
|
end
|
|
788
767
|
|
|
768
|
+
def status_resolution_at_done
|
|
769
|
+
done_time = started_stopped_times.last
|
|
770
|
+
return [nil, nil] if done_time.nil?
|
|
771
|
+
|
|
772
|
+
status_change = nil
|
|
773
|
+
resolution = nil
|
|
774
|
+
|
|
775
|
+
@changes.each do |change|
|
|
776
|
+
break if change.time > done_time
|
|
777
|
+
|
|
778
|
+
status_change = change if change.status?
|
|
779
|
+
resolution = change.value if change.resolution?
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
status = status_change ? find_or_create_status(id: status_change.value_id, name: status_change.value) : nil
|
|
783
|
+
[status, resolution]
|
|
784
|
+
end
|
|
785
|
+
|
|
789
786
|
def sprints
|
|
790
787
|
sprint_ids = []
|
|
791
788
|
|
|
@@ -806,13 +803,13 @@ class Issue
|
|
|
806
803
|
def compact_text text, max: 60
|
|
807
804
|
return '' if text.nil?
|
|
808
805
|
|
|
809
|
-
if text.is_a? Hash
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
806
|
+
text = if text.is_a? Hash
|
|
807
|
+
@board.project_config.atlassian_document_format.to_text(text)
|
|
808
|
+
else
|
|
809
|
+
text
|
|
810
|
+
end
|
|
811
|
+
text = text.gsub(/\s+/, ' ').strip
|
|
812
|
+
text = "#{text[0...max]}..." if text.length > max
|
|
816
813
|
text
|
|
817
814
|
end
|
|
818
815
|
|
|
@@ -823,6 +820,14 @@ class Issue
|
|
|
823
820
|
created = parse_time(history['created'])
|
|
824
821
|
|
|
825
822
|
history['items']&.each do |item|
|
|
823
|
+
if item['field'] == 'status' && item['to'].nil?
|
|
824
|
+
board.project_config.file_system.log(
|
|
825
|
+
"Issue #{key} has a status change without a 'to' id " \
|
|
826
|
+
"(from #{item['fromString'].inspect} to #{item['toString'].inspect}). Using id 0."
|
|
827
|
+
)
|
|
828
|
+
item = item.merge('to' => '0')
|
|
829
|
+
end
|
|
830
|
+
|
|
826
831
|
@changes << ChangeItem.new(raw: item, time: created, author_raw: history['author'])
|
|
827
832
|
end
|
|
828
833
|
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class IssuePrinter
|
|
4
|
+
def initialize issue
|
|
5
|
+
@issue = issue
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def to_s
|
|
9
|
+
issue = @issue
|
|
10
|
+
result = +''
|
|
11
|
+
result << "#{issue.key} (#{issue.type}): #{issue.compact_text issue.summary, max: 200}\n"
|
|
12
|
+
|
|
13
|
+
assignee = issue.raw['fields']['assignee']
|
|
14
|
+
result << " [assignee] #{assignee['name'].inspect} <#{assignee['emailAddress']}>\n" unless assignee.nil?
|
|
15
|
+
|
|
16
|
+
issue.raw['fields']['issuelinks']&.each do |link|
|
|
17
|
+
result << " [link] #{link['type']['outward']} #{link['outwardIssue']['key']}\n" if link['outwardIssue']
|
|
18
|
+
result << " [link] #{link['type']['inward']} #{link['inwardIssue']['key']}\n" if link['inwardIssue']
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
history = [] # time, type, detail
|
|
22
|
+
|
|
23
|
+
if issue.board.cycletime
|
|
24
|
+
started_at, stopped_at = issue.started_stopped_times
|
|
25
|
+
history << [started_at, nil, 'vvvv Started here vvvv', true] if started_at
|
|
26
|
+
history << [stopped_at, nil, '^^^^ Finished here ^^^^', true] if stopped_at
|
|
27
|
+
else
|
|
28
|
+
result << " Unable to determine start/end times as board #{issue.board.id} has no cycletime specified\n"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
issue.discarded_change_times&.each do |time|
|
|
32
|
+
history << [time, nil, '^^^^ Changes discarded ^^^^', true]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
(issue.changes + (issue.discarded_changes || [])).each do |change|
|
|
36
|
+
history << [change.time, change.field, create_change_message(change: change, issue: issue), change.artificial?]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
result << " History:\n"
|
|
40
|
+
type_width = history.collect { |_time, type, _detail, _artificial| type&.length || 0 }.max
|
|
41
|
+
sort_history!(history)
|
|
42
|
+
history.each do |time, type, detail, _artificial|
|
|
43
|
+
type = type.nil? ? '-' * type_width : type.rjust(type_width)
|
|
44
|
+
result << " #{time.strftime '%Y-%m-%d %H:%M:%S %z'} [#{type}] #{detail}\n"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
result
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def create_change_message change:, issue:
|
|
51
|
+
value, old_value = format_change_values(change: change, issue: issue)
|
|
52
|
+
|
|
53
|
+
message = +''
|
|
54
|
+
message << "#{old_value} -> " unless old_value.nil? || old_value.empty?
|
|
55
|
+
message << value
|
|
56
|
+
if change.artificial?
|
|
57
|
+
message << ' (Artificial entry)'
|
|
58
|
+
else
|
|
59
|
+
message << " (Author: #{change.author})"
|
|
60
|
+
end
|
|
61
|
+
message
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def format_change_values change:, issue:
|
|
65
|
+
if change.status?
|
|
66
|
+
value = "#{change.value.inspect}:#{change.value_id.inspect}"
|
|
67
|
+
old_value = change.old_value ? "#{change.old_value.inspect}:#{change.old_value_id.inspect}" : nil
|
|
68
|
+
elsif change.sprint?
|
|
69
|
+
added = change.value_id - change.old_value_id
|
|
70
|
+
removed = change.old_value_id - change.value_id
|
|
71
|
+
value = "#{change.value.inspect} #{change.value_id}"
|
|
72
|
+
value << " (added: #{added})" unless added.empty?
|
|
73
|
+
value << " (removed: #{removed})" unless removed.empty?
|
|
74
|
+
old_value = nil
|
|
75
|
+
else
|
|
76
|
+
value = issue.compact_text(change.value).inspect
|
|
77
|
+
old_value = change.old_value ? issue.compact_text(change.old_value).inspect : nil
|
|
78
|
+
end
|
|
79
|
+
[value, old_value]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def sort_history! history
|
|
83
|
+
history.sort! do |a, b|
|
|
84
|
+
if a[0] == b[0]
|
|
85
|
+
if a[1].nil?
|
|
86
|
+
1
|
|
87
|
+
elsif b[1].nil?
|
|
88
|
+
-1
|
|
89
|
+
else
|
|
90
|
+
a[1] <=> b[1]
|
|
91
|
+
end
|
|
92
|
+
else
|
|
93
|
+
a[0] <=> b[0]
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -28,9 +28,12 @@ class JiraGateway
|
|
|
28
28
|
|
|
29
29
|
stdout, stderr, status = capture3(command, stdin_data: stdin_data)
|
|
30
30
|
unless status.success?
|
|
31
|
-
@file_system.
|
|
32
|
-
@file_system.
|
|
33
|
-
@file_system.
|
|
31
|
+
@file_system.error "Failed call with exit status #{status.exitstatus}!"
|
|
32
|
+
@file_system.error "Returned (stdout): #{stdout.inspect}"
|
|
33
|
+
@file_system.error "Returned (stderr): #{stderr.inspect}"
|
|
34
|
+
if stderr.include?('401')
|
|
35
|
+
raise 'The request was not authorized. Verify that your authentication token hasn\'t expired'
|
|
36
|
+
end
|
|
34
37
|
raise "Failed call with exit status #{status.exitstatus}. " \
|
|
35
38
|
"See #{@file_system.logfile_name} for details"
|
|
36
39
|
end
|