vole
Clone
Read only, no credentials:
https://rodent.software/git/vole.git
Write over SSH with a registered key:
git clone ssh://git@0.0.0.0:2222/vole.git
A free and open-source forge that lives in your Git repo.
Let instance admins create repositories
455835e7b5d329ca64c96234dfffc7b6702f370e · Keenan Nemetz · 15d ago · parent 395554f4
Let instance admins create repositories The Admin section gains a New repository form: name, owner, and description. Creation uses the same store method as the CLI, seeds the owner as repo admin, updates the index, is audited, and lands on the new repository's access editor. This completes the instance-admin slice in the UI plan.
server/web/admin_repos.go +83
| @@ -0,0 +1,83 @@ | |||
| 1 | package web | ||
| 2 | |||
| 3 | // Creating a repository is an instance-admin operation because it chooses the | ||
| 4 | // owner and the name that every clone URL will use. After creation the admin is | ||
| 5 | // sent straight to the access editor, which is the next thing they usually need. | ||
| 6 | |||
| 7 | import ( | ||
| 8 | "net/http" | ||
| 9 | "strings" | ||
| 10 | |||
| 11 | "github.com/rodent-software/vole/forge/identity" | ||
| 12 | ) | ||
| 13 | |||
| 14 | type adminRepoNewView struct { | ||
| 15 | Page | ||
| 16 | Accounts []*identity.Account | ||
| 17 | Name string | ||
| 18 | Description string | ||
| 19 | Owner string | ||
| 20 | Error string | ||
| 21 | } | ||
| 22 | |||
| 23 | func (w *Web) adminRepoNewView(r *http.Request, msg string) adminRepoNewView { | ||
| 24 | var accounts []*identity.Account | ||
| 25 | for _, acct := range w.Ident.All() { | ||
| 26 | if acct.Active() { | ||
| 27 | accounts = append(accounts, acct) | ||
| 28 | } | ||
| 29 | } | ||
| 30 | return adminRepoNewView{Page: w.page(r, "New repository"), Accounts: accounts, Error: msg} | ||
| 31 | } | ||
| 32 | |||
| 33 | func (w *Web) AdminNewRepoPage(rw http.ResponseWriter, r *http.Request) { | ||
| 34 | if _, ok := w.requireInstanceAdmin(rw, r); !ok { | ||
| 35 | return | ||
| 36 | } | ||
| 37 | w.render(rw, "admin_repo_new", w.adminRepoNewView(r, ""), 0) | ||
| 38 | } | ||
| 39 | |||
| 40 | func (w *Web) CreateAdminRepo(rw http.ResponseWriter, r *http.Request) { | ||
| 41 | pr, ok := w.requireInstanceAdmin(rw, r) | ||
| 42 | if !ok { | ||
| 43 | return | ||
| 44 | } | ||
| 45 | r.Body = http.MaxBytesReader(rw, r.Body, 1<<20) | ||
| 46 | if !w.checkCSRF(r) { | ||
| 47 | http.Error(rw, "bad request", http.StatusForbidden) | ||
| 48 | return | ||
| 49 | } | ||
| 50 | if err := r.ParseForm(); err != nil { | ||
| 51 | http.Error(rw, "cannot read the form", http.StatusBadRequest) | ||
| 52 | return | ||
| 53 | } | ||
| 54 | name := strings.TrimSpace(r.PostFormValue("name")) | ||
| 55 | ownerName := strings.TrimSpace(r.PostFormValue("owner")) | ||
| 56 | description := strings.TrimSpace(r.PostFormValue("description")) | ||
| 57 | |||
| 58 | view := w.adminRepoNewView(r, "") | ||
| 59 | view.Name, view.Owner, view.Description = name, ownerName, description | ||
| 60 | fail := func(msg string) { | ||
| 61 | view.Error = msg | ||
| 62 | w.render(rw, "admin_repo_new", view, http.StatusBadRequest) | ||
| 63 | } | ||
| 64 | |||
| 65 | owner, err := w.Ident.Find(ownerName) | ||
| 66 | if err != nil || !owner.Active() { | ||
| 67 | fail("Choose an active account as the owner.") | ||
| 68 | return | ||
| 69 | } | ||
| 70 | m, err := w.Repos.Create(name, owner.ID, description) | ||
| 71 | if err != nil { | ||
| 72 | fail(err.Error()) | ||
| 73 | return | ||
| 74 | } | ||
| 75 | if w.Index != nil { | ||
| 76 | if err := w.Index.Upsert(m); err != nil { | ||
| 77 | w.serverError(rw, r, "repository created but the index was not updated: "+err.Error()) | ||
| 78 | return | ||
| 79 | } | ||
| 80 | } | ||
| 81 | w.audit("web.admin.repo.create", pr.account.ID, m.Name+" owner="+owner.Name) | ||
| 82 | http.Redirect(rw, r, "/admin/repos/"+m.Name+"/access", http.StatusSeeOther) | ||
| 83 | } | ||