package web import ( "errors" "fmt" "log" "net/http" "os" "path/filepath" "git.josie-c.com/josie/simplegit/internal/db" ) type repoSettingsData struct { navData CloneURL string Error string } // ownerRepo resolves a repo page and requires the caller to be its owner, // regardless of visibility. On failure it has written the response. func (s *Server) ownerRepo(w http.ResponseWriter, r *http.Request) (db.Repo, string, *db.User, bool) { repo, repoPath, user, ok := s.resolveRepo(w, r) if !ok { return db.Repo{}, "", nil, false } if !isOwner(user, repo) { if user == nil { http.Redirect(w, r, "/login", http.StatusSeeOther) } else { http.NotFound(w, r) } return db.Repo{}, "", nil, false } return repo, repoPath, user, true } func (s *Server) repoView(r *http.Request, repo db.Repo, user *db.User) repoSettingsData { return repoSettingsData{ navData: nav(r, repo, user, "settings"), CloneURL: cloneURL(s.cfg.BaseURL, r.PathValue("user"), repo.Name), } } // handleRepoSettings renders the per-repo settings page. func (s *Server) handleRepoSettings(w http.ResponseWriter, r *http.Request) { repo, _, user, ok := s.ownerRepo(w, r) if !ok { return } s.render(w, "repo_settings.html", http.StatusOK, s.repoView(r, repo, user)) } // handleRepoVisibility toggles a repo between public and private. func (s *Server) handleRepoVisibility(w http.ResponseWriter, r *http.Request) { repo, _, _, ok := s.ownerRepo(w, r) if !ok { return } r.Body = http.MaxBytesReader(w, r.Body, loginFormMax) if err := r.ParseForm(); err != nil { http.Error(w, "bad form", http.StatusBadRequest) return } visibility := r.FormValue("visibility") if visibility != visibilityPublic { visibility = visibilityPrivate } if err := db.UpdateRepoVisibility(s.database, repo.ID, visibility); err != nil { s.internalError(w, r, err) return } http.Redirect(w, r, repoBasePath(r)+"/settings", http.StatusSeeOther) } // handleRepoRename moves the bare directory and updates the row. func (s *Server) handleRepoRename(w http.ResponseWriter, r *http.Request) { repo, repoPath, user, ok := s.ownerRepo(w, r) if !ok { return } r.Body = http.MaxBytesReader(w, r.Body, loginFormMax) if err := r.ParseForm(); err != nil { http.Error(w, "bad form", http.StatusBadRequest) return } owner := r.PathValue("user") name := formText(r, "name", maxNameLen+1) fail := func(msg string) { data := s.repoView(r, repo, user) data.Error = msg s.render(w, "repo_settings.html", http.StatusUnprocessableEntity, data) } if name == repo.Name { http.Redirect(w, r, repoBasePath(r)+"/settings", http.StatusSeeOther) return } if len(name) > maxNameLen || !repoNamePattern.MatchString(name) { fail(fmt.Sprintf("invalid repository name %q", name)) return } if _, err := db.GetRepoByName(s.database, owner, name); err == nil { fail("you already have a repository with that name") return } else if !errors.Is(err, db.ErrNotFound) { s.internalError(w, r, err) return } newPath := filepath.Join(s.cfg.DataDir, "repos", owner, name+".git") if err := os.Rename(repoPath, newPath); err != nil { log.Printf("web: rename repo %s: %v", repoPath, err) fail("could not move the repository on disk") return } if err := db.RenameRepo(s.database, repo.ID, name); err != nil { log.Printf("web: rename repo row %d: %v", repo.ID, err) if rollbackErr := os.Rename(newPath, repoPath); rollbackErr != nil { log.Printf("web: rename rollback %s: %v", newPath, rollbackErr) } s.internalError(w, r, err) return } http.Redirect(w, r, "/"+owner+"/"+name+"/settings", http.StatusSeeOther) } // handleRepoDelete removes the row, the bare directory, and any release // asset uploads (the DB rows cascade, but files on disk do not). func (s *Server) handleRepoDelete(w http.ResponseWriter, r *http.Request) { repo, repoPath, _, ok := s.ownerRepo(w, r) if !ok { return } releases, err := db.ListReleases(s.database, repo.ID) if err != nil { log.Printf("web: list releases for delete %d: %v", repo.ID, err) } if err := db.DeleteRepo(s.database, repo.ID); err != nil { s.internalError(w, r, err) return } if err := os.RemoveAll(repoPath); err != nil { log.Printf("web: remove repo dir %s: %v", repoPath, err) } for _, release := range releases { if err := os.RemoveAll(s.releaseUploadDir(release.ID)); err != nil { log.Printf("web: remove release uploads %d: %v", release.ID, err) } } http.Redirect(w, r, "/", http.StatusSeeOther) } // repoBasePath is the canonical /{owner}/{repo} for the request. func repoBasePath(r *http.Request) string { return "/" + r.PathValue("user") + "/" + r.PathValue("repo") }