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}) }