package web
import (
"errors"
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"regexp"
"git.josie-c.com/josie/simplegit/internal/db"
"git.josie-c.com/josie/simplegit/internal/git"
)
// Repo names become URL path segments (/user/repo), same policy as usernames.
var repoNamePattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9_.-]*$`)
type newRepoData struct {
Username string
Name string
Description string
Visibility string
Error string
}
type createdData struct {
Username string
RepoName string
PushURL string
}
func (s *Server) handleNewRepoForm(w http.ResponseWriter, r *http.Request) {
user := requireUser(w, r)
if user == nil {
return
}
s.render(w, "new.html", http.StatusOK, newRepoData{Username: user.Username, Visibility: visibilityPrivate})
}
// Repo creation is owner-only: anonymous visitors are redirected to the
// login page, so unauthenticated traffic can never materialize repos (or
// their on-disk directories) on the server.
func (s *Server) handleCreateRepo(w http.ResponseWriter, r *http.Request) {
user := requireUser(w, r)
if user == nil {
return
}
r.Body = http.MaxBytesReader(w, r.Body, loginFormMax)
if err := r.ParseForm(); err != nil {
http.Error(w, "bad form", http.StatusBadRequest)
return
}
// Truncate before validating so an oversized form value can never reach
// the error page or the log unbounded.
name := formText(r, "name", maxNameLen+1)
description := formText(r, "description", maxTitleLen)
visibility := r.FormValue("visibility")
if visibility != visibilityPublic {
visibility = visibilityPrivate
}
fail := func(status int, msg string) {
log.Printf("web: create repo %q: %s", name, msg)
s.render(w, "new.html", status, newRepoData{
Username: user.Username,
Name: name, Description: description, Visibility: visibility, Error: msg,
})
}
if len(name) > maxNameLen || !repoNamePattern.MatchString(name) {
fail(http.StatusUnprocessableEntity, fmt.Sprintf("invalid repository name %q", name))
return
}
if _, err := db.GetRepoByName(s.database, user.Username, name); err == nil {
fail(http.StatusUnprocessableEntity, "you already have a repository with that name")
return
} else if !errors.Is(err, db.ErrNotFound) {
s.internalError(w, r, err)
return
}
// DB row first, bare repo second: if git fails we roll the row back,
// whereas a stray directory from a failed run would shadow a future create.
repo, err := db.CreateRepo(s.database, user.ID, name, description, visibility)
if err != nil {
fail(http.StatusInternalServerError, "could not create the repository")
return
}
repoPath := filepath.Join(s.cfg.DataDir, "repos", user.Username, repo.Name+".git")
if err := git.InitBare(repoPath, repo.DefaultBranch); err != nil {
log.Printf("web: git init %s: %v", repoPath, err)
if err := db.DeleteRepo(s.database, repo.ID); err != nil {
log.Printf("web: rollback repo %d: %v", repo.ID, err)
}
os.RemoveAll(repoPath)
fail(http.StatusInternalServerError, "could not initialize the repository on disk")
return
}
pushURL := cloneURL(s.cfg.BaseURL, user.Username, repo.Name)
s.render(w, "created.html", http.StatusOK, createdData{Username: user.Username, RepoName: repo.Name, PushURL: pushURL})
}