Commit a8ca19c99f

a8ca19c99fed81a831a98acd0823647b7a413909

parent: 584dc8d6ee

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-28 22:02 UTC

web: MR list offers a fork link or a sign-in prompt to visitors who cannot open one directly

Ref #270

Layout: unified · split

internal/httpd/mrslist_test.go added +86
@@ -0,0 +1,86 @@
1package httpd
2
3import (
4 "net/http"
5 "net/http/httptest"
6 "strings"
7 "testing"
8 "time"
9
10 "gitbay.org/gitbay/internal/config"
11 "gitbay.org/gitbay/internal/store"
12)
13
14// A repository's MR list offers the right next step by access level: a
15// writer gets "New merge request", a signed-in reader without push gets
16// a fork link, and a signed-out visitor gets a sign-in prompt (#270).
17func TestMRsListContributionHintByAccess(t *testing.T) {
18 st, err := store.Open(":memory:")
19 if err != nil {
20 t.Fatal(err)
21 }
22 defer st.Close()
23 if err := st.MigrateUp(); err != nil {
24 t.Fatal(err)
25 }
26 owner, err := st.CreateUser("alice", false)
27 if err != nil {
28 t.Fatal(err)
29 }
30 reader, err := st.CreateUser("bob", false)
31 if err != nil {
32 t.Fatal(err)
33 }
34 if _, err := st.CreateRepo("user", owner, "app", "public"); err != nil {
35 t.Fatal(err)
36 }
37
38 cfg := config.Default()
39 cfg.Web.Mode = "accounts"
40 s := New(cfg, st)
41
42 // mrs reads the viewer through s.viewer(r), which resolves a
43 // session cookie (internal/httpd/accounts.go:37-47) rather than
44 // taking the viewer as a parameter the way a POST handler test
45 // does. Give a real viewer a real session; leave the request
46 // cookie-less for the anonymous case.
47 sessionFor := func(uid int64) *http.Cookie {
48 tok, hash, err := store.NewToken()
49 if err != nil {
50 t.Fatal(err)
51 }
52 if err := st.CreateWebSession(hash, uid, time.Hour); err != nil {
53 t.Fatal(err)
54 }
55 return s.sessionCookieFor(tok)
56 }
57
58 get := func(uid int64) string {
59 req := httptest.NewRequest("GET", "/alice/app/mrs", nil)
60 req.SetPathValue("owner", "alice")
61 req.SetPathValue("repo", "app")
62 if uid != 0 {
63 req.AddCookie(sessionFor(uid))
64 }
65 rr := httptest.NewRecorder()
66 s.mrs(rr, req)
67 return rr.Body.String()
68 }
69 anonymous := get(0)
70 if !strings.Contains(anonymous, "Sign in to propose a change") {
71 t.Errorf("signed-out visitor: missing sign-in prompt:\n%s", anonymous)
72 }
73 if strings.Contains(anonymous, "New merge request") {
74 t.Error("signed-out visitor should not see New merge request")
75 }
76
77 readerOut := get(reader)
78 if !strings.Contains(readerOut, "Fork this repository to propose a change") {
79 t.Errorf("reader without push: missing fork hint:\n%s", readerOut)
80 }
81
82 ownerOut := get(owner)
83 if !strings.Contains(ownerOut, "New merge request") {
84 t.Errorf("owner: missing New merge request link:\n%s", ownerOut)
85 }
86}
internal/httpd/mrsrow_test.go +7 −6
@@ -10,12 +10,13 @@ import (
1010// mrsPageData mirrors the anonymous struct the mrs handler renders with.
1111type mrsPageData struct {
1212 repoPage
13 State string
14 Query string
15 Filters []listFilter
16 Facets []facetGroup
17 MRs []mrRow
18 Older string
13 State string
14 Query string
15 Filters []listFilter
16 Facets []facetGroup
17 MRs []mrRow
18 Older string
19 CanWrite bool
1920}
2021
2122func renderMRs(t *testing.T, rows []mrRow, state string) string {
internal/httpd/web.go +3 −1
@@ -2037,6 +2037,7 @@ func (s *Server) mrs(w http.ResponseWriter, r *http.Request) {
20372037 return
20382038 }
20392039 p.Tab = "merge requests"
2040 canWrite := s.canWriteRepo(r, p.Repo)
20402041 state := r.URL.Query().Get("state")
20412042 if state == "" {
20422043 state = "open"
@@ -2096,9 +2097,10 @@ func (s *Server) mrs(w http.ResponseWriter, r *http.Request) {
20962097 MRs []mrRow
20972098 LabelColors map[string]template.CSS
20982099 Older string
2100 CanWrite bool
20992101 }{p, state, mf.Search,
21002102 activeFilters(state, [][2]string{{"label", mf.Label}, {"author", mf.Author}, {"milestone", mf.Milestone}}),
2101 facets, rows, s.labelColors(p.Repo), older})
2103 facets, rows, s.labelColors(p.Repo), older, canWrite})
21022104}
21032105
21042106func (s *Server) mr(w http.ResponseWriter, r *http.Request) {
internal/web/templates/mrs.html +3 −1
@@ -13,7 +13,9 @@
1313 </form>
1414 {{range .Filters}}<p class="meta">{{.Key}}: {{if eq .Key "label"}}<span class="chip label" style="{{index $.LabelColors .Value}}">{{.Value}}</span>{{else}}<b>{{.Value}}</b>{{end}} <a href="{{.Clear}}">clear</a></p>{{end}}
1515</div>
16{{if .Viewer}}<p class="meta"><a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/mrs/new">New merge request</a></p>{{end}}
16{{if .CanWrite}}<p class="meta"><a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/mrs/new">New merge request</a></p>
17{{else if .Viewer}}<p class="meta"><a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/fork">Fork this repository to propose a change</a></p>
18{{else}}<p class="meta"><a href="/login">Sign in to propose a change</a></p>{{end}}
1719<ul class="issuelist rows">
1820{{range .MRs}}{{$n := .Number}}<li>
1921 <div class="issuemain">