feat(org): add organisations with members, distinct profile and owner select on create
10 files changed, +202 −11
- extra.go modified +6 −2
- main.go modified +24 −0
- search.go modified +2 −1
- static/style.css modified +31 −0
- store.go modified +75 −1
- templates/create.html modified +1 −0
- templates/org.html added +29 −0
- templates/search.html modified +1 −1
- templates/user.html modified +1 −0
- web.go modified +32 −6
extra.go modified
| @@ -451,12 +451,16 @@ func (s *Server) activityCounts(owner User) map[string]int { | ||
| 451 | 451 | activities.Unlock() |
| 452 | 452 | counts := map[string]int{} |
| 453 | 453 | email := strings.ToLower(s.store.VerifiedEmail(owner.ID)) |
| 454 | - if email == "" { | |
| 454 | + org := s.store.IsOrg(owner.ID) | |
| 455 | + if email == "" && !org { | |
| 455 | 456 | return counts |
| 456 | 457 | } |
| 457 | 458 | seen := map[string]bool{} |
| 458 | 459 | repos, _ := s.store.PublicRepos("", "") |
| 459 | 460 | for _, repo := range repos { |
| 461 | + if org && repo.Owner.ID != owner.ID { | |
| 462 | + continue | |
| 463 | + } | |
| 460 | 464 | g := Git{Dir: s.repoDir(repo)} |
| 461 | 465 | out, err := g.run(nil, "log", "--all", "--no-merges", "--format=%H%x09%at%x09%ae") |
| 462 | 466 | if err != nil { |
| @@ -464,7 +468,7 @@ func (s *Server) activityCounts(owner User) map[string]int { | ||
| 464 | 468 | } |
| 465 | 469 | for _, line := range strings.Split(string(out), "\n") { |
| 466 | 470 | 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]] { | |
| 468 | 472 | continue |
| 469 | 473 | } |
| 470 | 474 | seen[f[0]] = true |
main.go modified
| @@ -27,6 +27,7 @@ func main() { | ||
| 27 | 27 | ban name | unban name block or restore an account |
| 28 | 28 | delete name remove an account and its repositories |
| 29 | 29 | 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 | |
| 30 | 31 | mail address send a test message through the configured mailer |
| 31 | 32 | backup file write a consistent copy of the database`) |
| 32 | 33 | } |
| @@ -114,6 +115,29 @@ func main() { | ||
| 114 | 115 | log.Fatal(err) |
| 115 | 116 | } |
| 116 | 117 | 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 | + } | |
| 117 | 141 | case "feature": |
| 118 | 142 | if len(args) < 2 { |
| 119 | 143 | os.Exit(2) |
search.go modified
| @@ -120,6 +120,7 @@ type userHit struct { | ||
| 120 | 120 | User |
| 121 | 121 | Profile Profile |
| 122 | 122 | Repos int |
| 123 | + Org bool | |
| 123 | 124 | score int |
| 124 | 125 | } |
| 125 | 126 | |
| @@ -241,7 +242,7 @@ func (s *Server) find(w http.ResponseWriter, r *http.Request) { | ||
| 241 | 242 | continue |
| 242 | 243 | } |
| 243 | 244 | 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} | |
| 245 | 246 | for _, repo := range owned { |
| 246 | 247 | if repo.Unlisted { |
| 247 | 248 | continue |
static/style.css modified
| @@ -402,3 +402,34 @@ pre.rangediff,pre.body{white-space:pre-wrap;overflow-wrap:anywhere} | ||
| 402 | 402 | .dashhead{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--line);margin-bottom:.6rem;padding-bottom:.3rem} |
| 403 | 403 | .dashhead h3{border:0;margin:0;padding:0} |
| 404 | 404 | @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{ | ||
| 76 | 76 | `alter table users add column recovery text not null default ''`, |
| 77 | 77 | `alter table pulls add column draft integer not null default 0`, |
| 78 | 78 | `alter table repos add column featured integer not null default 0`, |
| 79 | + `alter table users add column org integer not null default 0`, | |
| 79 | 80 | } |
| 80 | 81 | |
| 81 | 82 | const extraSchema = ` |
| @@ -149,6 +150,12 @@ create table if not exists issue_comments ( | ||
| 149 | 150 | ` |
| 150 | 151 | |
| 151 | 152 | 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 | +); | |
| 152 | 159 | create table if not exists collaborators ( |
| 153 | 160 | repo integer not null references repos(id) on delete cascade, |
| 154 | 161 | user integer not null references users(id) on delete cascade, |
| @@ -733,7 +740,74 @@ func (s *Store) CanWrite(repo Repo, user *User) bool { | ||
| 733 | 740 | if user == nil { |
| 734 | 741 | return false |
| 735 | 742 | } |
| 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) | |
| 737 | 811 | } |
| 738 | 812 | |
| 739 | 813 | func (s *Store) CanRead(repo Repo, user *User) bool { |
templates/create.html modified
| @@ -2,6 +2,7 @@ | ||
| 2 | 2 | <h3>Create new repository</h3> |
| 3 | 3 | <form method="post" action="/create" class="twocol"> |
| 4 | 4 | <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}} | |
| 5 | 6 | <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> |
| 6 | 7 | <label>Description <input name="description" maxlength="200" placeholder="Short description, shown in lists" value="{{.Data.Description}}"></label> |
| 7 | 8 | <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 @@ | ||
| 24 | 24 | {{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}} |
| 25 | 25 | {{else}} |
| 26 | 26 | <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}} | |
| 28 | 28 | {{end}} |
| 29 | 29 | </div> |
| 30 | 30 | </div> |
templates/user.html modified
| @@ -16,6 +16,7 @@ | ||
| 16 | 16 | {{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}} |
| 17 | 17 | <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> |
| 18 | 18 | </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}} | |
| 19 | 20 | {{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}} |
| 20 | 21 | </aside> |
| 21 | 22 | <div class="main"> |
web.go modified
| @@ -342,7 +342,7 @@ func (s *Server) require(w http.ResponseWriter, r *http.Request) *User { | ||
| 342 | 342 | |
| 343 | 343 | func (s *Server) createForm(w http.ResponseWriter, r *http.Request) { |
| 344 | 344 | 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")}}) | |
| 346 | 346 | } |
| 347 | 347 | } |
| 348 | 348 | |
| @@ -351,11 +351,20 @@ func (s *Server) create(w http.ResponseWriter, r *http.Request) { | ||
| 351 | 351 | if u == nil { |
| 352 | 352 | return |
| 353 | 353 | } |
| 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 | + } | |
| 354 | 363 | 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")) | |
| 356 | 365 | if err != nil { |
| 357 | 366 | 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")}}) | |
| 359 | 368 | return |
| 360 | 369 | } |
| 361 | 370 | dir := s.repoDir(repo) |
| @@ -744,7 +753,9 @@ func (s *Server) route(w http.ResponseWriter, r *http.Request) { | ||
| 744 | 753 | } |
| 745 | 754 | user := s.current(r) |
| 746 | 755 | 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) | |
| 748 | 759 | if err != nil { |
| 749 | 760 | s.fail(w, r, 500, err.Error()) |
| 750 | 761 | return |
| @@ -760,7 +771,22 @@ func (s *Server) route(w http.ResponseWriter, r *http.Request) { | ||
| 760 | 771 | if year < 1970 || year > time.Now().Year() { |
| 761 | 772 | year = 0 |
| 762 | 773 | } |
| 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}) | |
| 764 | 790 | return |
| 765 | 791 | } |
| 766 | 792 | name := parts[1] |
| @@ -911,7 +937,7 @@ type repoPage struct { | ||
| 911 | 937 | func (s *Server) repoPage(r *http.Request, repo Repo, ref string) (repoPage, Git, error) { |
| 912 | 938 | g := Git{Dir: s.repoDir(repo)} |
| 913 | 939 | 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)} | |
| 915 | 941 | if user != nil { |
| 916 | 942 | rp.Starred = s.store.Starred(user.ID, repo.ID) |
| 917 | 943 | } |