A CLI-first git forge.

cli forge git self-hosted

https://gitbay.org

Commit a733a81313

a733a81313bf75b49fc786e6ff21cbfb58d70bae

parent: 35cb93f6f1

Verified · cmc

cmc <hello@cleberg.net> · 2026-08-24T23:19:56Z

web: pinning, org repos, labels, linked authors and parents

Part of the #10 review round. Repo pages get a pin/unpin toggle for
the logged-in viewer (POST pin route). The new-repository form offers
an owner picker covering organizations you admin, applying the same
rule as repo create. The new-issue form takes space-separated labels
(applied with write access, matching the SSH rule); label chips on
issue lists and pages link to a ?label= filtered view and topic chips
already link filtered explore. Usernames across issue/MR pages, list
rows, and the dashboard link to profiles; commit pages link their
parent commits.
e2e/design_test.go +100
@@ -1,6 +1,8 @@
11 package e2e
22
33 import (
4 "encoding/json"
5 "net/url"
46 "os"
57 "path/filepath"
68 "strings"
@@ -64,3 +66,101 @@ func TestReadmeRelativeLinks(t *testing.T) {
6466 }
6567 }
6668 }
69
70func TestWebInteractions(t *testing.T) {
71 inst := startInstanceWith(t, "[web]\nmode = \"accounts\"\n")
72 aliceKey := inst.newKey(t, "alice")
73 inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub", "--email", "alice@example.test", "--verified")
74 if _, _, code := inst.ssh(t, aliceKey, "", "org", "create", "theorg"); code != 0 {
75 t.Fatal("org create failed")
76 }
77
78 out, _, code := inst.ssh(t, aliceKey, "", "web", "login", "--json")
79 if code != 0 {
80 t.Fatal("web login failed")
81 }
82 var env2 struct {
83 Data struct {
84 URL string `json:"url"`
85 } `json:"data"`
86 }
87 json.Unmarshal([]byte(out), &env2)
88 browser := newBrowser(t)
89 browserGet(t, browser, inst.base()+env2.Data.URL[strings.Index(env2.Data.URL, "/login"):])
90
91 // Create a repo under the org through the web form.
92 if status, _ := browserPost(t, browser, inst.base()+"/new",
93 url.Values{"owner": {"theorg"}, "name": {"webborn"}, "visibility": {"public"}}); status != 200 {
94 t.Fatalf("org repo via web: %d", status)
95 }
96 if out, _, code := inst.ssh(t, aliceKey, "", "repo", "show", "theorg/webborn"); code != 0 {
97 t.Fatalf("org repo missing: %s", out)
98 }
99
100 // Pin from the web; the repo header reflects it and the dashboard
101 // lists it; a second toggle unpins.
102 if status, _ := browserPost(t, browser, inst.base()+"/theorg/webborn/pin", url.Values{}); status != 200 {
103 t.Fatal("pin toggle failed")
104 }
105 _, body := browserGet(t, browser, inst.base()+"/theorg/webborn")
106 if !strings.Contains(body, "★ pinned") {
107 t.Fatal("repo header not pinned")
108 }
109 if _, body = browserGet(t, browser, inst.base()+"/"); !strings.Contains(body, "theorg<span") {
110 t.Fatal("dashboard missing pinned repo")
111 }
112 browserPost(t, browser, inst.base()+"/theorg/webborn/pin", url.Values{})
113 if _, body = browserGet(t, browser, inst.base()+"/theorg/webborn"); !strings.Contains(body, "☆ pin") {
114 t.Fatal("unpin failed")
115 }
116
117 // New issue with labels through the form; label chips filter.
118 if status, _ := browserPost(t, browser, inst.base()+"/theorg/webborn/issues/new",
119 url.Values{"title": {"styled"}, "body": {"b"}, "labels": {"bug ui"}}); status != 200 {
120 t.Fatal("issue via web failed")
121 }
122 if status, _ := browserPost(t, browser, inst.base()+"/theorg/webborn/issues/new",
123 url.Values{"title": {"plain"}}); status != 200 {
124 t.Fatal("second issue failed")
125 }
126 _, body = browserGet(t, browser, inst.base()+"/theorg/webborn/issues?label=bug")
127 if !strings.Contains(body, "styled") || strings.Contains(body, ">plain<") {
128 t.Fatalf("label filter wrong:\n%s", body)
129 }
130 _, body = browserGet(t, browser, inst.base()+"/theorg/webborn/issues/1")
131 if !strings.Contains(body, `href="/theorg/webborn/issues?label=bug"`) ||
132 !strings.Contains(body, `href="/alice">alice</a>`) {
133 t.Fatal("issue page chips/author not linked")
134 }
135}
136
137func TestCommitParentLinks(t *testing.T) {
138 inst := startInstance(t)
139 aliceKey := inst.newKey(t, "alice")
140 inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
141 if _, _, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/app"); code != 0 {
142 t.Fatal("repo create failed")
143 }
144 work := t.TempDir()
145 env := inst.gitEnv(aliceKey)
146 mustGit(t, work, env, "clone", inst.sshURL("alice/app"), "w")
147 dir := filepath.Join(work, "w")
148 os.WriteFile(filepath.Join(dir, "a.txt"), []byte("a\n"), 0o644)
149 mustGit(t, dir, env, "checkout", "-q", "-b", "main")
150 mustGit(t, dir, env, "add", ".")
151 mustGit(t, dir, env, "commit", "-q", "-m", "first")
152 first := strings.TrimSpace(mustGit(t, dir, env, "rev-parse", "HEAD"))
153 os.WriteFile(filepath.Join(dir, "a.txt"), []byte("b\n"), 0o644)
154 mustGit(t, dir, env, "commit", "-qam", "second")
155 head := strings.TrimSpace(mustGit(t, dir, env, "rev-parse", "HEAD"))
156 mustGit(t, dir, env, "push", "-q", "origin", "main")
157
158 _, body := inst.get(t, "/alice/app/commit/"+head)
159 if !strings.Contains(body, `href="/alice/app/commit/`+first+`"`) {
160 t.Fatal("parent commit not linked")
161 }
162 _, body = inst.get(t, "/alice/app/commit/"+first)
163 if strings.Contains(body, ">parent") {
164 t.Fatal("root commit shows a parent")
165 }
166}
internal/gitutil/messages.go +9
@@ -8,6 +8,15 @@ import (
88
99 const zeroSHA = "0000000000000000000000000000000000000000"
1010
11// Parents returns a commit's parent shas.
12func Parents(dir, sha string) []string {
13 out, err := exec.Command("git", "-C", dir, "log", "-1", "--format=%P", sha).Output()
14 if err != nil {
15 return nil
16 }
17 return strings.Fields(string(out))
18}
19
1120 // LastCommitDate returns the committer date (YYYY-MM-DD) of the ref tip,
1221 // or "" for empty repos.
1322 func LastCommitDate(dir, ref string) string {
internal/httpd/accounts.go +68 −12
@@ -104,12 +104,30 @@ func (s *Server) logout(w http.ResponseWriter, r *http.Request) {
104104 http.Redirect(w, r, "/", http.StatusSeeOther)
105105 }
106106
107func (s *Server) newRepoForm(w http.ResponseWriter, r *http.Request, u store.User) {
107// adminOrgs lists organizations the user administers, for owner pickers.
108func (s *Server) adminOrgs(u store.User) []string {
109 var out []string
110 if orgs, err := s.st.ListOrgsForUser(u.ID); err == nil {
111 for _, o := range orgs {
112 if o.Role == "admin" {
113 out = append(out, o.Username)
114 }
115 }
116 }
117 return out
118}
119
120func (s *Server) renderNewRepo(w http.ResponseWriter, u store.User, errMsg string) {
108121 s.render(w, "new.html", struct {
109122 Site string
110123 Viewer string
124 Orgs []string
111125 Error string
112 }{s.siteName(), u.Username, ""})
126 }{s.siteName(), u.Username, s.adminOrgs(u), errMsg})
127}
128
129func (s *Server) newRepoForm(w http.ResponseWriter, r *http.Request, u store.User) {
130 s.renderNewRepo(w, u, "")
113131 }
114132
115133 func (s *Server) newRepoSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
@@ -118,29 +136,56 @@ func (s *Server) newRepoSubmit(w http.ResponseWriter, r *http.Request, u store.U
118136 if r.FormValue("visibility") == "private" {
119137 visibility = "private"
120138 }
121 fail := func(msg string) {
122 s.render(w, "new.html", struct {
123 Site string
124 Viewer string
125 Error string
126 }{s.siteName(), u.Username, msg})
127 }
139 fail := func(msg string) { s.renderNewRepo(w, u, msg) }
128140 if err := policy.ValidateName(name); err != nil {
129141 fail(err.Error())
130142 return
131143 }
132 id, err := s.st.CreateRepo("user", u.ID, name, visibility)
144 // Owner: yourself, or an org you admin — same rule as repo create.
145 owner := r.FormValue("owner")
146 ownerKind, ownerID := "user", u.ID
147 if owner == "" {
148 owner = u.Username
149 }
150 if owner != u.Username {
151 org, err := s.st.OrgByName(owner)
152 if err != nil {
153 fail("no such organization")
154 return
155 }
156 role, _ := s.st.OrgRole(org.ID, u.ID)
157 if role != "admin" {
158 fail("only admins of " + owner + " can create repositories there")
159 return
160 }
161 ownerKind, ownerID = "org", org.ID
162 }
163 id, err := s.st.CreateRepo(ownerKind, ownerID, name, visibility)
133164 if err != nil {
134165 fail(err.Error())
135166 return
136167 }
137 dir := control.RepoDir(s.cfg.Server.Root, u.Username, name)
168 dir := control.RepoDir(s.cfg.Server.Root, owner, name)
138169 if err := gitutil.InitBare(dir, "main", control.HooksDir(s.cfg.Server.Root)); err != nil {
139170 s.st.DeleteRepo(id)
140171 fail("initializing repository failed")
141172 return
142173 }
143 http.Redirect(w, r, "/"+u.Username+"/"+name, http.StatusSeeOther)
174 http.Redirect(w, r, "/"+owner+"/"+name, http.StatusSeeOther)
175}
176
177// pinToggle pins or unpins the repo for the logged-in viewer.
178func (s *Server) pinToggle(w http.ResponseWriter, r *http.Request, u store.User) {
179 repo, ok := s.repoForUser(w, r, u, policy.CanRead)
180 if !ok {
181 return
182 }
183 if s.st.IsPinned(u.ID, repo.ID) {
184 s.st.UnpinRepo(u.ID, repo.ID)
185 } else {
186 s.st.PinRepo(u.ID, repo.ID)
187 }
188 http.Redirect(w, r, "/"+repo.Path(), http.StatusSeeOther)
144189 }
145190
146191 // repoForUser is repoFor with a write/read permission requirement for a
@@ -258,6 +303,17 @@ func (s *Server) issueCreateSubmit(w http.ResponseWriter, r *http.Request, u sto
258303 http.Error(w, "internal error", http.StatusInternalServerError)
259304 return
260305 }
306 // Labels need write access, matching the SSH rule; ignored otherwise.
307 if labels := strings.Fields(r.FormValue("labels")); len(labels) > 0 {
308 grant, _ := s.st.AccessRole(repo.ID, u.ID)
309 if policy.CanWrite(u, repo, grant) {
310 if iss, err := s.st.IssueByNumber(repo.ID, n); err == nil {
311 for _, l := range labels {
312 s.st.SetIssueLabel(repo.ID, iss.ID, l, true)
313 }
314 }
315 }
316 }
261317 http.Redirect(w, r, fmt.Sprintf("/%s/issues/%d", repo.Path(), n), http.StatusSeeOther)
262318 }
263319
internal/httpd/routes.go +2
@@ -85,6 +85,8 @@ func (s *Server) Routes() []Route {
8585 routes = append(routes,
8686 Route{Method: "POST", Pattern: "/new", Mutating: true,
8787 Handler: s.checkOrigin(s.requireUser(s.newRepoSubmit))},
88 Route{Method: "POST", Pattern: "/{owner}/{repo}/pin", Mutating: true,
89 Handler: s.checkOrigin(s.requireUser(s.pinToggle))},
8890 Route{Method: "GET", Pattern: "/{owner}/{repo}/issues/new",
8991 Handler: s.requireUser(s.issueCreateForm)},
9092 Route{Method: "POST", Pattern: "/{owner}/{repo}/issues/new", Mutating: true,
internal/httpd/web.go +25 −2
@@ -215,6 +215,7 @@ type repoPage struct {
215215 Dir string
216216 Tab string // active tab in the repo header
217217 Topics []string
218 Pinned bool // by the viewer
218219 }
219220
220221 // repoFor resolves the repo for a web request; false means 404 was sent.
@@ -244,9 +245,14 @@ func (s *Server) repoFor(w http.ResponseWriter, r *http.Request, ref string) (re
244245 ref = repo.DefaultBranch
245246 }
246247 topics, _ := s.st.ListTopics(repo.ID)
248 pinned := false
249 if viewer.ID != 0 {
250 pinned = s.st.IsPinned(viewer.ID, repo.ID)
251 }
247252 return repoPage{
248253 Site: s.siteName(),
249254 Viewer: viewer.Username,
255 Pinned: pinned,
250256 Desc: gitutil.ReadDescription(control.RepoDir(s.cfg.Server.Root, repo.OwnerName, repo.Name)),
251257 Repo: repo,
252258 Ref: ref,
@@ -1064,11 +1070,13 @@ func (s *Server) commit(w http.ResponseWriter, r *http.Request) {
10641070 s.render(w, "commit.html", struct {
10651071 repoPage
10661072 SHA, ShortSHA, AuthorName, AuthorEmail, CommitterEmail, Date, Message string
1073 Parents []string
10671074 Sig sigView
10681075 Checks []store.CommitStatus
10691076 DiffLines []diffLine
10701077 }{p, full, full[:10], parsed.AuthorName, parsed.AuthorEmail, committerEmail,
1071 time.Unix(parsed.AuthorUnix, 0).UTC().Format(time.RFC3339), msg, v, checks, lines})
1078 time.Unix(parsed.AuthorUnix, 0).UTC().Format(time.RFC3339), msg,
1079 gitutil.Parents(p.Dir, full), v, checks, lines})
10721080 }
10731081
10741082 // labelPalette provides default label chip colors: mid-tone hues that stay
@@ -1117,12 +1125,27 @@ func (s *Server) issues(w http.ResponseWriter, r *http.Request) {
11171125 issues[i].Labels = labels[issues[i].ID]
11181126 }
11191127 }
1128 // ?label=x narrows to issues carrying that label (chips link here).
1129 labelFilter := r.URL.Query().Get("label")
1130 if labelFilter != "" {
1131 var kept []store.Issue
1132 for _, iss := range issues {
1133 for _, l := range iss.Labels {
1134 if l == labelFilter {
1135 kept = append(kept, iss)
1136 break
1137 }
1138 }
1139 }
1140 issues = kept
1141 }
11201142 s.render(w, "issues.html", struct {
11211143 repoPage
11221144 State string
1145 Label string
11231146 Issues []store.Issue
11241147 LabelColors map[string]template.CSS
1125 }{p, state, issues, s.labelColors(p.Repo.ID)})
1148 }{p, state, labelFilter, issues, s.labelColors(p.Repo.ID)})
11261149 }
11271150
11281151 func (s *Server) issue(w http.ResponseWriter, r *http.Request) {
internal/store/dashboard.go +7
@@ -74,6 +74,13 @@ func (s *Store) PinRepo(userID, repoID int64) error {
7474 return err
7575 }
7676
77func (s *Store) IsPinned(userID, repoID int64) bool {
78 var n int
79 s.DB.QueryRow("SELECT COUNT(*) FROM repo_pins WHERE user_id = ? AND repo_id = ?",
80 userID, repoID).Scan(&n)
81 return n > 0
82}
83
7784 func (s *Store) UnpinRepo(userID, repoID int64) error {
7885 res, err := s.DB.Exec(
7986 "DELETE FROM repo_pins WHERE user_id = ? AND repo_id = ?", userID, repoID)
internal/web/static/style.css +13
@@ -149,6 +149,19 @@ h1.repotitle a { color: var(--fg); }
149149 h1.repotitle a:last-of-type { font-weight: 650; }
150150 h1.repotitle a:hover { color: var(--accent); text-decoration: none; }
151151 h1.repotitle .sep { color: var(--muted); margin: 0 0.15em; }
152form.pinform { display: inline; margin-left: var(--sp-2); vertical-align: middle; }
153button.pinbtn {
154 background: none;
155 border: 1px solid var(--line);
156 border-radius: var(--r-pill);
157 color: var(--muted);
158 font-size: var(--fs-0);
159 padding: 0.05rem 0.55rem;
160 cursor: pointer;
161}
162button.pinbtn:hover { border-color: var(--accent); color: var(--accent); filter: none; }
163button.pinbtn.pinned { color: var(--accent); border-color: var(--accent); }
164a.chip.label:hover, a.chip.topic:hover { text-decoration: none; filter: brightness(1.15); }
152165 .repohead .desc { margin: 0 0 var(--sp-2); }
153166
154167 /* repo tabs */
internal/web/templates/commit.html +1
@@ -4,6 +4,7 @@
44 <div class="commithead">
55 <h2>commit <code>{{.ShortSHA}}</code></h2>
66 <p class="meta"><code class="fullsha">{{.SHA}}</code></p>
7 {{if .Parents}}<p class="meta">parent{{if gt (len .Parents) 1}}s{{end}}:{{range .Parents}} <code><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/commit/{{.}}">{{short .}}</a></code>{{end}}</p>{{end}}
78 <p>{{template "sigbadge" .Sig}}{{range .Checks}} <span class="badge check-{{.State}}">{{.Context}}: {{.State}}</span>{{end}}</p>
89 <p class="meta"><span title="{{.AuthorEmail}}">{{.AuthorName}}</span> &lt;{{.AuthorEmail}}&gt; · {{.Date}}
910 {{if .CommitterEmail}}<br>committer: &lt;{{.CommitterEmail}}&gt;{{end}}</p>
internal/web/templates/dashboard.html +2 −2
@@ -15,7 +15,7 @@
1515 {{range .MRs}}<li>
1616 <div class="issuemain">
1717 <p class="title"><a href="/{{.RepoPath}}/mrs/{{.Number}}">{{.RepoPath}}!{{.Number}} {{.Title}}</a></p>
18 <p class="meta">{{.Author}} · {{when .UpdatedAt}}{{if eq .State "source_gone"}} · <span class="chip chip-source_gone">source gone</span>{{end}}</p>
18 <p class="meta"><a href="/{{.Author}}">{{.Author}}</a> · {{when .UpdatedAt}}{{if eq .State "source_gone"}} · <span class="chip chip-source_gone">source gone</span>{{end}}</p>
1919 </div>
2020 </li>
2121 {{else}}<li class="empty">no open merge requests</li>{{end}}
@@ -25,7 +25,7 @@
2525 {{range .Issues}}<li>
2626 <div class="issuemain">
2727 <p class="title"><a href="/{{.RepoPath}}/issues/{{.Number}}">{{.RepoPath}}#{{.Number}} {{.Title}}</a></p>
28 <p class="meta">{{.Author}} · {{when .UpdatedAt}}</p>
28 <p class="meta"><a href="/{{.Author}}">{{.Author}}</a> · {{when .UpdatedAt}}</p>
2929 </div>
3030 </li>
3131 {{else}}<li class="empty">no open issues</li>{{end}}
internal/web/templates/issue.html +3 −3
@@ -3,12 +3,12 @@
33 {{template "repoheader" .}}
44 <h2 class="issuetitle">{{.Issue.Title}} <span class="issuenumber">#{{.Issue.Number}}</span></h2>
55 <p class="issuemeta"><span class="chip {{if eq .Issue.State "open"}}chip-open{{else}}chip-done{{end}}">{{.Issue.State}}</span>
6{{.Issue.Author}} opened this on {{when .Issue.CreatedAt}}
7{{if .Issue.Labels}} · {{range .Issue.Labels}}<span class="chip label" style="{{index $.LabelColors .}}">{{.}}</span> {{end}}{{end}}
6<a href="/{{.Issue.Author}}">{{.Issue.Author}}</a> opened this on {{when .Issue.CreatedAt}}
7{{if .Issue.Labels}} · {{range .Issue.Labels}}<a class="chip label" style="{{index $.LabelColors .}}" href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/issues?label={{.}}">{{.}}</a> {{end}}{{end}}
88 {{if .Issue.Assignees}} · assigned to {{range .Issue.Assignees}}{{.}} {{end}}{{end}}
99 {{if .Issue.Milestone}} · milestone <a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/milestones">{{.Issue.Milestone}}</a>{{end}}</p>
1010 {{if .BodyHTML}}<article class="comment">
11 <header class="commenthead"><strong>{{.Issue.Author}}</strong> <span class="when">{{when .Issue.CreatedAt}}</span></header>
11 <header class="commenthead"><strong><a href="/{{.Issue.Author}}">{{.Issue.Author}}</a></strong> <span class="when">{{when .Issue.CreatedAt}}</span></header>
1212 <div class="rendered">{{.BodyHTML}}</div>
1313 </article>{{end}}
1414 {{range .Comments}}
internal/web/templates/issuenew.html +1
@@ -7,6 +7,7 @@
77 <form method="post" action="/{{.Repo.OwnerName}}/{{.Repo.Name}}/issues/new" class="commentform">
88 <p><input type="text" name="title" placeholder="title" required></p>
99 <p><textarea name="body" rows="12">{{.Body}}</textarea></p>
10<p><input type="text" name="labels" placeholder="labels, space-separated (write access)"></p>
1011 <p><button type="submit">open issue</button></p>
1112 </form>
1213 {{end}}
internal/web/templates/issues.html +3 −2
@@ -8,6 +8,7 @@
88 <a {{if eq .State "closed"}}class="active" {{end}}href="?state=closed">closed</a>
99 <a {{if eq .State "all"}}class="active" {{end}}href="?state=all">all</a>
1010 </nav>
11 {{if .Label}}<p class="meta">label: <span class="chip label" style="{{index .LabelColors .Label}}">{{.Label}}</span> <a href="?state={{.State}}">clear</a></p>{{end}}
1112 <span class="spacer"></span>
1213 <p class="meta"><a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/milestones">milestones</a>{{if .Viewer}} · <a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/issues/new">new issue</a>{{end}}</p>
1314 </div>
@@ -15,8 +16,8 @@
1516 {{range .Issues}}<li>
1617 <div class="issuemain">
1718 <p class="title"><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/issues/{{.Number}}">{{.Title}}</a>
18 {{range .Labels}}<span class="chip label" style="{{index $.LabelColors .}}">{{.}}</span> {{end}}</p>
19 <p class="meta">#{{.Number}} opened by {{.Author}}{{if .Milestone}} · <a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/milestones">{{.Milestone}}</a>{{end}}</p>
19 {{range .Labels}}<a class="chip label" style="{{index $.LabelColors .}}" href="?label={{.}}">{{.}}</a> {{end}}</p>
20 <p class="meta">#{{.Number}} opened by <a href="/{{.Author}}">{{.Author}}</a>{{if .Milestone}} · <a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/milestones">{{.Milestone}}</a>{{end}}</p>
2021 </div>
2122 <span class="chip {{if eq .State "open"}}chip-open{{else}}chip-done{{end}}">{{.State}}</span>
2223 </li>
internal/web/templates/layout.html +1 −1
@@ -29,7 +29,7 @@
2929
3030 {{define "repoheader"}}
3131 <div class="repohead">
32<h1 class="repotitle"><a class="owner" href="/{{.Repo.OwnerName}}">{{.Repo.OwnerName}}</a><span class="sep">/</span><a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}">{{.Repo.Name}}</a>{{if eq .Repo.Visibility "private"}} <span class="chip chip-neutral">private</span>{{end}}{{if .Repo.Settings.Archived}} <span class="chip chip-stale">archived</span>{{end}}</h1>
32<h1 class="repotitle"><a class="owner" href="/{{.Repo.OwnerName}}">{{.Repo.OwnerName}}</a><span class="sep">/</span><a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}">{{.Repo.Name}}</a>{{if eq .Repo.Visibility "private"}} <span class="chip chip-neutral">private</span>{{end}}{{if .Repo.Settings.Archived}} <span class="chip chip-stale">archived</span>{{end}}{{if .Viewer}}<form method="post" action="/{{.Repo.OwnerName}}/{{.Repo.Name}}/pin" class="inline pinform"><button type="submit" class="pinbtn{{if .Pinned}} pinned{{end}}" title="{{if .Pinned}}unpin from dashboard{{else}}pin to dashboard{{end}}">{{if .Pinned}}★ pinned{{else}}☆ pin{{end}}</button></form>{{end}}</h1>
3333 {{if .Desc}}<p class="desc">{{.Desc}}</p>{{end}}
3434 {{if .Topics}}<p class="topics">{{range .Topics}}<span class="chip topic">{{.}}</span> {{end}}</p>{{end}}
3535 <nav class="tabs">
internal/web/templates/mr.html +1 −1
@@ -3,7 +3,7 @@
33 {{template "repoheader" .}}
44 <h2 class="issuetitle">{{.MR.Title}} <span class="issuenumber">!{{.MR.Number}}</span></h2>
55 <p class="issuemeta"><span class="chip chip-{{.MR.State}}">{{.MR.State}}</span>
6{{.MR.Author}} wants to merge {{if .MR.SourcePath}}{{.MR.SourcePath}}:{{end}}{{.MR.SourceRef}} into {{.MR.TargetRef}}
6<a href="/{{.MR.Author}}">{{.MR.Author}}</a> wants to merge {{if .MR.SourcePath}}{{.MR.SourcePath}}:{{end}}{{.MR.SourceRef}} into {{.MR.TargetRef}}
77 at <code>{{short .MR.HeadSHA}}</code>{{if .MR.Milestone}} · milestone <a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/milestones">{{.MR.Milestone}}</a>{{end}}</p>
88 {{if .BodyHTML}}<article class="comment">
99 <header class="commenthead"><strong>{{.MR.Author}}</strong></header>
internal/web/templates/mrs.html +1 −1
@@ -14,7 +14,7 @@
1414 {{range .MRs}}<li>
1515 <div class="issuemain">
1616 <p class="title"><a href="/{{$.Repo.OwnerName}}/{{$.Repo.Name}}/mrs/{{.Number}}">{{.Title}}</a></p>
17 <p class="meta">!{{.Number}} by {{.Author}} · {{if .SourcePath}}{{.SourcePath}}:{{end}}{{.SourceRef}} → {{.TargetRef}}</p>
17 <p class="meta">!{{.Number}} by <a href="/{{.Author}}">{{.Author}}</a> · {{if .SourcePath}}{{.SourcePath}}:{{end}}{{.SourceRef}} → {{.TargetRef}}</p>
1818 </div>
1919 <span class="chip chip-{{.State}}">{{.State}}</span>
2020 </li>
internal/web/templates/new.html +5 −1
@@ -3,7 +3,11 @@
33 <h1>new repository</h1>
44 {{if .Error}}<p class="error">{{.Error}}</p>{{end}}
55 <form method="post" action="/new">
6<p><label>name <input name="name" required pattern="[a-z0-9][a-z0-9._-]*"></label> (under {{.Viewer}}/)</p>
6<p><label>owner <select name="owner">
7 <option value="{{.Viewer}}">{{.Viewer}}</option>
8 {{range .Orgs}}<option value="{{.}}">{{.}}</option>{{end}}
9</select></label>
10<label>/ name <input name="name" required pattern="[a-z0-9][a-z0-9._-]*"></label></p>
711 <p><label><input type="radio" name="visibility" value="public" checked> public</label>
812 <label><input type="radio" name="visibility" value="private"> private</label></p>
913 <p><button type="submit">create</button></p>