tenseleyflow/shithub / db1998e

Browse files

repo: show head check status on code tab

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
db1998e004eed8db8d4202f05803c85483f53ef7
Parents
be95514
Tree
073a810

5 changed files

StatusFile+-
M internal/web/handlers/repo/code.go 5 0
A internal/web/handlers/repo/code_checks.go 93 0
A internal/web/handlers/repo/code_checks_test.go 71 0
M internal/web/static/css/shithub.css 15 0
M internal/web/templates/repo/tree.html 3 0
internal/web/handlers/repo/code.gomodified
@@ -220,6 +220,10 @@ func (h *Handlers) renderRepoTree(w http.ResponseWriter, r *http.Request, cc *co
220220
 	if headFound {
221221
 		headAuthor = identity.New(h.d.Pool).Resolve(r.Context(), head.AuthorEmail)
222222
 	}
223
+	headCheckSummary := codeCommitCheckSummary{}
224
+	if headFound {
225
+		headCheckSummary = h.codeCommitCheckSummary(r.Context(), cc.owner, cc.row.Name, cc.row.ID, head.OID)
226
+	}
223227
 	commitCount, countErr := repogit.CountCommits(r.Context(), cc.gitDir, cc.ref)
224228
 	if countErr != nil {
225229
 		h.d.Logger.WarnContext(r.Context(), "code: CountCommits", "error", countErr)
@@ -251,6 +255,7 @@ func (h *Handlers) renderRepoTree(w http.ResponseWriter, r *http.Request, cc *co
251255
 		"Head":          head,
252256
 		"HeadFound":     headFound,
253257
 		"HeadAuthor":    headAuthor,
258
+		"HeadChecks":    headCheckSummary,
254259
 		"BranchCompare": codeBranchCompareData(r.Context(), cc),
255260
 		"CommitCount":   commitCount,
256261
 		"README":        template.HTML(readme.HTML), //nolint:gosec // sanitized by mdrender
internal/web/handlers/repo/code_checks.goadded
@@ -0,0 +1,93 @@
1
+// SPDX-License-Identifier: AGPL-3.0-or-later
2
+
3
+package repo
4
+
5
+import (
6
+	"context"
7
+	"fmt"
8
+
9
+	checksdb "github.com/tenseleyFlow/shithub/internal/checks/sqlc"
10
+)
11
+
12
+type codeCommitCheckSummary struct {
13
+	Show       bool
14
+	Label      string
15
+	StateClass string
16
+	StateIcon  string
17
+	Href       string
18
+}
19
+
20
+func (h *Handlers) codeCommitCheckSummary(ctx context.Context, owner, repoName string, repoID int64, headSHA string) codeCommitCheckSummary {
21
+	runs, err := h.cq.ListCheckRunsForCommit(ctx, h.d.Pool, checksdb.ListCheckRunsForCommitParams{
22
+		RepoID:  repoID,
23
+		HeadSha: headSHA,
24
+	})
25
+	if err != nil {
26
+		h.d.Logger.WarnContext(ctx, "code: ListCheckRunsForCommit", "repo_id", repoID, "head_sha", headSHA, "error", err)
27
+		return codeCommitCheckSummary{}
28
+	}
29
+	summary := summarizeCodeCommitChecks(runs)
30
+	if !summary.Show {
31
+		return summary
32
+	}
33
+	summary.Href = fmt.Sprintf("/%s/%s/actions", owner, repoName)
34
+	return summary
35
+}
36
+
37
+func summarizeCodeCommitChecks(runs []checksdb.CheckRun) codeCommitCheckSummary {
38
+	if len(runs) == 0 {
39
+		return codeCommitCheckSummary{}
40
+	}
41
+	total := len(runs)
42
+	pending := 0
43
+	failing := 0
44
+	for _, run := range runs {
45
+		if run.Status != checksdb.CheckStatusCompleted {
46
+			pending++
47
+			continue
48
+		}
49
+		if !run.Conclusion.Valid {
50
+			pending++
51
+			continue
52
+		}
53
+		switch run.Conclusion.CheckConclusion {
54
+		case checksdb.CheckConclusionSuccess, checksdb.CheckConclusionSkipped, checksdb.CheckConclusionNeutral:
55
+		default:
56
+			failing++
57
+		}
58
+	}
59
+	switch {
60
+	case failing > 0:
61
+		return codeCommitCheckSummary{
62
+			Show:       true,
63
+			Label:      checkSummaryLabel(failing, total, "failed"),
64
+			StateClass: "failure",
65
+			StateIcon:  "x-circle-fill",
66
+		}
67
+	case pending > 0:
68
+		return codeCommitCheckSummary{
69
+			Show:       true,
70
+			Label:      checkSummaryLabel(pending, total, "pending"),
71
+			StateClass: "pending",
72
+			StateIcon:  "dot-fill",
73
+		}
74
+	default:
75
+		return codeCommitCheckSummary{
76
+			Show:       true,
77
+			Label:      checkSummaryLabel(total, total, "successful"),
78
+			StateClass: "success",
79
+			StateIcon:  "check-circle-fill",
80
+		}
81
+	}
82
+}
83
+
84
+func checkSummaryLabel(count, total int, state string) string {
85
+	checkWord := "checks"
86
+	if total == 1 {
87
+		checkWord = "check"
88
+	}
89
+	if count == total {
90
+		return fmt.Sprintf("%d %s %s", total, checkWord, state)
91
+	}
92
+	return fmt.Sprintf("%d of %d checks %s", count, total, state)
93
+}
internal/web/handlers/repo/code_checks_test.goadded
@@ -0,0 +1,71 @@
1
+// SPDX-License-Identifier: AGPL-3.0-or-later
2
+
3
+package repo
4
+
5
+import (
6
+	"testing"
7
+
8
+	checksdb "github.com/tenseleyFlow/shithub/internal/checks/sqlc"
9
+)
10
+
11
+func TestSummarizeCodeCommitChecks(t *testing.T) {
12
+	tests := []struct {
13
+		name       string
14
+		runs       []checksdb.CheckRun
15
+		show       bool
16
+		stateClass string
17
+		label      string
18
+	}{
19
+		{
20
+			name: "empty",
21
+			show: false,
22
+		},
23
+		{
24
+			name: "success",
25
+			runs: []checksdb.CheckRun{
26
+				completedCheck(checksdb.CheckConclusionSuccess),
27
+			},
28
+			show:       true,
29
+			stateClass: "success",
30
+			label:      "1 check successful",
31
+		},
32
+		{
33
+			name: "pending wins over success",
34
+			runs: []checksdb.CheckRun{
35
+				completedCheck(checksdb.CheckConclusionSuccess),
36
+				{Status: checksdb.CheckStatusInProgress},
37
+			},
38
+			show:       true,
39
+			stateClass: "pending",
40
+			label:      "1 of 2 checks pending",
41
+		},
42
+		{
43
+			name: "failure wins over pending",
44
+			runs: []checksdb.CheckRun{
45
+				completedCheck(checksdb.CheckConclusionFailure),
46
+				{Status: checksdb.CheckStatusQueued},
47
+			},
48
+			show:       true,
49
+			stateClass: "failure",
50
+			label:      "1 of 2 checks failed",
51
+		},
52
+	}
53
+	for _, tt := range tests {
54
+		t.Run(tt.name, func(t *testing.T) {
55
+			got := summarizeCodeCommitChecks(tt.runs)
56
+			if got.Show != tt.show || got.StateClass != tt.stateClass || got.Label != tt.label {
57
+				t.Fatalf("summary: got show=%t state=%q label=%q", got.Show, got.StateClass, got.Label)
58
+			}
59
+		})
60
+	}
61
+}
62
+
63
+func completedCheck(conclusion checksdb.CheckConclusion) checksdb.CheckRun {
64
+	return checksdb.CheckRun{
65
+		Status: checksdb.CheckStatusCompleted,
66
+		Conclusion: checksdb.NullCheckConclusion{
67
+			CheckConclusion: conclusion,
68
+			Valid:           true,
69
+		},
70
+	}
71
+}
internal/web/static/css/shithub.cssmodified
@@ -6015,6 +6015,21 @@ button.shithub-repo-action {
60156015
 .shithub-tree-commit-meta a {
60166016
   color: var(--fg-muted);
60176017
 }
6018
+.shithub-tree-commit-meta .shithub-tree-check-status {
6019
+  color: currentColor;
6020
+}
6021
+.shithub-tree-check-status {
6022
+  display: inline-flex;
6023
+  align-items: center;
6024
+  justify-content: center;
6025
+  width: 1.25rem;
6026
+  height: 1.25rem;
6027
+  border-radius: 999px;
6028
+  text-decoration: none;
6029
+}
6030
+.shithub-tree-check-status:hover {
6031
+  background: var(--button-default-hover-bg);
6032
+}
60186033
 .shithub-tree-commit-count {
60196034
   display: inline-flex;
60206035
   align-items: center;
internal/web/templates/repo/tree.htmlmodified
@@ -128,6 +128,9 @@
128128
               <a href="/{{ .Owner }}/{{ .Repo.Name }}/commit/{{ .Head.OID }}" class="shithub-tree-commit-subject">{{ .Head.Subject }}</a>
129129
             </div>
130130
             <div class="shithub-tree-commit-meta">
131
+              {{ if .HeadChecks.Show }}
132
+              <a href="{{ .HeadChecks.Href }}" class="shithub-tree-check-status shithub-actions-state-{{ .HeadChecks.StateClass }}" aria-label="{{ .HeadChecks.Label }}" title="{{ .HeadChecks.Label }}">{{ octicon .HeadChecks.StateIcon }}</a>
133
+              {{ end }}
131134
               <a href="/{{ .Owner }}/{{ .Repo.Name }}/commit/{{ .Head.OID }}"><code title="{{ .Head.OID }}">{{ slice .Head.OID 0 7 }}</code></a>
132135
               <time datetime="{{ .Head.AuthorWhen.Format "2006-01-02T15:04:05Z" }}">{{ relativeTime .Head.AuthorWhen }}</time>
133136
               <a href="/{{ .Owner }}/{{ .Repo.Name }}/commits/{{ .Ref }}" class="shithub-tree-commit-count">{{ octicon "history" }} {{ .CommitCount }} Commits</a>