Commit b754784fc6
Verified · cmc
e2e/description_test.go +32
| @@ -55,6 +55,38 @@ func TestRepoDescriptions(t *testing.T) { | ||
| 55 | 55 | } |
| 56 | 56 | } |
| 57 | 57 | |
| 58 | // Website: set, shown in show and on the repo page; bad scheme and | |
| 59 | // non-admins refused. | |
| 60 | if _, errOut, code := inst.ssh(t, aliceKey, "", | |
| 61 | "repo", "settings", "website", "alice/tool", "https://tool.example.org"); code != 0 { | |
| 62 | t.Fatalf("settings website: %s", errOut) | |
| 63 | } | |
| 64 | out, _, _ = inst.ssh(t, aliceKey, "", "repo", "show", "alice/tool", "--json") | |
| 65 | if !strings.Contains(out, `"website":"https://tool.example.org"`) { | |
| 66 | t.Fatalf("website show: %s", out) | |
| 67 | } | |
| 68 | if _, body := inst.get(t, "/alice/tool"); !strings.Contains(body, `href="https://tool.example.org"`) { | |
| 69 | t.Fatalf("website missing on repo page:\n%s", body) | |
| 70 | } | |
| 71 | if _, _, code := inst.ssh(t, aliceKey, "", "repo", "settings", "website", "alice/tool", "javascript:alert(1)"); code != 2 { | |
| 72 | t.Fatalf("bad scheme accepted") | |
| 73 | } | |
| 74 | if _, _, code := inst.ssh(t, bobKey, "", "repo", "settings", "website", "alice/tool", "https://x.example"); code != 4 { | |
| 75 | t.Fatalf("non-admin set website: want exit 4") | |
| 76 | } | |
| 77 | // Clearing removes it. | |
| 78 | if _, _, code := inst.ssh(t, aliceKey, "", "repo", "settings", "website", "alice/tool", "''"); code != 0 { | |
| 79 | t.Fatal("website clear failed") | |
| 80 | } | |
| 81 | out, _, _ = inst.ssh(t, aliceKey, "", "repo", "show", "alice/tool", "--json") | |
| 82 | if strings.Contains(out, `"website"`) { | |
| 83 | t.Fatalf("website not cleared: %s", out) | |
| 84 | } | |
| 85 | if _, _, code := inst.ssh(t, aliceKey, "", | |
| 86 | "repo", "settings", "website", "alice/tool", "https://tool.example.org"); code != 0 { | |
| 87 | t.Fatal("website re-set failed") | |
| 88 | } | |
| 89 | ||
| 58 | 90 | // Forks inherit the description. |
| 59 | 91 | if _, errOut, code := inst.ssh(t, bobKey, "", "repo", "fork", "alice/tool"); code != 0 { |
| 60 | 92 | t.Fatalf("fork: %s", errOut) |
internal/control/repo.go +37 −2
| @@ -48,6 +48,8 @@ func init() { | ||
| 48 | 48 | Summary: "unprotect a branch: repo settings unprotect <owner/name> <branch>", Run: runUnprotect}) |
| 49 | 49 | register(Command{Path: []string{"repo", "settings", "description"}, |
| 50 | 50 | Summary: "set the repository description: repo settings description <owner/name> <text> ('' clears)", Run: runSetDescription}) |
| 51 | register(Command{Path: []string{"repo", "settings", "website"}, | |
| 52 | Summary: "set the repository website: repo settings website <owner/name> <url> ('' clears)", Run: runSetWebsite}) | |
| 51 | 53 | register(Command{Path: []string{"repo", "settings", "git-daemon"}, |
| 52 | 54 | Summary: "expose over git://: repo settings git-daemon <owner/name> on|off", Run: runGitDaemon}) |
| 53 | 55 | register(Command{Path: []string{"repo", "archive"}, |
| @@ -236,6 +238,7 @@ func runRepoShow(c *Ctx, args []string) int { | ||
| 236 | 238 | type out struct { |
| 237 | 239 | Path string `json:"path"` |
| 238 | 240 | Description string `json:"description,omitempty"` |
| 241 | Website string `json:"website,omitempty"` | |
| 239 | 242 | Visibility string `json:"visibility"` |
| 240 | 243 | DefaultBranch string `json:"default_branch"` |
| 241 | 244 | ProtectedBranches []string `json:"protected_branches,omitempty"` |
| @@ -248,8 +251,8 @@ func runRepoShow(c *Ctx, args []string) int { | ||
| 248 | 251 | if err != nil { |
| 249 | 252 | return c.fail(protocol.ExitFailure, "%v", err) |
| 250 | 253 | } |
| 251 | d := out{repo.Path(), desc, repo.Visibility, repo.DefaultBranch, repo.Settings.ProtectedBranches, | |
| 252 | repo.Settings.Archived, topics, nil} | |
| 254 | d := out{repo.Path(), desc, repo.Settings.Website, repo.Visibility, repo.DefaultBranch, | |
| 255 | repo.Settings.ProtectedBranches, repo.Settings.Archived, topics, nil} | |
| 253 | 256 | // Mirror status is admin-only, like repo mirror list. The token never |
| 254 | 257 | // leaves the server. |
| 255 | 258 | if grant, err := c.Store.AccessRole(repo.ID, c.User.ID); err == nil && policy.CanAdmin(c.User, repo, grant) { |
| @@ -270,6 +273,9 @@ func runRepoShow(c *Ctx, args []string) int { | ||
| 270 | 273 | if d.Description != "" { |
| 271 | 274 | fmt.Fprintf(w, "%s\n", d.Description) |
| 272 | 275 | } |
| 276 | if d.Website != "" { | |
| 277 | fmt.Fprintf(w, "website: %s\n", d.Website) | |
| 278 | } | |
| 273 | 279 | if len(d.Topics) > 0 { |
| 274 | 280 | fmt.Fprintf(w, "topics: %s\n", strings.Join(d.Topics, ", ")) |
| 275 | 281 | } |
| @@ -485,6 +491,35 @@ func runSetDescription(c *Ctx, args []string) int { | ||
| 485 | 491 | }) |
| 486 | 492 | } |
| 487 | 493 | |
| 494 | func runSetWebsite(c *Ctx, args []string) int { | |
| 495 | if len(args) != 2 { | |
| 496 | return c.fail(protocol.ExitUsage, "usage: repo settings website <owner/name> <url>") | |
| 497 | } | |
| 498 | site := strings.TrimSpace(args[1]) | |
| 499 | if err := validateWebsite(site); err != nil { | |
| 500 | return c.fail(protocol.ExitUsage, "%v", err) | |
| 501 | } | |
| 502 | if len(site) > 256 { | |
| 503 | return c.fail(protocol.ExitUsage, "website URL too long (max 256)") | |
| 504 | } | |
| 505 | repo, code := resolveRepo(c, args[0], policy.CanAdmin) | |
| 506 | if code >= 0 { | |
| 507 | return code | |
| 508 | } | |
| 509 | s := repo.Settings | |
| 510 | s.Website = site | |
| 511 | if err := c.Store.SetRepoSettings(repo.ID, s); err != nil { | |
| 512 | return c.fail(protocol.ExitFailure, "%v", err) | |
| 513 | } | |
| 514 | return c.emit(map[string]string{"website": site}, func(w io.Writer) { | |
| 515 | if site == "" { | |
| 516 | fmt.Fprintf(w, "website cleared on %s\n", repo.Path()) | |
| 517 | } else { | |
| 518 | fmt.Fprintf(w, "website set on %s\n", repo.Path()) | |
| 519 | } | |
| 520 | }) | |
| 521 | } | |
| 522 | ||
| 488 | 523 | func runGitDaemon(c *Ctx, args []string) int { |
| 489 | 524 | if len(args) != 2 || (args[1] != "on" && args[1] != "off") { |
| 490 | 525 | return c.fail(protocol.ExitUsage, "usage: repo settings git-daemon <owner/name> on|off") |
internal/store/repos.go +1
| @@ -28,6 +28,7 @@ type RepoSettings struct { | ||
| 28 | 28 | RequireResolved bool `json:"require_resolved,omitempty"` |
| 29 | 29 | GitDaemon bool `json:"git_daemon,omitempty"` |
| 30 | 30 | Archived bool `json:"archived,omitempty"` |
| 31 | Website string `json:"website,omitempty"` | |
| 31 | 32 | } |
| 32 | 33 | |
| 33 | 34 | // Path returns the canonical owner/name form. |
internal/web/templates/layout.html +1
| @@ -31,6 +31,7 @@ | ||
| 31 | 31 | <div class="repohead"> |
| 32 | 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> |
| 33 | 33 | {{if .Desc}}<p class="desc">{{.Desc}}</p>{{end}} |
| 34 | {{if .Repo.Settings.Website}}<p class="meta repowebsite"><a href="{{.Repo.Settings.Website}}" rel="nofollow">{{.Repo.Settings.Website}}</a></p>{{end}} | |
| 34 | 35 | {{if .Topics}}<p class="topics">{{range .Topics}}<span class="chip topic">{{.}}</span> {{end}}</p>{{end}} |
| 35 | 36 | {{range .Mirrors}}<p class="meta mirrorline">{{if eq .Direction "push"}}mirrors to{{else}}mirrors from{{end}} <a href="{{.URL}}" rel="nofollow">{{.Target}}</a>{{if .Error}} · <span class="mirrorerr">sync error: {{.Error}}</span>{{else if .Synced}} · synced {{.Synced}}{{end}}</p> |
| 36 | 37 | {{end}}<nav class="tabs"> |