cli_issue_history.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 3

Tags 45

↑↓ moveenter openesc close