josie / simplegit

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