tenseleyflow/shithub / 8ae944a

Browse files

Align repo action buttons

Authored by espadonne
SHA
8ae944ae448ed2ab21ebc58e30502aa0eb998a2b
Parents
598b09f
Tree
73fe7a3

10 changed files

StatusFile+-
M internal/web/handlers/repo/code.go 1 0
M internal/web/handlers/repo/issues.go 4 0
M internal/web/handlers/repo/labels_milestones.go 2 0
M internal/web/handlers/repo/repo.go 1 0
A internal/web/handlers/repo/repo_actions.go 108 0
M internal/web/handlers/repo/repo_test.go 17 0
M internal/web/handlers/repo/social.go 3 3
M internal/web/render/octicons.go 2 0
M internal/web/static/css/shithub.css 90 4
M internal/web/templates/_repo_header.html 66 3
internal/web/handlers/repo/code.gomodified
@@ -168,6 +168,7 @@ func (h *Handlers) renderRepoTree(w http.ResponseWriter, r *http.Request, cc *co
168168
 		"SSHEnabled":    h.d.CloneURLs.SSHEnabled,
169169
 		"SSHCloneURL":   h.cloneSSH(cc.owner, cc.row.Name),
170170
 		"RepoTopics":    topics,
171
+		"RepoActions":   h.repoActions(r, cc.row.ID),
171172
 		"RepoCounts":    h.subnavCounts(r.Context(), cc.row.ID, cc.row.ForkCount),
172173
 		"CanSettings":   h.canViewSettings(middleware.CurrentUserFromContext(r.Context())),
173174
 		"ActiveSubnav":  "code",
internal/web/handlers/repo/issues.gomodified
@@ -156,6 +156,7 @@ func (h *Handlers) issuesList(w http.ResponseWriter, r *http.Request) {
156156
 		"Page":         page,
157157
 		"PerPage":      perPage,
158158
 		"CSRFToken":    middleware.CSRFTokenForRequest(r),
159
+		"RepoActions":  h.repoActions(r, row.ID),
159160
 		"RepoCounts":   h.subnavCounts(r.Context(), row.ID, row.ForkCount),
160161
 		"CanSettings":  h.canViewSettings(middleware.CurrentUserFromContext(r.Context())),
161162
 		"ActiveSubnav": "issues",
@@ -176,6 +177,7 @@ func (h *Handlers) issueNewForm(w http.ResponseWriter, r *http.Request) {
176177
 		"Owner":        owner.Username,
177178
 		"Repo":         row,
178179
 		"CSRFToken":    middleware.CSRFTokenForRequest(r),
180
+		"RepoActions":  h.repoActions(r, row.ID),
179181
 		"RepoCounts":   h.subnavCounts(r.Context(), row.ID, row.ForkCount),
180182
 		"CanSettings":  h.canViewSettings(middleware.CurrentUserFromContext(r.Context())),
181183
 		"ActiveSubnav": "issues",
@@ -238,6 +240,7 @@ func (h *Handlers) renderIssueCreateError(w http.ResponseWriter, r *http.Request
238240
 		"FormBody":     body,
239241
 		"Error":        msg,
240242
 		"CSRFToken":    middleware.CSRFTokenForRequest(r),
243
+		"RepoActions":  h.repoActions(r, row.ID),
241244
 		"RepoCounts":   h.subnavCounts(r.Context(), row.ID, row.ForkCount),
242245
 		"CanSettings":  h.canViewSettings(middleware.CurrentUserFromContext(r.Context())),
243246
 		"ActiveSubnav": "issues",
@@ -349,6 +352,7 @@ func (h *Handlers) issueView(w http.ResponseWriter, r *http.Request) {
349352
 		"CanEditIssueMilestone": policy.Can(r.Context(), pdeps, actor, policy.ActionIssueLabel, repoRef).Allow,
350353
 		"CanLockIssue":          policy.Can(r.Context(), pdeps, actor, policy.ActionIssueClose, repoRef).Allow,
351354
 		"CSRFToken":             middleware.CSRFTokenForRequest(r),
355
+		"RepoActions":           h.repoActions(r, row.ID),
352356
 		"RepoCounts":            h.subnavCounts(r.Context(), row.ID, row.ForkCount),
353357
 		"CanSettings":           h.canViewSettings(viewer),
354358
 		"ActiveSubnav":          "issues",
internal/web/handlers/repo/labels_milestones.gomodified
@@ -35,6 +35,7 @@ func (h *Handlers) labelsList(w http.ResponseWriter, r *http.Request) {
3535
 		"Labels":         labels,
3636
 		"CanManageIssue": canManage,
3737
 		"CSRFToken":      middleware.CSRFTokenForRequest(r),
38
+		"RepoActions":    h.repoActions(r, row.ID),
3839
 		"RepoCounts":     h.subnavCounts(r.Context(), row.ID, row.ForkCount),
3940
 		"CanSettings":    h.canViewSettings(viewer),
4041
 		"ActiveSubnav":   "issues",
@@ -137,6 +138,7 @@ func (h *Handlers) milestonesList(w http.ResponseWriter, r *http.Request) {
137138
 		"Milestones":     ms,
138139
 		"CanManageIssue": canManage,
139140
 		"CSRFToken":      middleware.CSRFTokenForRequest(r),
141
+		"RepoActions":    h.repoActions(r, row.ID),
140142
 		"RepoCounts":     h.subnavCounts(r.Context(), row.ID, row.ForkCount),
141143
 		"CanSettings":    h.canViewSettings(viewer),
142144
 		"ActiveSubnav":   "issues",
internal/web/handlers/repo/repo.gomodified
@@ -367,6 +367,7 @@ func (h *Handlers) repoHome(w http.ResponseWriter, r *http.Request) {
367367
 		"HTTPSCloneURL": h.cloneHTTPS(owner, row.Name),
368368
 		"SSHEnabled":    h.d.CloneURLs.SSHEnabled,
369369
 		"SSHCloneURL":   h.cloneSSH(owner, row.Name),
370
+		"RepoActions":   h.repoActions(r, row.ID),
370371
 		"RepoCounts":    h.subnavCounts(r.Context(), row.ID, row.ForkCount),
371372
 		"CanSettings":   h.canViewSettings(middleware.CurrentUserFromContext(r.Context())),
372373
 		"ActiveSubnav":  "code",
internal/web/handlers/repo/repo_actions.goadded
@@ -0,0 +1,108 @@
1
+// SPDX-License-Identifier: AGPL-3.0-or-later
2
+
3
+package repo
4
+
5
+import (
6
+	"net/http"
7
+	"net/url"
8
+	"strings"
9
+
10
+	"github.com/tenseleyFlow/shithub/internal/social"
11
+	"github.com/tenseleyFlow/shithub/internal/web/middleware"
12
+)
13
+
14
+type repoActionView struct {
15
+	IsLoggedIn   bool
16
+	LoginURL     string
17
+	ReturnTo     string
18
+	Starred      bool
19
+	WatchLevel   string
20
+	WatchOptions []repoWatchOptionView
21
+}
22
+
23
+type repoWatchOptionView struct {
24
+	Level       string
25
+	Label       string
26
+	Description string
27
+	Checked     bool
28
+}
29
+
30
+func (h *Handlers) repoActions(r *http.Request, repoID int64) repoActionView {
31
+	viewer := middleware.CurrentUserFromContext(r.Context())
32
+	returnTo := r.URL.RequestURI()
33
+	if returnTo == "" {
34
+		returnTo = r.URL.Path
35
+	}
36
+	if returnTo == "" {
37
+		returnTo = "/"
38
+	}
39
+	out := repoActionView{
40
+		IsLoggedIn: !viewer.IsAnonymous(),
41
+		LoginURL:   "/login?next=" + url.QueryEscape(returnTo),
42
+		ReturnTo:   returnTo,
43
+		WatchLevel: string(social.WatchParticipating),
44
+	}
45
+	if viewer.IsAnonymous() {
46
+		out.WatchOptions = repoWatchOptions(social.WatchParticipating)
47
+		return out
48
+	}
49
+	deps := h.socialDeps()
50
+	starred, err := social.HasStar(r.Context(), deps, viewer.ID, repoID)
51
+	if err != nil {
52
+		h.d.Logger.WarnContext(r.Context(), "repo actions: star lookup", "error", err, "repo_id", repoID, "user_id", viewer.ID)
53
+	} else {
54
+		out.Starred = starred
55
+	}
56
+	level, err := social.CurrentLevel(r.Context(), deps, viewer.ID, repoID)
57
+	if err != nil {
58
+		h.d.Logger.WarnContext(r.Context(), "repo actions: watch lookup", "error", err, "repo_id", repoID, "user_id", viewer.ID)
59
+		level = social.WatchParticipating
60
+	}
61
+	out.WatchLevel = string(level)
62
+	out.WatchOptions = repoWatchOptions(level)
63
+	return out
64
+}
65
+
66
+func repoWatchOptions(current social.WatchLevel) []repoWatchOptionView {
67
+	return []repoWatchOptionView{
68
+		{
69
+			Level:       string(social.WatchParticipating),
70
+			Label:       "Participating and @mentions",
71
+			Description: "Only receive notifications from this repository when participating or mentioned.",
72
+			Checked:     current == social.WatchParticipating,
73
+		},
74
+		{
75
+			Level:       string(social.WatchAll),
76
+			Label:       "All Activity",
77
+			Description: "Notified of all notifications on this repository.",
78
+			Checked:     current == social.WatchAll,
79
+		},
80
+		{
81
+			Level:       string(social.WatchIgnore),
82
+			Label:       "Ignore",
83
+			Description: "Never notified.",
84
+			Checked:     current == social.WatchIgnore,
85
+		},
86
+	}
87
+}
88
+
89
+func redirectAfterRepoAction(w http.ResponseWriter, r *http.Request, fallback string) {
90
+	dest := fallback
91
+	if err := r.ParseForm(); err == nil {
92
+		if returnTo := strings.TrimSpace(r.PostFormValue("return_to")); safeLocalPath(returnTo) {
93
+			dest = returnTo
94
+		}
95
+	}
96
+	http.Redirect(w, r, dest, http.StatusSeeOther)
97
+}
98
+
99
+func safeLocalPath(path string) bool {
100
+	if path == "" || !strings.HasPrefix(path, "/") || strings.HasPrefix(path, "//") {
101
+		return false
102
+	}
103
+	u, err := url.Parse(path)
104
+	if err != nil {
105
+		return false
106
+	}
107
+	return !u.IsAbs() && u.Host == "" && strings.HasPrefix(u.Path, "/")
108
+}
internal/web/handlers/repo/repo_test.gomodified
@@ -149,6 +149,23 @@ func withViewer(req *http.Request, viewer middleware.CurrentUser) *http.Request
149149
 	return req.WithContext(middleware.WithCurrentUserForTest(req.Context(), viewer))
150150
 }
151151
 
152
+func TestSafeLocalPath(t *testing.T) {
153
+	t.Parallel()
154
+	tests := map[string]bool{
155
+		"/alice/repo":              true,
156
+		"/alice/repo/issues/1?x=1": true,
157
+		"":                         false,
158
+		"alice/repo":               false,
159
+		"//evil.example/path":      false,
160
+		"https://evil.example":     false,
161
+	}
162
+	for path, want := range tests {
163
+		if got := safeLocalPath(path); got != want {
164
+			t.Errorf("safeLocalPath(%q) = %v; want %v", path, got, want)
165
+		}
166
+	}
167
+}
168
+
152169
 // callLoad invokes loadRepoAndAuthorize via a test handler so we can
153170
 // exercise the chi URL-param plumbing the way the real router does.
154171
 // Returns (status, ok) — `ok` is what loadRepoAndAuthorize returned.
internal/web/handlers/repo/social.gomodified
@@ -58,7 +58,7 @@ func (h *Handlers) repoStar(w http.ResponseWriter, r *http.Request) {
5858
 		h.handleSocialError(w, r, err)
5959
 		return
6060
 	}
61
-	http.Redirect(w, r, "/"+owner+"/"+row.Name, http.StatusSeeOther)
61
+	redirectAfterRepoAction(w, r, "/"+owner+"/"+row.Name)
6262
 }
6363
 
6464
 // repoUnstar handles POST /{owner}/{repo}/unstar. Same shape as star;
@@ -74,7 +74,7 @@ func (h *Handlers) repoUnstar(w http.ResponseWriter, r *http.Request) {
7474
 		h.handleSocialError(w, r, err)
7575
 		return
7676
 	}
77
-	http.Redirect(w, r, "/"+owner+"/"+row.Name, http.StatusSeeOther)
77
+	redirectAfterRepoAction(w, r, "/"+owner+"/"+row.Name)
7878
 }
7979
 
8080
 // repoWatch handles POST /{owner}/{repo}/watch with a level form
@@ -102,7 +102,7 @@ func (h *Handlers) repoWatch(w http.ResponseWriter, r *http.Request) {
102102
 		h.handleSocialError(w, r, err)
103103
 		return
104104
 	}
105
-	http.Redirect(w, r, "/"+owner+"/"+row.Name, http.StatusSeeOther)
105
+	redirectAfterRepoAction(w, r, "/"+owner+"/"+row.Name)
106106
 }
107107
 
108108
 // stargazersList renders /{owner}/{repo}/stargazers. Read-public on
internal/web/render/octicons.gomodified
@@ -70,6 +70,8 @@ func BuiltinOcticons() OcticonResolver {
7070
 		// S29: notification bell for the top-bar inbox link.
7171
 		"bell": trustedSVG(`<svg xmlns="http://www.w3.org/2000/svg" ` + cls +
7272
 			`><path d="M8 16a2 2 0 0 0 1.985-1.75c.017-.137-.097-.25-.235-.25h-3.5c-.138 0-.252.113-.235.25A2 2 0 0 0 8 16zM3 5a5 5 0 0 1 10 0v2.947c0 .05.015.098.042.139l1.703 2.555A1.519 1.519 0 0 1 13.482 13H2.518a1.519 1.519 0 0 1-1.263-2.36l1.703-2.554A.255.255 0 0 0 3 7.947zm5-3.5A3.5 3.5 0 0 0 4.5 5v2.947c0 .346-.102.683-.294.97l-1.703 2.556a.018.018 0 0 0-.003.01.017.017 0 0 0 .002.012.017.017 0 0 0 .015.005h10.964a.017.017 0 0 0 .016-.005.018.018 0 0 0 0-.022l-1.703-2.555a1.749 1.749 0 0 1-.294-.97V5A3.5 3.5 0 0 0 8 1.5z"/></svg>`),
73
+		"triangle-down": trustedSVG(`<svg xmlns="http://www.w3.org/2000/svg" ` + cls +
74
+			`><path d="m4.427 7.427 3.396 3.396a.25.25 0 0 0 .354 0l3.396-3.396A.25.25 0 0 0 11.396 7H4.604a.25.25 0 0 0-.177.427Z"/></svg>`),
7375
 		// Repo "Code" dropdown chrome (clone widget).
7476
 		"download": trustedSVG(`<svg xmlns="http://www.w3.org/2000/svg" ` + cls +
7577
 			`><path d="M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14zm5.78-2.92a.749.749 0 0 1-1.06 0L3.72 7.78a.749.749 0 1 1 1.06-1.06L7.25 9.19V1.75a.75.75 0 0 1 1.5 0v7.44l2.47-2.47a.749.749 0 1 1 1.06 1.06z"/></svg>`),
internal/web/static/css/shithub.cssmodified
@@ -1125,23 +1125,109 @@ code {
11251125
 .shithub-repo-page-title .shithub-repo-name { font-weight: 600; }
11261126
 .shithub-repo-title-icon { color: var(--fg-muted); display: inline-flex; align-items: center; }
11271127
 .shithub-repo-actions { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
1128
+.shithub-repo-action-form { margin: 0; }
11281129
 .shithub-repo-action {
11291130
   display: inline-flex;
11301131
   align-items: center;
11311132
   gap: 0.35rem;
1132
-  padding: 0.35rem 0.7rem;
1133
+  min-height: 32px;
1134
+  padding: 0 0.7rem;
11331135
   border: 1px solid var(--border-default);
11341136
   border-radius: 6px;
11351137
   color: var(--fg-default);
11361138
   background: var(--canvas-subtle);
11371139
   font-size: 0.875rem;
11381140
   font-weight: 600;
1141
+  line-height: 20px;
1142
+  white-space: nowrap;
1143
+  cursor: pointer;
1144
+}
1145
+.shithub-repo-action:hover { text-decoration: none; background: var(--canvas-inset); }
1146
+button.shithub-repo-action {
1147
+  font-family: inherit;
1148
+}
1149
+.shithub-repo-action.is-active {
1150
+  color: var(--accent-fg);
11391151
 }
1140
-.shithub-repo-action span {
1152
+.shithub-repo-action-label { color: inherit; }
1153
+.shithub-repo-action .shithub-counter {
11411154
   color: var(--fg-muted);
11421155
   font-weight: 600;
1143
-  padding-left: 0.35rem;
1144
-  border-left: 1px solid var(--border-default);
1156
+  font-size: 0.75rem;
1157
+  padding: 0 0.35rem;
1158
+  min-width: 1.25rem;
1159
+  border-radius: 999px;
1160
+  background: rgba(110, 118, 129, 0.18);
1161
+}
1162
+.shithub-repo-action-menu { position: relative; }
1163
+.shithub-repo-action-menu > summary { list-style: none; }
1164
+.shithub-repo-action-menu > summary::-webkit-details-marker { display: none; }
1165
+.shithub-repo-action-button svg:last-child {
1166
+  width: 12px;
1167
+  height: 12px;
1168
+  color: var(--fg-muted);
1169
+}
1170
+.shithub-repo-action-popover {
1171
+  position: absolute;
1172
+  right: 0;
1173
+  top: calc(100% + 0.4rem);
1174
+  z-index: 20;
1175
+  width: min(340px, calc(100vw - 2rem));
1176
+  padding: 0.5rem 0;
1177
+  border: 1px solid var(--border-default);
1178
+  border-radius: 8px;
1179
+  background: var(--canvas-default);
1180
+  box-shadow: 0 16px 32px rgba(1, 4, 9, 0.2);
1181
+}
1182
+.shithub-repo-action-popover strong {
1183
+  display: block;
1184
+  padding: 0.45rem 0.85rem 0.55rem;
1185
+  border-bottom: 1px solid var(--border-muted);
1186
+}
1187
+.shithub-repo-action-option-form { margin: 0; }
1188
+.shithub-repo-action-option {
1189
+  width: 100%;
1190
+  display: grid;
1191
+  grid-template-columns: 16px minmax(0, 1fr);
1192
+  gap: 0.6rem;
1193
+  padding: 0.65rem 0.85rem;
1194
+  border: 0;
1195
+  border-bottom: 1px solid var(--border-muted);
1196
+  background: transparent;
1197
+  color: var(--fg-default);
1198
+  font: inherit;
1199
+  text-align: left;
1200
+  cursor: pointer;
1201
+}
1202
+.shithub-repo-action-option:hover { background: var(--canvas-subtle); }
1203
+.shithub-repo-action-radio {
1204
+  width: 14px;
1205
+  height: 14px;
1206
+  margin-top: 0.25rem;
1207
+  border: 1px solid var(--border-default);
1208
+  border-radius: 50%;
1209
+}
1210
+.shithub-repo-action-option.is-selected .shithub-repo-action-radio {
1211
+  border-color: var(--accent-emphasis);
1212
+  box-shadow: inset 0 0 0 3px var(--canvas-default);
1213
+  background: var(--accent-emphasis);
1214
+}
1215
+.shithub-repo-action-option-title {
1216
+  display: block;
1217
+  font-weight: 600;
1218
+}
1219
+.shithub-repo-action-option-description {
1220
+  display: block;
1221
+  margin-top: 0.15rem;
1222
+  color: var(--fg-muted);
1223
+  font-size: 0.78rem;
1224
+  line-height: 1.35;
1225
+}
1226
+.shithub-repo-action-popover-link {
1227
+  display: block;
1228
+  padding: 0.55rem 0.85rem 0.35rem;
1229
+  color: var(--fg-muted);
1230
+  font-size: 0.82rem;
11451231
 }
11461232
 .shithub-repo-subnav {
11471233
   display: flex;
internal/web/templates/_repo_header.htmlmodified
@@ -8,10 +8,73 @@
88
       <a href="/{{ .Owner }}/{{ .Repo.Name }}" class="shithub-repo-name">{{ .Repo.Name }}</a>
99
       {{ if eq (printf "%s" .Repo.Visibility) "private" }}<span class="shithub-pill shithub-pill-private">Private</span>{{ else }}<span class="shithub-pill">Public</span>{{ end }}
1010
     </h1>
11
+    {{ $root := . }}
1112
     <div class="shithub-repo-actions" aria-label="Repository actions">
12
-      <a href="/{{ .Owner }}/{{ .Repo.Name }}/watchers" class="shithub-repo-action">{{ octicon "eye" }} Watch <span>{{ .Repo.WatcherCount }}</span></a>
13
-      <a href="/{{ .Owner }}/{{ .Repo.Name }}/forks" class="shithub-repo-action">{{ octicon "repo-forked" }} Fork <span>{{ .Repo.ForkCount }}</span></a>
14
-      <a href="/{{ .Owner }}/{{ .Repo.Name }}/stargazers" class="shithub-repo-action">{{ octicon "star" }} Star <span>{{ .Repo.StarCount }}</span></a>
13
+      {{ with .RepoActions }}
14
+      {{ if .IsLoggedIn }}
15
+      <details class="shithub-repo-action-menu">
16
+        <summary class="shithub-repo-action shithub-repo-action-button" aria-label="Notification settings">
17
+          {{ octicon "bell" }}
18
+          <span class="shithub-repo-action-label">Notifications</span>
19
+          {{ octicon "triangle-down" }}
20
+        </summary>
21
+        <div class="shithub-repo-action-popover" role="menu" aria-label="Notification settings">
22
+          <strong>Notifications</strong>
23
+          {{ range .WatchOptions }}
24
+          <form method="post" action="/{{ $root.Owner }}/{{ $root.Repo.Name }}/watch" class="shithub-repo-action-option-form">
25
+            <input type="hidden" name="csrf_token" value="{{ $root.CSRFToken }}">
26
+            <input type="hidden" name="level" value="{{ .Level }}">
27
+            <input type="hidden" name="return_to" value="{{ $root.RepoActions.ReturnTo }}">
28
+            <button type="submit" class="shithub-repo-action-option{{ if .Checked }} is-selected{{ end }}" role="menuitemradio" aria-checked="{{ .Checked }}">
29
+              <span class="shithub-repo-action-radio" aria-hidden="true"></span>
30
+              <span>
31
+                <span class="shithub-repo-action-option-title">{{ .Label }}</span>
32
+                <span class="shithub-repo-action-option-description">{{ .Description }}</span>
33
+              </span>
34
+            </button>
35
+          </form>
36
+          {{ end }}
37
+          <a href="/{{ $root.Owner }}/{{ $root.Repo.Name }}/watchers" class="shithub-repo-action-popover-link">{{ $root.Repo.WatcherCount }} watcher{{ if ne $root.Repo.WatcherCount 1 }}s{{ end }}</a>
38
+        </div>
39
+      </details>
40
+      <form method="post" action="/{{ $root.Owner }}/{{ $root.Repo.Name }}/fork" class="shithub-repo-action-form">
41
+        <input type="hidden" name="csrf_token" value="{{ $root.CSRFToken }}">
42
+        <button type="submit" class="shithub-repo-action">
43
+          {{ octicon "repo-forked" }}
44
+          <span class="shithub-repo-action-label">Fork</span>
45
+          <span class="shithub-counter">{{ $root.Repo.ForkCount }}</span>
46
+        </button>
47
+      </form>
48
+      <form method="post" action="/{{ $root.Owner }}/{{ $root.Repo.Name }}/{{ if .Starred }}unstar{{ else }}star{{ end }}" class="shithub-repo-action-form">
49
+        <input type="hidden" name="csrf_token" value="{{ $root.CSRFToken }}">
50
+        <input type="hidden" name="return_to" value="{{ .ReturnTo }}">
51
+        <button type="submit" class="shithub-repo-action{{ if .Starred }} is-active{{ end }}" aria-pressed="{{ .Starred }}">
52
+          {{ octicon "star" }}
53
+          <span class="shithub-repo-action-label">{{ if .Starred }}Starred{{ else }}Star{{ end }}</span>
54
+          <span class="shithub-counter">{{ $root.Repo.StarCount }}</span>
55
+        </button>
56
+      </form>
57
+      {{ else }}
58
+      <a href="{{ .LoginURL }}" class="shithub-repo-action" aria-label="You must be signed in to change notification settings">
59
+        {{ octicon "bell" }}
60
+        <span class="shithub-repo-action-label">Notifications</span>
61
+      </a>
62
+      <a href="{{ .LoginURL }}" class="shithub-repo-action" aria-label="You must be signed in to fork a repository">
63
+        {{ octicon "repo-forked" }}
64
+        <span class="shithub-repo-action-label">Fork</span>
65
+        <span class="shithub-counter">{{ $root.Repo.ForkCount }}</span>
66
+      </a>
67
+      <a href="{{ .LoginURL }}" class="shithub-repo-action" aria-label="You must be signed in to star a repository">
68
+        {{ octicon "star" }}
69
+        <span class="shithub-repo-action-label">Star</span>
70
+        <span class="shithub-counter">{{ $root.Repo.StarCount }}</span>
71
+      </a>
72
+      {{ end }}
73
+      {{ else }}
74
+      <a href="/{{ .Owner }}/{{ .Repo.Name }}/watchers" class="shithub-repo-action">{{ octicon "eye" }} Watch <span class="shithub-counter">{{ .Repo.WatcherCount }}</span></a>
75
+      <a href="/{{ .Owner }}/{{ .Repo.Name }}/forks" class="shithub-repo-action">{{ octicon "repo-forked" }} Fork <span class="shithub-counter">{{ .Repo.ForkCount }}</span></a>
76
+      <a href="/{{ .Owner }}/{{ .Repo.Name }}/stargazers" class="shithub-repo-action">{{ octicon "star" }} Star <span class="shithub-counter">{{ .Repo.StarCount }}</span></a>
77
+      {{ end }}
1578
     </div>
1679
   </div>
1780
   {{ template "repo-subnav" . }}