1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package app
4
5import (
6 "fmt"
7 "io"
8 "slices"
9 "strings"
10
11 "bnema.dev/ermite-api/gen/ermite"
12 "google.golang.org/protobuf/types/known/timestamppb"
13)
14
15const jsonIssueEventHelp = `
16JSON fields of a history event: id, kind (referenced, closed, reopened, resolved), issue,
17merge_request (0 for none), merge_request_title, closes, actor, created_at (RFC 3339 UTC).`
18
19type jsonIssueEvent struct {
20 ID string `json:"id"`
21 Kind string `json:"kind"`
22 Issue int64 `json:"issue"`
23 MergeRequest int64 `json:"merge_request"`
24 MergeRequestTitle string `json:"merge_request_title"`
25 Closes bool `json:"closes"`
26 Actor string `json:"actor"`
27 CreatedAt *string `json:"created_at"`
28}
29
30func toJSONIssueEvents(events []*ermite.IssueEvent) []jsonIssueEvent {
31 out := make([]jsonIssueEvent, 0, len(events))
32 for _, e := range events {
33 out = append(out, jsonIssueEvent{ID: e.GetId(), Kind: strings.ToLower(strings.TrimPrefix(e.GetKind().String(), "ISSUE_EVENT_KIND_")), Issue: e.GetIssue(), MergeRequest: e.GetMergeRequest(), MergeRequestTitle: e.GetMergeRequestTitle(), Closes: e.GetCloses(), Actor: e.GetActor(), CreatedAt: jsonTime(e.GetCreatedAt())})
34 }
35 return out
36}
37
38func issueHistoryText(e *ermite.IssueEvent, mrSide bool) string {
39 subject := "this"
40 if mrSide {
41 subject = fmt.Sprintf("#%d", e.GetIssue())
42 }
43 var text string
44 switch e.GetKind() {
45 case ermite.IssueEventKind_ISSUE_EVENT_KIND_REFERENCED:
46 text = fmt.Sprintf("referenced %s in !%d", subject, e.GetMergeRequest())
47 if e.GetCloses() {
48 text += " (will resolve)"
49 }
50 case ermite.IssueEventKind_ISSUE_EVENT_KIND_RESOLVED:
51 text = "resolved " + subject
52 if e.GetMergeRequest() > 0 {
53 text += fmt.Sprintf(" by merging !%d", e.GetMergeRequest())
54 }
55 case ermite.IssueEventKind_ISSUE_EVENT_KIND_CLOSED:
56 text = "closed " + subject
57 case ermite.IssueEventKind_ISSUE_EVENT_KIND_REOPENED:
58 text = "reopened " + subject
59 default:
60 text = "updated " + subject
61 }
62 return clean(e.GetActor()) + " " + text
63}
64
65type issueTimelineRow struct {
66 at *timestamppb.Timestamp
67 print func(io.Writer) error
68}
69
70func newIssueTimeline(events []*ermite.IssueEvent, mrSide bool) []issueTimelineRow {
71 rows := make([]issueTimelineRow, 0, len(events))
72 for _, e := range events {
73 rows = append(rows, issueTimelineRow{at: e.GetCreatedAt(), print: func(w io.Writer) error {
74 _, err := fmt.Fprintf(w, " → %s %s\n", shortTime(e.GetCreatedAt(), "-"), issueHistoryText(e, mrSide))
75 return err
76 }})
77 }
78 return rows
79}
80
81func printIssueTimeline(w io.Writer, rows []issueTimelineRow) error {
82 // Stable sorting puts comments before events at the same instant.
83 slices.SortStableFunc(rows, func(a, b issueTimelineRow) int {
84 if n := a.at.AsTime().Compare(b.at.AsTime()); n != 0 {
85 return n
86 }
87 return 0
88 })
89 for _, row := range rows {
90 if err := row.print(w); err != nil {
91 return err
92 }
93 }
94 return nil
95}
96
97func mrIssueStateName(s ermite.MergeRequestIssueState) string {
98 switch s {
99 case ermite.MergeRequestIssueState_MERGE_REQUEST_ISSUE_STATE_OPEN:
100 return stateOpen
101 case ermite.MergeRequestIssueState_MERGE_REQUEST_ISSUE_STATE_CLOSED:
102 return stateClosed
103 case ermite.MergeRequestIssueState_MERGE_REQUEST_ISSUE_STATE_RESOLVED:
104 return stateResolved
105 default:
106 return "unknown"
107 }
108}