~gitpocket/gitpocket

feat(org): add organisations with members, distinct profile and owner select on create

commit
9a8f9e1065ff1150b89cfb086ebbb031a5a2e947 browse tree
parent
f7c873c7c4451f0795f0c261be37d1afa410e092
author
james <j@cpl0.zip>
signature
Verified ~james signed with a key registered by ~james
date
2026-11-13 10:47 UTC

10 files changed, +202 −11

extra.go modified

@@ -451,12 +451,16 @@ func (s *Server) activityCounts(owner User) map[string]int {
451451 activities.Unlock()
452452 counts := map[string]int{}
453453 email := strings.ToLower(s.store.VerifiedEmail(owner.ID))
454- if email == "" {
454+ org := s.store.IsOrg(owner.ID)
455+ if email == "" && !org {
455456 return counts
456457 }
457458 seen := map[string]bool{}
458459 repos, _ := s.store.PublicRepos("", "")
459460 for _, repo := range repos {
461+ if org && repo.Owner.ID != owner.ID {
462+ continue
463+ }
460464 g := Git{Dir: s.repoDir(repo)}
461465 out, err := g.run(nil, "log", "--all", "--no-merges", "--format=%H%x09%at%x09%ae")
462466 if err != nil {
@@ -464,7 +468,7 @@ func (s *Server) activityCounts(owner User) map[string]int {
464468 }
465469 for _, line := range strings.Split(string(out), "\n") {
466470 f := strings.Split(line, "\t")
467- if len(f) != 3 || strings.ToLower(f[2]) != email || seen[f[0]] {
471+ if len(f) != 3 || (!org && strings.ToLower(f[2]) != email) || seen[f[0]] {
468472 continue
469473 }
470474 seen[f[0]] = true

main.go modified

@@ -27,6 +27,7 @@ func main() {
2727 ban name | unban name block or restore an account
2828 delete name remove an account and its repositories
2929 feature owner/repo... | -owner/repo set or clear the featured projects, in order
30+ org name [+user|-user]... create an organisation and add or remove members
3031 mail address send a test message through the configured mailer
3132 backup file write a consistent copy of the database`)
3233 }
@@ -114,6 +115,29 @@ func main() {
114115 log.Fatal(err)
115116 }
116117 fmt.Println("sent")
118+ case "org":
119+ if len(args) < 2 {
120+ os.Exit(2)
121+ }
122+ store := mustStore(*root)
123+ org, err := store.UserByName(args[1])
124+ if err != nil {
125+ if org, err = store.CreateOrg(args[1]); err != nil {
126+ log.Fatal(err)
127+ }
128+ } else if !store.IsOrg(org.ID) {
129+ log.Fatalf("~%s is a user, not an organisation", args[1])
130+ }
131+ for _, spec := range args[2:] {
132+ name := strings.TrimPrefix(strings.TrimLeft(spec, "+-"), "~")
133+ u, err := store.UserByName(name)
134+ if err != nil || store.IsOrg(u.ID) {
135+ log.Fatalf("%s: no such user", spec)
136+ }
137+ if err := store.SetMember(org.ID, u.ID, !strings.HasPrefix(spec, "-")); err != nil {
138+ log.Fatal(err)
139+ }
140+ }
117141 case "feature":
118142 if len(args) < 2 {
119143 os.Exit(2)

search.go modified

@@ -120,6 +120,7 @@ type userHit struct {
120120 User
121121 Profile Profile
122122 Repos int
123+ Org bool
123124 score int
124125 }
125126
@@ -241,7 +242,7 @@ func (s *Server) find(w http.ResponseWriter, r *http.Request) {
241242 continue
242243 }
243244 owned, _ := s.store.ReposByOwner(u.ID, false)
244- hit := userHit{User: u, Profile: p, score: score}
245+ hit := userHit{User: u, Profile: p, Org: s.store.IsOrg(u.ID), score: score}
245246 for _, repo := range owned {
246247 if repo.Unlisted {
247248 continue

static/style.css modified

@@ -402,3 +402,34 @@ pre.rangediff,pre.body{white-space:pre-wrap;overflow-wrap:anywhere}
402402 .dashhead{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--line);margin-bottom:.6rem;padding-bottom:.3rem}
403403 .dashhead h3{border:0;margin:0;padding:0}
404404 @media(max-width:800px){.dash{grid-template-columns:1fr}}
405+.orghead{display:flex;align-items:flex-start;gap:1.4rem;margin:-1rem 0 1.6rem;padding:1.4rem 1.5rem;background:var(--accentsoft);border-left:4px solid var(--accent)}
406+.orghead .btn{margin-left:auto;flex:none}
407+.orgmark img,.orgmark span{display:flex;width:5.5rem;height:5.5rem;border-radius:2px;object-fit:cover}
408+.orgmark span,.people li span,.orglist span{align-items:center;justify-content:center;background:var(--accent);color:#fff;font-weight:600;text-transform:uppercase}
409+.orgmark span{font-size:2.4rem}
410+.orgident{min-width:0}
411+.orgident h2{display:flex;align-items:center;flex-wrap:wrap;gap:.6rem;font-size:1.7rem;margin:0}
412+.orgident .handle{color:var(--muted);margin:.1rem 0 .5rem}
413+.orgident .bio{margin:0 0 .5rem;max-width:44rem}
414+.orgident .bio p{margin:0}
415+.orgbadge{display:inline-block;border:1px solid var(--accent);color:var(--accent);border-radius:3px;padding:0 .4rem;font-size:.72rem;font-weight:600;letter-spacing:.04em;text-transform:uppercase;vertical-align:middle;line-height:1.5}
416+.facts.inline{display:flex;flex-wrap:wrap;gap:.2rem 1.2rem;margin:0}
417+.columns.org{grid-template-columns:minmax(0,1fr) 220px;gap:2rem}
418+.columns.org h3 .count,.people h3 .count{color:var(--muted);font-weight:400;font-size:.9rem;margin-left:.3rem}
419+.orgrepos{display:grid;grid-template-columns:repeat(auto-fill,minmax(16rem,1fr));gap:.7rem;margin-bottom:1.6rem}
420+.orgrepo{display:flex;flex-direction:column;gap:.3rem;padding:.8rem .9rem;border:1px solid var(--line);border-top:3px solid var(--accent);color:var(--fg);text-decoration:none}
421+.orgrepo:hover{background:var(--accentsoft)}
422+.orgrepo strong{color:var(--accent);font-size:1.05rem}
423+.orgrepo .desc{font-size:.93rem}
424+.orgrepo .meta{margin-top:auto;color:var(--muted);font-size:.82rem}
425+.people ul{list-style:none;margin:0;padding:0}
426+.people li a{display:grid;grid-template-columns:2.4rem 1fr;grid-template-rows:auto auto;column-gap:.6rem;align-items:center;padding:.35rem 0;color:var(--fg);text-decoration:none}
427+.people li img,.people li span{grid-row:1/3;display:flex;width:2.4rem;height:2.4rem;border-radius:50%;object-fit:cover}
428+.people li b{font-weight:600;font-size:.95rem}
429+.people li small{color:var(--muted)}
430+.orglist{margin:0 0 1rem}
431+.orglist h4{font-size:.85rem;color:var(--muted);margin:0 0 .4rem;font-weight:600}
432+.orglist a{display:flex;align-items:center;gap:.5rem;color:var(--fg);margin:.3rem 0}
433+.orglist img,.orglist span{display:flex;width:1.6rem;height:1.6rem;border-radius:2px;object-fit:cover;font-size:.8rem}
434+.hit.org .hitavatar span{background:var(--accent);color:#fff;border-radius:2px}
435+@media (max-width:800px){.orghead{flex-wrap:wrap;padding:1rem}.orghead .btn{margin-left:0}.columns.org{grid-template-columns:1fr}}

store.go modified

@@ -76,6 +76,7 @@ var migrations = []string{
7676 `alter table users add column recovery text not null default ''`,
7777 `alter table pulls add column draft integer not null default 0`,
7878 `alter table repos add column featured integer not null default 0`,
79+ `alter table users add column org integer not null default 0`,
7980 }
8081
8182 const extraSchema = `
@@ -149,6 +150,12 @@ create table if not exists issue_comments (
149150 `
150151
151152 const collabSchema = `
153+create table if not exists members (
154+ org integer not null references users(id) on delete cascade,
155+ user integer not null references users(id) on delete cascade,
156+ created integer not null,
157+ primary key(org, user)
158+);
152159 create table if not exists collaborators (
153160 repo integer not null references repos(id) on delete cascade,
154161 user integer not null references users(id) on delete cascade,
@@ -733,7 +740,74 @@ func (s *Store) CanWrite(repo Repo, user *User) bool {
733740 if user == nil {
734741 return false
735742 }
736- return user.ID == repo.Owner.ID || s.IsCollaborator(repo.ID, user.ID)
743+ return s.Owns(repo, user) || s.IsCollaborator(repo.ID, user.ID)
744+}
745+
746+func (s *Store) Owns(repo Repo, user *User) bool {
747+ return user != nil && (user.ID == repo.Owner.ID || s.IsMember(repo.Owner.ID, user.ID))
748+}
749+
750+func (s *Store) IsOrg(id int64) bool {
751+ var org bool
752+ s.db.QueryRow(`select org from users where id = ?`, id).Scan(&org)
753+ return org
754+}
755+
756+func (s *Store) IsMember(org, user int64) bool {
757+ var n int
758+ s.db.QueryRow(`select count(*) from members where org = ? and user = ?`, org, user).Scan(&n)
759+ return n > 0
760+}
761+
762+func (s *Store) CreateOrg(name string) (User, error) {
763+ if !validName(name) {
764+ return User{}, errors.New("Names are lowercase letters, digits, dot, dash or underscore, at most 39 characters")
765+ }
766+ now := time.Now()
767+ res, err := s.db.Exec(`insert into users(name, hash, created, org) values (?, '!', ?, 1)`, name, now.Unix())
768+ if err != nil {
769+ if strings.Contains(err.Error(), "UNIQUE") {
770+ return User{}, errors.New("That name is taken")
771+ }
772+ return User{}, err
773+ }
774+ id, _ := res.LastInsertId()
775+ return User{ID: id, Name: name, Created: now}, nil
776+}
777+
778+func (s *Store) SetMember(org, user int64, member bool) error {
779+ if !member {
780+ _, err := s.db.Exec(`delete from members where org = ? and user = ?`, org, user)
781+ return err
782+ }
783+ _, err := s.db.Exec(`insert or ignore into members(org, user, created) values (?, ?, ?)`, org, user, time.Now().Unix())
784+ return err
785+}
786+
787+func (s *Store) userList(query string, id int64) []User {
788+ rows, err := s.db.Query(query, id)
789+ if err != nil {
790+ return nil
791+ }
792+ defer rows.Close()
793+ var users []User
794+ for rows.Next() {
795+ var u User
796+ var created int64
797+ if rows.Scan(&u.ID, &u.Name, &created) == nil {
798+ u.Created = time.Unix(created, 0)
799+ users = append(users, u)
800+ }
801+ }
802+ return users
803+}
804+
805+func (s *Store) Members(org int64) []User {
806+ return s.userList(`select u.id, u.name, u.created from members m join users u on u.id = m.user where m.org = ? order by m.created, u.name`, org)
807+}
808+
809+func (s *Store) Orgs(user int64) []User {
810+ return s.userList(`select u.id, u.name, u.created from members m join users u on u.id = m.org where m.user = ? order by u.name`, user)
737811 }
738812
739813 func (s *Store) CanRead(repo Repo, user *User) bool {

templates/create.html modified

@@ -2,6 +2,7 @@
22 <h3>Create new repository</h3>
33 <form method="post" action="/create" class="twocol">
44 <div>
5+{{with .Data.Orgs}}<label>Owner <select name="owner"><option value="{{$.User.Name}}">~{{$.User.Name}}</option>{{range .}}<option value="{{.Name}}"{{if eq .Name $.Data.Owner}} selected{{end}}>~{{.Name}}</option>{{end}}</select></label>{{end}}
56 <label>Repository name <input name="name" required autofocus pattern="[a-z0-9][a-z0-9_.-]{0,38}" placeholder="my-cool-project" value="{{.Data.Name}}"></label>
67 <label>Description <input name="description" maxlength="200" placeholder="Short description, shown in lists" value="{{.Data.Description}}"></label>
78 <label>Tags <input name="tags" placeholder="Up to three comma-separated topics" value="{{.Data.Tags}}"></label>

templates/org.html added

@@ -0,0 +1,29 @@
1+{{define "content"}}
2+{{$p := .Data.Profile}}
3+{{$o := .Data.Owner}}
4+<header class="orghead">
5+<div class="orgmark">{{if $p.Avatar}}<img src="/~{{$o.Name}}/avatar?v={{$p.Avatar}}" alt="">{{else}}<span>{{slice $o.Name 0 1}}</span>{{end}}</div>
6+<div class="orgident">
7+<h2>{{with $p.Display}}{{.}}{{else}}{{$o.Name}}{{end}} <span class="orgbadge">Organisation</span></h2>
8+<p class="handle">~{{$o.Name}}</p>
9+{{with .Data.Bio}}<div class="bio readme">{{.}}</div>{{end}}
10+<ul class="facts inline">
11+{{if $p.Location}}<li><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M8 15s5-4.6 5-8.5A5 5 0 0 0 3 6.5C3 10.4 8 15 8 15z"/><circle cx="8" cy="6.5" r="1.8"/></svg>{{$p.Location}}</li>{{end}}
12+{{if $p.Website}}<li><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M6.8 9.2a3 3 0 0 0 4.2 0l2.2-2.2a3 3 0 0 0-4.2-4.2l-.9.9"/><path d="M9.2 6.8a3 3 0 0 0-4.2 0L2.8 9a3 3 0 0 0 4.2 4.2l.9-.9"/></svg><a href="{{$p.Website}}" rel="nofollow me">{{bare $p.Website}}</a></li>{{end}}
13+<li title="{{date $o.Created}}"><svg viewBox="0 0 16 16" aria-hidden="true"><rect x="2" y="3" width="12" height="11" rx="1.5"/><path d="M2 6.5h12M5.5 1.5v3M10.5 1.5v3"/></svg>Since {{$o.Created.Format "January 2006"}}</li>
14+</ul>
15+</div>
16+{{if .Data.Member}}<a class="btn primary" href="/create?owner={{$o.Name}}">New repository</a>{{end}}
17+</header>
18+<div class="columns org">
19+<div class="main">
20+<h3 class="plain">Repositories <span class="count">{{len .Data.Repos}}</span></h3>
21+{{if .Data.Repos}}<div class="orgrepos">{{range .Data.Repos}}<a class="orgrepo" href="/{{.Path}}"><strong>{{.Name}}</strong>{{if .Private}} <span class="tag">private</span>{{else if .Unlisted}} <span class="tag">unlisted</span>{{end}}{{if .Description}}<span class="desc">{{.Description}}</span>{{end}}<span class="meta">Updated {{ago .Updated}}{{if .Stars}} · ☆ {{.Stars}}{{end}}</span></a>{{end}}</div>{{else}}<p class="info">This organisation does not have any repositories.</p>{{end}}
22+{{with .Data.Activity}}<section class="activity"><div class="heathead"><p class="muted small">{{.Total}} commit{{if ne .Total 1}}s{{end}} across the organisation {{if .Year}}in {{.Year}}{{else}}in the past 12 months{{end}}</p><nav class="years"><a href="?"{{if not .Year}} class="active"{{end}}>past 12 months</a>{{range .Years}}<a href="?year={{.}}"{{if eq . $.Data.Activity.Year}} class="active"{{end}}>{{.}}</a>{{end}}</nav></div>{{.Grid}}<p class="legend muted small">less <i class="h0"></i><i class="h1"></i><i class="h2"></i><i class="h3"></i><i class="h4"></i> more</p></section>{{end}}
23+</div>
24+<aside class="people">
25+<h3 class="plain">People <span class="count">{{len .Data.Members}}</span></h3>
26+<ul>{{range .Data.Members}}<li><a href="/~{{.Name}}">{{if .Profile.Avatar}}<img src="/~{{.Name}}/avatar?v={{.Profile.Avatar}}" alt="">{{else}}<span>{{slice .Name 0 1}}</span>{{end}}<b>{{with .Profile.Display}}{{.}}{{else}}~{{.Name}}{{end}}</b><small>~{{.Name}}</small></a></li>{{end}}</ul>
27+</aside>
28+</div>
29+{{end}}

templates/search.html modified

@@ -24,7 +24,7 @@
2424 {{range $d.Repos}}<div class="hit"><h4><a class="owner" href="/~{{.Owner.Name}}">~{{.Owner.Name}}</a>/<a href="/{{.Path}}">{{.Name}}</a></h4>{{with .Description}}<p>{{.}}</p>{{end}}{{if .Tags}}<p class="tags">{{range .Tags}}<a href="/search?q=tag:{{.}}">#{{.}}</a>{{end}}</p>{{end}}<p class="meta">{{with .Language}}<span><i class="dot" style="background:{{.Colour}}"></i>{{.Name}}</span>{{end}}{{if .Stars}}<span>☆ {{.Stars}}</span>{{end}}<span>Updated {{ago .Updated}}</span></p></div>{{else}}<p class="info">No repositories match. Try a shorter query or clear the language filter.</p>{{end}}
2525 {{else}}
2626 <div class="resulthead"><h3 class="plain">{{len $d.Users}} user{{if ne (len $d.Users) 1}}s{{end}}{{with $d.Query}} matching <q>{{.}}</q>{{end}}{{with $d.Lang}} writing {{.}}{{end}}</h3></div>
27-{{range $d.Users}}<div class="hit user"><a href="/~{{.Name}}" class="hitavatar">{{if .Profile.Avatar}}<img src="/~{{.Name}}/avatar?v={{.Profile.Avatar}}" alt="">{{else}}<span>{{slice .Name 0 1}}</span>{{end}}</a><div><h4><a href="/~{{.Name}}">{{with .Profile.Display}}{{.}}{{else}}~{{.Name}}{{end}}</a> <span class="muted">~{{.Name}}</span></h4>{{with .Profile.Bio}}<p class="bio">{{.}}</p>{{end}}<p class="meta">{{with .Profile.Location}}<span>{{.}}</span>{{end}}<span>{{.Repos}} public repositor{{if eq .Repos 1}}y{{else}}ies{{end}}</span><span>Joined {{.Created.Format "January 2006"}}</span></p></div></div>{{else}}<p class="info">No users match.</p>{{end}}
27+{{range $d.Users}}<div class="hit user{{if .Org}} org{{end}}"><a href="/~{{.Name}}" class="hitavatar">{{if .Profile.Avatar}}<img src="/~{{.Name}}/avatar?v={{.Profile.Avatar}}" alt="">{{else}}<span>{{slice .Name 0 1}}</span>{{end}}</a><div><h4><a href="/~{{.Name}}">{{with .Profile.Display}}{{.}}{{else}}~{{.Name}}{{end}}</a> <span class="muted">~{{.Name}}</span>{{if .Org}} <span class="orgbadge">Organisation</span>{{end}}</h4>{{with .Profile.Bio}}<p class="bio">{{.}}</p>{{end}}<p class="meta">{{with .Profile.Location}}<span>{{.}}</span>{{end}}<span>{{.Repos}} public repositor{{if eq .Repos 1}}y{{else}}ies{{end}}</span><span>{{if .Org}}Since{{else}}Joined{{end}} {{.Created.Format "January 2006"}}</span></p></div></div>{{else}}<p class="info">No users match.</p>{{end}}
2828 {{end}}
2929 </div>
3030 </div>

templates/user.html modified

@@ -16,6 +16,7 @@
1616 {{if $p.Website}}<li><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M6.8 9.2a3 3 0 0 0 4.2 0l2.2-2.2a3 3 0 0 0-4.2-4.2l-.9.9"/><path d="M9.2 6.8a3 3 0 0 0-4.2 0L2.8 9a3 3 0 0 0 4.2 4.2l.9-.9"/></svg><a href="{{$p.Website}}" rel="nofollow me">{{bare $p.Website}}</a></li>{{end}}
1717 <li title="{{date .Data.Owner.Created}}"><svg viewBox="0 0 16 16" aria-hidden="true"><rect x="2" y="3" width="12" height="11" rx="1.5"/><path d="M2 6.5h12M5.5 1.5v3M10.5 1.5v3"/></svg>Joined {{.Data.Owner.Created.Format "January 2006"}}</li>
1818 </ul>
19+{{with .Data.Orgs}}<div class="orglist"><h4>Organisations</h4>{{range .}}<a href="/~{{.Name}}" title="~{{.Name}}">{{if .Profile.Avatar}}<img src="/~{{.Name}}/avatar?v={{.Profile.Avatar}}" alt="">{{else}}<span>{{slice .Name 0 1}}</span>{{end}}{{with .Profile.Display}}{{.}}{{else}}{{.Name}}{{end}}</a>{{end}}</div>{{end}}
1920 {{if $own}}<div class="ownactions"><a class="btn primary block" href="/create">New repository</a><a class="btn block" href="/settings">Edit profile</a></div>{{end}}
2021 </aside>
2122 <div class="main">

web.go modified

@@ -342,7 +342,7 @@ func (s *Server) require(w http.ResponseWriter, r *http.Request) *User {
342342
343343 func (s *Server) createForm(w http.ResponseWriter, r *http.Request) {
344344 if u := s.require(w, r); u != nil {
345- s.render(w, r, "create", page{Title: "New repository", User: u})
345+ s.render(w, r, "create", page{Title: "New repository", User: u, Data: map[string]any{"Orgs": s.store.Orgs(u.ID), "Owner": r.URL.Query().Get("owner")}})
346346 }
347347 }
348348
@@ -351,11 +351,20 @@ func (s *Server) create(w http.ResponseWriter, r *http.Request) {
351351 if u == nil {
352352 return
353353 }
354+ owner := *u
355+ if o := r.FormValue("owner"); o != "" && o != u.Name {
356+ org, err := s.store.UserByName(o)
357+ if err != nil || !s.store.IsOrg(org.ID) || !s.store.IsMember(org.ID, u.ID) {
358+ s.fail(w, r, 403, "You are not a member of that organisation")
359+ return
360+ }
361+ owner = org
362+ }
354363 name := strings.TrimSpace(r.FormValue("name"))
355- repo, err := s.store.CreateRepo(*u, name, strings.TrimSpace(r.FormValue("description")), visibility(r.FormValue("visibility")), r.FormValue("tags"))
364+ repo, err := s.store.CreateRepo(owner, name, strings.TrimSpace(r.FormValue("description")), visibility(r.FormValue("visibility")), r.FormValue("tags"))
356365 if err != nil {
357366 w.WriteHeader(400)
358- s.render(w, r, "create", page{Title: "New repository", User: u, Error: err.Error()})
367+ s.render(w, r, "create", page{Title: "New repository", User: u, Error: err.Error(), Data: map[string]any{"Orgs": s.store.Orgs(u.ID), "Owner": owner.Name, "Name": name, "Description": r.FormValue("description"), "Tags": r.FormValue("tags")}})
359368 return
360369 }
361370 dir := s.repoDir(repo)
@@ -744,7 +753,9 @@ func (s *Server) route(w http.ResponseWriter, r *http.Request) {
744753 }
745754 user := s.current(r)
746755 if len(parts) == 1 || (len(parts) == 2 && parts[1] == "") {
747- repos, err := s.store.ReposByOwner(owner.ID, user != nil && user.ID == owner.ID)
756+ org := s.store.IsOrg(owner.ID)
757+ member := user != nil && (user.ID == owner.ID || s.store.IsMember(owner.ID, user.ID))
758+ repos, err := s.store.ReposByOwner(owner.ID, member)
748759 if err != nil {
749760 s.fail(w, r, 500, err.Error())
750761 return
@@ -760,7 +771,22 @@ func (s *Server) route(w http.ResponseWriter, r *http.Request) {
760771 if year < 1970 || year > time.Now().Year() {
761772 year = 0
762773 }
763- s.render(w, r, "user", page{Title: "~" + owner.Name, User: user, Math: hasMath(bio), Data: map[string]any{"Owner": owner, "Repos": repos, "Profile": profile, "Bio": bio, "Count": len(starred), "Activity": s.activity(owner, year)}})
774+ data := map[string]any{"Owner": owner, "Repos": repos, "Profile": profile, "Bio": bio, "Count": len(starred), "Activity": s.activity(owner, year), "Member": member}
775+ if org {
776+ var people []userHit
777+ for _, m := range s.store.Members(owner.ID) {
778+ people = append(people, userHit{User: m, Profile: s.store.Profile(m.ID)})
779+ }
780+ data["Members"] = people
781+ s.render(w, r, "org", page{Title: "~" + owner.Name, User: user, Math: hasMath(bio), Data: data})
782+ return
783+ }
784+ var orgs []userHit
785+ for _, o := range s.store.Orgs(owner.ID) {
786+ orgs = append(orgs, userHit{User: o, Profile: s.store.Profile(o.ID)})
787+ }
788+ data["Orgs"] = orgs
789+ s.render(w, r, "user", page{Title: "~" + owner.Name, User: user, Math: hasMath(bio), Data: data})
764790 return
765791 }
766792 name := parts[1]
@@ -911,7 +937,7 @@ type repoPage struct {
911937 func (s *Server) repoPage(r *http.Request, repo Repo, ref string) (repoPage, Git, error) {
912938 g := Git{Dir: s.repoDir(repo)}
913939 user := s.current(r)
914- rp := repoPage{Repo: repo, Owner: s.store.CanWrite(repo, user), IsOwner: user != nil && user.ID == repo.Owner.ID, Open: s.store.OpenPullCount(repo.ID), OpenIssues: s.store.OpenIssueCount(repo.ID), User: user, Stars: s.store.StarCount(repo.ID)}
940+ rp := repoPage{Repo: repo, Owner: s.store.CanWrite(repo, user), IsOwner: s.store.Owns(repo, user), Open: s.store.OpenPullCount(repo.ID), OpenIssues: s.store.OpenIssueCount(repo.ID), User: user, Stars: s.store.StarCount(repo.ID)}
915941 if user != nil {
916942 rp.Starred = s.store.Starred(user.ID, repo.ID)
917943 }