Commit 85a32ad0bd
Verified · cmc
Layout: unified · split
cmd/gitbay/main.go +1
| @@ -220,6 +220,7 @@ func repoCmd() *cobra.Command { | |||
| 220 | pass("protect", "protect a branch", passOpts{server: []string{"repo", "settings", "protect"}, needsRepo: true}), | 220 | pass("protect", "protect a branch", passOpts{server: []string{"repo", "settings", "protect"}, needsRepo: true}), |
| 221 | pass("unprotect", "unprotect a branch", passOpts{server: []string{"repo", "settings", "unprotect"}, needsRepo: true}), | 221 | pass("unprotect", "unprotect a branch", passOpts{server: []string{"repo", "settings", "unprotect"}, needsRepo: true}), |
| 222 | pass("require-signed", "require verified commit signatures: ... on|off", passOpts{server: []string{"repo", "settings", "require-signed"}, needsRepo: true}), | 222 | pass("require-signed", "require verified commit signatures: ... on|off", passOpts{server: []string{"repo", "settings", "require-signed"}, needsRepo: true}), |
| 223 | pass("description", "set the repository description: <text>", passOpts{server: []string{"repo", "settings", "description"}, needsRepo: true}), | ||
| 223 | pass("git-daemon", "expose over git://: ... on|off", passOpts{server: []string{"repo", "settings", "git-daemon"}, needsRepo: true}), | 224 | pass("git-daemon", "expose over git://: ... on|off", passOpts{server: []string{"repo", "settings", "git-daemon"}, needsRepo: true}), |
| 224 | ), | 225 | ), |
| 225 | ) | 226 | ) |
e2e/description_test.go added +76
| @@ -0,0 +1,76 @@ | |||
| 1 | package e2e | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "strings" | ||
| 5 | "testing" | ||
| 6 | ) | ||
| 7 | |||
| 8 | func TestRepoDescriptions(t *testing.T) { | ||
| 9 | inst := startInstance(t) | ||
| 10 | aliceKey := inst.newKey(t, "alice") | ||
| 11 | inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub") | ||
| 12 | |||
| 13 | // Set at creation; visible in show and list (human and JSON). | ||
| 14 | if _, errOut, code := inst.ssh(t, aliceKey, "", | ||
| 15 | "repo", "create", "alice/tool", "--description", "'a fine tool'"); code != 0 { | ||
| 16 | t.Fatalf("create: %s", errOut) | ||
| 17 | } | ||
| 18 | out, _, _ := inst.ssh(t, aliceKey, "", "repo", "show", "alice/tool", "--json") | ||
| 19 | if !strings.Contains(out, `"description":"a fine tool"`) { | ||
| 20 | t.Fatalf("show json: %s", out) | ||
| 21 | } | ||
| 22 | out, _, _ = inst.ssh(t, aliceKey, "", "repo", "list") | ||
| 23 | if !strings.Contains(out, "a fine tool") { | ||
| 24 | t.Fatalf("list: %s", out) | ||
| 25 | } | ||
| 26 | |||
| 27 | // The description lives in the bare repo's native description file. | ||
| 28 | sshOut, _, _ := inst.ssh(t, aliceKey, "", "repo", "show", "alice/tool") | ||
| 29 | if !strings.Contains(sshOut, "a fine tool") { | ||
| 30 | t.Fatalf("show human: %s", sshOut) | ||
| 31 | } | ||
| 32 | |||
| 33 | // Update via settings; first line only, trimmed. | ||
| 34 | if _, errOut, code := inst.ssh(t, aliceKey, "", | ||
| 35 | "repo", "settings", "description", "alice/tool", "'better now'"); code != 0 { | ||
| 36 | t.Fatalf("settings description: %s", errOut) | ||
| 37 | } | ||
| 38 | out, _, _ = inst.ssh(t, aliceKey, "", "repo", "show", "alice/tool", "--json") | ||
| 39 | if !strings.Contains(out, `"description":"better now"`) { | ||
| 40 | t.Fatalf("updated show: %s", out) | ||
| 41 | } | ||
| 42 | |||
| 43 | // Non-admins cannot set it. | ||
| 44 | bobKey := inst.newKey(t, "bob") | ||
| 45 | inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub") | ||
| 46 | if _, _, code := inst.ssh(t, bobKey, "", "repo", "settings", "description", "alice/tool", "hax"); code != 4 { | ||
| 47 | t.Fatalf("non-admin set: exit %d, want 4", code) | ||
| 48 | } | ||
| 49 | |||
| 50 | // Web: index, owner page, and repo header all show it. | ||
| 51 | for _, path := range []string{"/", "/alice", "/alice/tool"} { | ||
| 52 | status, body := inst.get(t, path) | ||
| 53 | if status != 200 || !strings.Contains(body, "better now") { | ||
| 54 | t.Fatalf("description missing at %s (%d)", path, status) | ||
| 55 | } | ||
| 56 | } | ||
| 57 | |||
| 58 | // Forks inherit the description. | ||
| 59 | if _, errOut, code := inst.ssh(t, bobKey, "", "repo", "fork", "alice/tool"); code != 0 { | ||
| 60 | t.Fatalf("fork: %s", errOut) | ||
| 61 | } | ||
| 62 | out, _, _ = inst.ssh(t, bobKey, "", "repo", "show", "bob/tool", "--json") | ||
| 63 | if !strings.Contains(out, `"description":"better now"`) { | ||
| 64 | t.Fatalf("fork description: %s", out) | ||
| 65 | } | ||
| 66 | |||
| 67 | // A repo with no description set stays clean (git's placeholder is | ||
| 68 | // treated as empty). | ||
| 69 | if _, _, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/plain"); code != 0 { | ||
| 70 | t.Fatal("plain create failed") | ||
| 71 | } | ||
| 72 | out, _, _ = inst.ssh(t, aliceKey, "", "repo", "show", "alice/plain", "--json") | ||
| 73 | if strings.Contains(out, "Unnamed repository") || strings.Contains(out, `"description"`) { | ||
| 74 | t.Fatalf("placeholder leaked: %s", out) | ||
| 75 | } | ||
| 76 | } | ||
internal/control/mr.go +3
| @@ -81,6 +81,9 @@ func runRepoFork(c *Ctx, args []string) int { | |||
| 81 | c.Store.DeleteRepo(id) | 81 | c.Store.DeleteRepo(id) |
| 82 | return c.fail(protocol.ExitFailure, "%v", err) | 82 | return c.fail(protocol.ExitFailure, "%v", err) |
| 83 | } | 83 | } |
| 84 | if desc := gitutil.ReadDescription(srcDir); desc != "" { | ||
| 85 | gitutil.WriteDescription(dstDir, desc) | ||
| 86 | } | ||
| 84 | if err := gitutil.FetchInto(dstDir, srcDir, "refs/heads/*", "refs/heads/*"); err != nil { | 87 | if err := gitutil.FetchInto(dstDir, srcDir, "refs/heads/*", "refs/heads/*"); err != nil { |
| 85 | // Empty source repos have nothing to fetch; that is fine. | 88 | // Empty source repos have nothing to fetch; that is fine. |
| 86 | if _, rerr := gitutil.ResolveRef(srcDir, src.DefaultBranch); rerr == nil { | 89 | if _, rerr := gitutil.ResolveRef(srcDir, src.DefaultBranch); rerr == nil { |
internal/control/repo.go +47 −10
| @@ -46,6 +46,8 @@ func init() { | |||
| 46 | Summary: "protect a branch: repo settings protect <owner/name> <branch>", Run: runProtect}) | 46 | Summary: "protect a branch: repo settings protect <owner/name> <branch>", Run: runProtect}) |
| 47 | register(Command{Path: []string{"repo", "settings", "unprotect"}, | 47 | register(Command{Path: []string{"repo", "settings", "unprotect"}, |
| 48 | Summary: "unprotect a branch: repo settings unprotect <owner/name> <branch>", Run: runUnprotect}) | 48 | Summary: "unprotect a branch: repo settings unprotect <owner/name> <branch>", Run: runUnprotect}) |
| 49 | register(Command{Path: []string{"repo", "settings", "description"}, | ||
| 50 | Summary: "set the repository description: repo settings description <owner/name> <text> ('' clears)", Run: runSetDescription}) | ||
| 49 | register(Command{Path: []string{"repo", "settings", "git-daemon"}, | 51 | register(Command{Path: []string{"repo", "settings", "git-daemon"}, |
| 50 | Summary: "expose over git://: repo settings git-daemon <owner/name> on|off", Run: runGitDaemon}) | 52 | Summary: "expose over git://: repo settings git-daemon <owner/name> on|off", Run: runGitDaemon}) |
| 51 | } | 53 | } |
| @@ -76,16 +78,22 @@ func resolveRepo(c *Ctx, path string, check func(store.User, store.Repo, string) | |||
| 76 | 78 | ||
| 77 | func runRepoCreate(c *Ctx, args []string) int { | 79 | func runRepoCreate(c *Ctx, args []string) int { |
| 78 | visibility := "public" | 80 | visibility := "public" |
| 79 | var path string | 81 | var path, description string |
| 80 | for _, a := range args { | 82 | for i := 0; i < len(args); i++ { |
| 81 | switch a { | 83 | switch args[i] { |
| 82 | case "--private": | 84 | case "--private": |
| 83 | visibility = "private" | 85 | visibility = "private" |
| 86 | case "--description": | ||
| 87 | if i+1 >= len(args) { | ||
| 88 | return c.fail(protocol.ExitUsage, "--description requires a value") | ||
| 89 | } | ||
| 90 | description = args[i+1] | ||
| 91 | i++ | ||
| 84 | default: | 92 | default: |
| 85 | if path != "" { | 93 | if path != "" { |
| 86 | return c.fail(protocol.ExitUsage, "usage: repo create <owner/name> [--private]") | 94 | return c.fail(protocol.ExitUsage, "usage: repo create <owner/name> [--private] [--description <text>]") |
| 87 | } | 95 | } |
| 88 | path = a | 96 | path = args[i] |
| 89 | } | 97 | } |
| 90 | } | 98 | } |
| 91 | owner, name, ok := strings.Cut(path, "/") | 99 | owner, name, ok := strings.Cut(path, "/") |
| @@ -119,6 +127,11 @@ func runRepoCreate(c *Ctx, args []string) int { | |||
| 119 | c.Store.DeleteRepo(id) | 127 | c.Store.DeleteRepo(id) |
| 120 | return c.fail(protocol.ExitFailure, "initializing repository: %v", err) | 128 | return c.fail(protocol.ExitFailure, "initializing repository: %v", err) |
| 121 | } | 129 | } |
| 130 | if description != "" { | ||
| 131 | if err := gitutil.WriteDescription(dir, description); err != nil { | ||
| 132 | return c.fail(protocol.ExitFailure, "writing description: %v", err) | ||
| 133 | } | ||
| 134 | } | ||
| 122 | type out struct { | 135 | type out struct { |
| 123 | Path string `json:"path"` | 136 | Path string `json:"path"` |
| 124 | Visibility string `json:"visibility"` | 137 | Visibility string `json:"visibility"` |
| @@ -143,16 +156,18 @@ func runRepoList(c *Ctx, args []string) int { | |||
| 143 | return c.fail(protocol.ExitFailure, "%v", err) | 156 | return c.fail(protocol.ExitFailure, "%v", err) |
| 144 | } | 157 | } |
| 145 | type out struct { | 158 | type out struct { |
| 146 | Path string `json:"path"` | 159 | Path string `json:"path"` |
| 147 | Visibility string `json:"visibility"` | 160 | Visibility string `json:"visibility"` |
| 161 | Description string `json:"description,omitempty"` | ||
| 148 | } | 162 | } |
| 149 | var ds []out | 163 | var ds []out |
| 150 | for _, r := range repos { | 164 | for _, r := range repos { |
| 151 | ds = append(ds, out{r.Path(), r.Visibility}) | 165 | desc := gitutil.ReadDescription(RepoDir(c.Cfg.Server.Root, r.OwnerName, r.Name)) |
| 166 | ds = append(ds, out{r.Path(), r.Visibility, desc}) | ||
| 152 | } | 167 | } |
| 153 | return c.emit(ds, func(w io.Writer) { | 168 | return c.emit(ds, func(w io.Writer) { |
| 154 | for _, d := range ds { | 169 | for _, d := range ds { |
| 155 | fmt.Fprintf(w, "%s\t%s\n", d.Path, d.Visibility) | 170 | fmt.Fprintf(w, "%s\t%s\t%s\n", d.Path, d.Visibility, d.Description) |
| 156 | } | 171 | } |
| 157 | }) | 172 | }) |
| 158 | } | 173 | } |
| @@ -167,13 +182,18 @@ func runRepoShow(c *Ctx, args []string) int { | |||
| 167 | } | 182 | } |
| 168 | type out struct { | 183 | type out struct { |
| 169 | Path string `json:"path"` | 184 | Path string `json:"path"` |
| 185 | Description string `json:"description,omitempty"` | ||
| 170 | Visibility string `json:"visibility"` | 186 | Visibility string `json:"visibility"` |
| 171 | DefaultBranch string `json:"default_branch"` | 187 | DefaultBranch string `json:"default_branch"` |
| 172 | ProtectedBranches []string `json:"protected_branches,omitempty"` | 188 | ProtectedBranches []string `json:"protected_branches,omitempty"` |
| 173 | } | 189 | } |
| 174 | d := out{repo.Path(), repo.Visibility, repo.DefaultBranch, repo.Settings.ProtectedBranches} | 190 | desc := gitutil.ReadDescription(RepoDir(c.Cfg.Server.Root, repo.OwnerName, repo.Name)) |
| 191 | d := out{repo.Path(), desc, repo.Visibility, repo.DefaultBranch, repo.Settings.ProtectedBranches} | ||
| 175 | return c.emit(d, func(w io.Writer) { | 192 | return c.emit(d, func(w io.Writer) { |
| 176 | fmt.Fprintf(w, "%s\t%s\tdefault: %s\n", d.Path, d.Visibility, d.DefaultBranch) | 193 | fmt.Fprintf(w, "%s\t%s\tdefault: %s\n", d.Path, d.Visibility, d.DefaultBranch) |
| 194 | if d.Description != "" { | ||
| 195 | fmt.Fprintf(w, "%s\n", d.Description) | ||
| 196 | } | ||
| 177 | if len(d.ProtectedBranches) > 0 { | 197 | if len(d.ProtectedBranches) > 0 { |
| 178 | fmt.Fprintf(w, "protected: %s\n", strings.Join(d.ProtectedBranches, ", ")) | 198 | fmt.Fprintf(w, "protected: %s\n", strings.Join(d.ProtectedBranches, ", ")) |
| 179 | } | 199 | } |
| @@ -353,6 +373,23 @@ func runSettingsShow(c *Ctx, args []string) int { | |||
| 353 | }) | 373 | }) |
| 354 | } | 374 | } |
| 355 | 375 | ||
| 376 | func runSetDescription(c *Ctx, args []string) int { | ||
| 377 | if len(args) != 2 { | ||
| 378 | return c.fail(protocol.ExitUsage, "usage: repo settings description <owner/name> <text>") | ||
| 379 | } | ||
| 380 | repo, code := resolveRepo(c, args[0], policy.CanAdmin) | ||
| 381 | if code >= 0 { | ||
| 382 | return code | ||
| 383 | } | ||
| 384 | dir := RepoDir(c.Cfg.Server.Root, repo.OwnerName, repo.Name) | ||
| 385 | if err := gitutil.WriteDescription(dir, args[1]); err != nil { | ||
| 386 | return c.fail(protocol.ExitFailure, "%v", err) | ||
| 387 | } | ||
| 388 | return c.emit(map[string]string{"description": gitutil.ReadDescription(dir)}, func(w io.Writer) { | ||
| 389 | fmt.Fprintf(w, "description set on %s\n", repo.Path()) | ||
| 390 | }) | ||
| 391 | } | ||
| 392 | |||
| 356 | func runGitDaemon(c *Ctx, args []string) int { | 393 | func runGitDaemon(c *Ctx, args []string) int { |
| 357 | if len(args) != 2 || (args[1] != "on" && args[1] != "off") { | 394 | if len(args) != 2 || (args[1] != "on" && args[1] != "off") { |
| 358 | return c.fail(protocol.ExitUsage, "usage: repo settings git-daemon <owner/name> on|off") | 395 | return c.fail(protocol.ExitUsage, "usage: repo settings git-daemon <owner/name> on|off") |
internal/gitutil/gitutil.go +27
| @@ -146,3 +146,30 @@ func SetHead(dir, branch string) error { | |||
| 146 | } | 146 | } |
| 147 | return nil | 147 | return nil |
| 148 | } | 148 | } |
| 149 | |||
| 150 | // gitDefaultDescription is the placeholder git init writes; treated as no | ||
| 151 | // description at all. | ||
| 152 | const gitDefaultDescription = "Unnamed repository; edit this file 'description' to name the repository." | ||
| 153 | |||
| 154 | // ReadDescription returns the repo's description from the classic | ||
| 155 | // <repo>.git/description file, empty for the git-init placeholder. | ||
| 156 | func ReadDescription(dir string) string { | ||
| 157 | raw, err := os.ReadFile(filepath.Join(dir, "description")) | ||
| 158 | if err != nil { | ||
| 159 | return "" | ||
| 160 | } | ||
| 161 | desc := strings.TrimSpace(string(raw)) | ||
| 162 | if desc == gitDefaultDescription { | ||
| 163 | return "" | ||
| 164 | } | ||
| 165 | return desc | ||
| 166 | } | ||
| 167 | |||
| 168 | // WriteDescription sets the description file: first line only, capped. | ||
| 169 | func WriteDescription(dir, desc string) error { | ||
| 170 | desc, _, _ = strings.Cut(strings.TrimSpace(desc), "\n") | ||
| 171 | if len(desc) > 256 { | ||
| 172 | desc = desc[:256] | ||
| 173 | } | ||
| 174 | return os.WriteFile(filepath.Join(dir, "description"), []byte(desc+"\n"), 0o644) | ||
| 175 | } | ||
internal/httpd/web.go +21 −5
| @@ -48,6 +48,20 @@ func (s *Server) stylesheet(w http.ResponseWriter, r *http.Request) { | |||
| 48 | w.Write(web.StyleCSS) | 48 | w.Write(web.StyleCSS) |
| 49 | } | 49 | } |
| 50 | 50 | ||
| 51 | // describedRepo pairs a repo with its description for listings. | ||
| 52 | type describedRepo struct { | ||
| 53 | store.Repo | ||
| 54 | Desc string | ||
| 55 | } | ||
| 56 | |||
| 57 | func (s *Server) describeAll(repos []store.Repo) []describedRepo { | ||
| 58 | var out []describedRepo | ||
| 59 | for _, r := range repos { | ||
| 60 | out = append(out, describedRepo{r, gitutil.ReadDescription(control.RepoDir(s.cfg.Server.Root, r.OwnerName, r.Name))}) | ||
| 61 | } | ||
| 62 | return out | ||
| 63 | } | ||
| 64 | |||
| 51 | func (s *Server) index(w http.ResponseWriter, r *http.Request) { | 65 | func (s *Server) index(w http.ResponseWriter, r *http.Request) { |
| 52 | repos, err := s.st.ListPublicRepos() | 66 | repos, err := s.st.ListPublicRepos() |
| 53 | if err != nil { | 67 | if err != nil { |
| @@ -71,15 +85,16 @@ func (s *Server) index(w http.ResponseWriter, r *http.Request) { | |||
| 71 | s.render(w, "index.html", struct { | 85 | s.render(w, "index.html", struct { |
| 72 | Site string | 86 | Site string |
| 73 | Viewer string | 87 | Viewer string |
| 74 | Repos []store.Repo | 88 | Repos []describedRepo |
| 75 | Mine []store.Repo | 89 | Mine []describedRepo |
| 76 | }{s.siteName(), viewer.Username, repos, mine}) | 90 | }{s.siteName(), viewer.Username, s.describeAll(repos), s.describeAll(mine)}) |
| 77 | } | 91 | } |
| 78 | 92 | ||
| 79 | // repoPage is the shared context for repo-scoped pages. | 93 | // repoPage is the shared context for repo-scoped pages. |
| 80 | type repoPage struct { | 94 | type repoPage struct { |
| 81 | Site string | 95 | Site string |
| 82 | Viewer string | 96 | Viewer string |
| 97 | Desc string | ||
| 83 | Repo store.Repo | 98 | Repo store.Repo |
| 84 | Ref string | 99 | Ref string |
| 85 | CloneURL string | 100 | CloneURL string |
| @@ -115,6 +130,7 @@ func (s *Server) repoFor(w http.ResponseWriter, r *http.Request, ref string) (re | |||
| 115 | return repoPage{ | 130 | return repoPage{ |
| 116 | Site: s.siteName(), | 131 | Site: s.siteName(), |
| 117 | Viewer: viewer.Username, | 132 | Viewer: viewer.Username, |
| 133 | Desc: gitutil.ReadDescription(control.RepoDir(s.cfg.Server.Root, repo.OwnerName, repo.Name)), | ||
| 118 | Repo: repo, | 134 | Repo: repo, |
| 119 | Ref: ref, | 135 | Ref: ref, |
| 120 | CloneURL: s.cfg.Server.SiteURL + "/" + repo.Path() + ".git", | 136 | CloneURL: s.cfg.Server.SiteURL + "/" + repo.Path() + ".git", |
| @@ -186,10 +202,10 @@ func (s *Server) ownerPage(w http.ResponseWriter, r *http.Request) { | |||
| 186 | Viewer string | 202 | Viewer string |
| 187 | Owner string | 203 | Owner string |
| 188 | Kind string | 204 | Kind string |
| 189 | Repos []store.Repo | 205 | Repos []describedRepo |
| 190 | Members []store.OrgMember | 206 | Members []store.OrgMember |
| 191 | Orgs []store.OrgMember | 207 | Orgs []store.OrgMember |
| 192 | }{s.siteName(), viewer.Username, name, kind, visible, members, orgs}) | 208 | }{s.siteName(), viewer.Username, name, kind, s.describeAll(visible), members, orgs}) |
| 193 | } | 209 | } |
| 194 | 210 | ||
| 195 | func (s *Server) repoHome(w http.ResponseWriter, r *http.Request) { | 211 | func (s *Server) repoHome(w http.ResponseWriter, r *http.Request) { |
internal/web/static/style.css +1
| @@ -45,6 +45,7 @@ pre.diff .del { color: var(--bad); } | |||
| 45 | pre.diff .hunk { color: var(--link); } | 45 | pre.diff .hunk { color: var(--link); } |
| 46 | pre.diff .meta { color: var(--muted); } | 46 | pre.diff .meta { color: var(--muted); } |
| 47 | .error { color: var(--bad); } | 47 | .error { color: var(--bad); } |
| 48 | .desc, td.desc { color: var(--muted); } | ||
| 48 | .badge { | 49 | .badge { |
| 49 | display: inline-block; padding: 0.05rem 0.5rem; border-radius: 10px; | 50 | display: inline-block; padding: 0.05rem 0.5rem; border-radius: 10px; |
| 50 | font-size: 12px; border: 1px solid; | 51 | font-size: 12px; border: 1px solid; |
internal/web/templates/index.html +2 −2
| @@ -4,12 +4,12 @@ | |||
| 4 | <form method="post" action="/logout" style="display:inline"><button type="submit">logout</button></form></p>{{end}} | 4 | <form method="post" action="/logout" style="display:inline"><button type="submit">logout</button></form></p>{{end}} |
| 5 | <h1>repositories</h1> | 5 | <h1>repositories</h1> |
| 6 | <table> | 6 | <table> |
| 7 | {{range .Repos}}<tr><td><a href="/{{.OwnerName}}">{{.OwnerName}}</a>/<a href="/{{.OwnerName}}/{{.Name}}">{{.Name}}</a></td><td>{{.DefaultBranch}}</td></tr> | 7 | {{range .Repos}}<tr><td><a href="/{{.OwnerName}}">{{.OwnerName}}</a>/<a href="/{{.OwnerName}}/{{.Name}}">{{.Name}}</a></td><td class="desc">{{.Desc}}</td><td>{{.DefaultBranch}}</td></tr> |
| 8 | {{else}}<tr><td>no public repositories</td></tr>{{end}} | 8 | {{else}}<tr><td>no public repositories</td></tr>{{end}} |
| 9 | </table> | 9 | </table> |
| 10 | {{if .Mine}}<h2>your private repositories</h2> | 10 | {{if .Mine}}<h2>your private repositories</h2> |
| 11 | <table> | 11 | <table> |
| 12 | {{range .Mine}}<tr><td><a href="/{{.OwnerName}}/{{.Name}}">{{.OwnerName}}/{{.Name}}</a></td><td>{{.Visibility}}</td></tr> | 12 | {{range .Mine}}<tr><td><a href="/{{.OwnerName}}/{{.Name}}">{{.OwnerName}}/{{.Name}}</a></td><td class="desc">{{.Desc}}</td><td>{{.Visibility}}</td></tr> |
| 13 | {{end}} | 13 | {{end}} |
| 14 | </table>{{end}} | 14 | </table>{{end}} |
| 15 | {{end}} | 15 | {{end}} |
internal/web/templates/layout.html +1
| @@ -18,6 +18,7 @@ | |||
| 18 | 18 | ||
| 19 | {{define "repoheader"}} | 19 | {{define "repoheader"}} |
| 20 | <h1><a href="/{{.Repo.OwnerName}}">{{.Repo.OwnerName}}</a>/<a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}">{{.Repo.Name}}</a></h1> | 20 | <h1><a href="/{{.Repo.OwnerName}}">{{.Repo.OwnerName}}</a>/<a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}">{{.Repo.Name}}</a></h1> |
| 21 | {{if .Desc}}<p class="desc">{{.Desc}}</p>{{end}} | ||
| 21 | <nav class="tabs"> | 22 | <nav class="tabs"> |
| 22 | <a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}">files</a> | 23 | <a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}">files</a> |
| 23 | <a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/log">log</a> | 24 | <a href="/{{.Repo.OwnerName}}/{{.Repo.Name}}/log">log</a> |
internal/web/templates/owner.html +1
| @@ -6,6 +6,7 @@ | |||
| 6 | <table> | 6 | <table> |
| 7 | {{range .Repos}}<tr> | 7 | {{range .Repos}}<tr> |
| 8 | <td><a href="/{{.OwnerName}}/{{.Name}}">{{.Name}}</a></td> | 8 | <td><a href="/{{.OwnerName}}/{{.Name}}">{{.Name}}</a></td> |
| 9 | <td class="desc">{{.Desc}}</td> | ||
| 9 | <td>{{.Visibility}}</td> | 10 | <td>{{.Visibility}}</td> |
| 10 | <td>{{.DefaultBranch}}</td> | 11 | <td>{{.DefaultBranch}}</td> |
| 11 | </tr> | 12 | </tr> |