Code
- hypercode
- controllers
- repositories_controller.go
repositories_controller.go1018 lines
package controllers
import (
"fmt"
"log/slog"
"net/http"
"os"
"os/exec"
"path/filepath"
"github.com/go-chi/chi/v5"
"github.com/hypercodehq/hypercode/database/models"
"github.com/hypercodehq/hypercode/database/repositories"
"github.com/hypercodehq/hypercode/httperror"
custommiddleware "github.com/hypercodehq/hypercode/middleware"
"github.com/hypercodehq/hypercode/services"
"github.com/hypercodehq/hypercode/views/pages"
)
type RepositoriesController interface {
Create(w http.ResponseWriter, r *http.Request) error
Store(w http.ResponseWriter, r *http.Request) error
Show(w http.ResponseWriter, r *http.Request) error
Tree(w http.ResponseWriter, r *http.Request) error
Settings(w http.ResponseWriter, r *http.Request) error
UpdateSettings(w http.ResponseWriter, r *http.Request) error
Delete(w http.ResponseWriter, r *http.Request) error
Star(w http.ResponseWriter, r *http.Request) error
Unstar(w http.ResponseWriter, r *http.Request) error
AddCollaborator(w http.ResponseWriter, r *http.Request) error
RemoveCollaborator(w http.ResponseWriter, r *http.Request) error
UpdateCollaboratorRole(w http.ResponseWriter, r *http.Request) error
}
type repositoriesController struct {
repos repositories.RepositoriesRepository
users repositories.UsersRepository
contributors repositories.ContributorsRepository
stars repositories.StarsRepository
orgs repositories.OrganizationsRepository
authService services.AuthService
gitService services.GitService
reposBasePath string
}
func NewRepositoriesController(
repos repositories.RepositoriesRepository,
users repositories.UsersRepository,
contributors repositories.ContributorsRepository,
stars repositories.StarsRepository,
orgs repositories.OrganizationsRepository,
authService services.AuthService,
gitService services.GitService,
reposBasePath string,
) RepositoriesController {
return &repositoriesController{
repos: repos,
users: users,
contributors: contributors,
stars: stars,
orgs: orgs,
authService: authService,
gitService: gitService,
reposBasePath: reposBasePath,
}
}
func (c *repositoriesController) Create(w http.ResponseWriter, r *http.Request) error {
user, err := c.authService.GetUserFromCookie(r)
if err != nil {
http.Redirect(w, r, "/auth/sign-in", http.StatusSeeOther)
return nil
}
if user == nil {
http.Redirect(w, r, "/auth/sign-in", http.StatusSeeOther)
return nil
}
// Get all organizations (for now, show all orgs - later add membership check)
// TODO: Filter by organizations where user is a member
orgs, err := c.orgs.FindAll()
if err != nil {
slog.Error("failed to fetch organizations", "error", err)
orgs = []*models.Organization{}
}
return pages.NewRepository(r, &pages.NewRepositoryData{
User: user,
Organizations: orgs,
DefaultBranch: "main",
}).Render(w, r)
}
func (c *repositoriesController) Store(w http.ResponseWriter, r *http.Request) error {
user, err := c.authService.GetUserFromCookie(r)
if err != nil {
http.Redirect(w, r, "/auth/sign-in", http.StatusSeeOther)
return nil
}
if err := r.ParseForm(); err != nil {
return httperror.BadRequest("invalid form data")
}
name := r.FormValue("name")
visibility := r.FormValue("visibility")
defaultBranch := r.FormValue("default_branch")
ownerUsername := r.FormValue("owner")
if visibility == "" {
visibility = "public"
}
if defaultBranch == "" {
defaultBranch = "main"
}
if ownerUsername == "" {
ownerUsername = user.Username
}
// Get all organizations for error handling
orgs, _ := c.orgs.FindAll()
repoData := &pages.NewRepositoryData{
Name: name,
DefaultBranch: defaultBranch,
Visibility: visibility,
Owner: ownerUsername,
User: user,
Organizations: orgs,
}
hasErrors := false
if name == "" {
repoData.NameError = "Repository name is required"
hasErrors = true
}
if hasErrors {
return pages.NewRepository(r, repoData).Render(w, r)
}
// Determine if owner is user or organization
var repo *models.Repository
var ownerIDForPath string
if ownerUsername == user.Username {
// Check for existing repo under user
existingRepo, err := c.repos.FindByUserAndName(user.ID, name)
if err != nil {
slog.Error("failed to check for existing repository", "error", err)
}
if existingRepo != nil {
repoData.NameError = "Repository name already exists"
return pages.NewRepository(r, repoData).Render(w, r)
}
// Create for user
repo, err = c.repos.CreateForUser(user.ID, name, visibility, defaultBranch, nil)
if err != nil {
slog.Error("failed to create repository", "error", err)
repoData.NameError = "Failed to create repository"
return pages.NewRepository(r, repoData).Render(w, r)
}
ownerIDForPath = fmt.Sprintf("%d", user.ID)
} else {
// Find organization
org, err := c.orgs.FindByUsername(ownerUsername)
if err != nil {
slog.Error("failed to find organization", "error", err)
repoData.NameError = "Organization not found"
return pages.NewRepository(r, repoData).Render(w, r)
}
if org == nil {
repoData.NameError = "Organization not found"
return pages.NewRepository(r, repoData).Render(w, r)
}
// Check for existing repo under org
existingRepo, err := c.repos.FindByOrgAndName(org.ID, name)
if err != nil {
slog.Error("failed to check for existing repository", "error", err)
}
if existingRepo != nil {
repoData.NameError = "Repository name already exists"
return pages.NewRepository(r, repoData).Render(w, r)
}
// Create for organization
repo, err = c.repos.CreateForOrg(org.ID, name, visibility, defaultBranch, nil)
if err != nil {
slog.Error("failed to create repository", "error", err)
repoData.NameError = "Failed to create repository"
return pages.NewRepository(r, repoData).Render(w, r)
}
ownerIDForPath = fmt.Sprintf("org_%d", org.ID)
}
// Create admin contributor
_, err = c.contributors.Create(repo.ID, user.ID, "admin")
if err != nil {
slog.Error("failed to create admin contributor", "error", err)
}
repoPath := filepath.Join(c.reposBasePath, ownerIDForPath, fmt.Sprintf("%d", repo.ID))
if err := os.MkdirAll(repoPath, 0755); err != nil {
slog.Error("failed to create repository directory", "error", err)
return httperror.New(http.StatusInternalServerError, "failed to create repository")
}
cmd := exec.Command("git", "init", "--bare")
cmd.Dir = repoPath
if err := cmd.Run(); err != nil {
slog.Error("failed to initialize git repository", "error", err)
return httperror.New(http.StatusInternalServerError, "failed to initialize repository")
}
configCmd := exec.Command("git", "config", "http.receivepack", "true")
configCmd.Dir = repoPath
if err := configCmd.Run(); err != nil {
slog.Error("failed to configure git repository", "error", err)
return httperror.New(http.StatusInternalServerError, "failed to configure repository")
}
slog.Info("repository created", "owner", ownerUsername, "name", name, "visibility", visibility, "creator", user.Username)
http.Redirect(w, r, fmt.Sprintf("/%s/%s", ownerUsername, name), http.StatusSeeOther)
return nil
}
func (c *repositoriesController) Show(w http.ResponseWriter, r *http.Request) error {
owner := chi.URLParam(r, "owner")
repoName := chi.URLParam(r, "repo")
repo, err := c.repos.FindByOwnerAndName(owner, repoName)
if err != nil {
return httperror.NotFound("repository not found")
}
if repo == nil {
return httperror.NotFound("repository not found")
}
if repo.Visibility == "private" {
user, err := c.authService.GetUserFromCookie(r)
if err != nil || user == nil {
return httperror.Unauthorized("authentication required")
}
if repo.OwnerUserID != nil && *repo.OwnerUserID != user.ID {
return httperror.Forbidden("access denied")
}
}
user, _ := c.authService.GetUserFromCookie(r)
// Determine if user can manage the repository
canManage := false
if user != nil && repo.OwnerUserID != nil && *repo.OwnerUserID == user.ID {
canManage = true
} else if user != nil {
// Check if user is an admin contributor
contributor, err := c.contributors.FindByRepositoryAndUser(repo.ID, user.ID)
if err == nil && contributor != nil && contributor.Role == "admin" {
canManage = true
}
}
// Get star count
starCount, err := c.stars.CountByRepository(repo.ID)
if err != nil {
slog.Error("failed to count stars", "error", err)
starCount = 0
}
// Check if user has starred
hasStarred := false
if user != nil {
star, err := c.stars.FindByUserAndRepository(repo.ID, user.ID)
if err != nil {
slog.Error("failed to check if user starred", "error", err)
}
if star != nil {
hasStarred = true
}
}
host := r.Host
cloneURL := fmt.Sprintf("https://%s/%s/%s", host, owner, repoName)
data := &pages.ShowRepositoryData{
User: user,
Repository: repo,
OwnerUsername: owner,
CloneURL: cloneURL,
IsPublic: repo.Visibility == "public",
CanManage: canManage,
StarCount: starCount,
HasStarred: hasStarred,
}
return pages.ShowRepository(r, data).Render(w, r)
}
func (c *repositoriesController) Tree(w http.ResponseWriter, r *http.Request) error {
owner := chi.URLParam(r, "owner")
repoName := chi.URLParam(r, "repo")
ref := chi.URLParam(r, "ref")
treePath := chi.URLParam(r, "*")
repo, err := c.repos.FindByOwnerAndName(owner, repoName)
if err != nil {
return httperror.NotFound("repository not found")
}
if repo == nil {
return httperror.NotFound("repository not found")
}
// Check visibility and auth
if repo.Visibility == "private" {
user, err := c.authService.GetUserFromCookie(r)
if err != nil || user == nil {
return httperror.Unauthorized("authentication required")
}
if repo.OwnerUserID != nil && *repo.OwnerUserID != user.ID {
// Check if user is a contributor
contributor, err := c.contributors.FindByRepositoryAndUser(repo.ID, user.ID)
if err != nil || contributor == nil {
return httperror.Forbidden("access denied")
}
}
}
user, _ := c.authService.GetUserFromCookie(r)
// Determine if user can manage the repository
canManage := false
if user != nil && repo.OwnerUserID != nil && *repo.OwnerUserID == user.ID {
canManage = true
} else if user != nil {
contributor, err := c.contributors.FindByRepositoryAndUser(repo.ID, user.ID)
if err == nil && contributor != nil && contributor.Role == "admin" {
canManage = true
}
}
// Get star count
starCount, err := c.stars.CountByRepository(repo.ID)
if err != nil {
slog.Error("failed to count stars", "error", err)
starCount = 0
}
// Check if user has starred
hasStarred := false
if user != nil {
star, err := c.stars.FindByUserAndRepository(repo.ID, user.ID)
if err != nil {
slog.Error("failed to check if user starred", "error", err)
}
if star != nil {
hasStarred = true
}
}
// Determine repository path on disk
var ownerIDForPath string
if repo.OwnerUserID != nil {
ownerIDForPath = fmt.Sprintf("%d", *repo.OwnerUserID)
} else if repo.OwnerOrgID != nil {
ownerIDForPath = fmt.Sprintf("org_%d", *repo.OwnerOrgID)
}
repoPath := filepath.Join(c.reposBasePath, ownerIDForPath, fmt.Sprintf("%d", repo.ID))
// List branches
branches, err := c.gitService.ListBranches(repoPath)
if err != nil {
slog.Error("failed to list branches", "error", err)
branches = []string{}
}
// If no ref specified, use default branch (or first available branch if default doesn't exist)
if ref == "" {
if len(branches) == 0 {
// Empty repository
data := &pages.RepositoryTreeData{
User: user,
Repository: repo,
OwnerUsername: owner,
CanManage: canManage,
StarCount: starCount,
HasStarred: hasStarred,
Branches: []string{},
CurrentBranch: repo.DefaultBranch,
CurrentPath: "",
Entries: []services.TreeEntry{},
IsEmpty: true,
}
return pages.RepositoryTree(r, data).Render(w, r)
}
// Validate that default branch exists, otherwise use first available branch
ref = repo.DefaultBranch
defaultBranchExists := false
for _, branch := range branches {
if branch == repo.DefaultBranch {
defaultBranchExists = true
break
}
}
if !defaultBranchExists {
ref = branches[0]
slog.Warn("default branch not found when no ref specified, using first available branch",
"owner", owner,
"repo", repoName,
"defaultBranch", repo.DefaultBranch,
"actualBranches", branches,
"using", ref)
}
}
// Validate that ref exists in branches
refExists := false
for _, branch := range branches {
if branch == ref {
refExists = true
break
}
}
if !refExists && len(branches) > 0 {
// Find a valid branch to redirect to
targetBranch := ""
// First, check if the default branch exists
defaultBranchExists := false
for _, branch := range branches {
if branch == repo.DefaultBranch {
defaultBranchExists = true
targetBranch = repo.DefaultBranch
break
}
}
// If default branch doesn't exist, use the first available branch
if !defaultBranchExists {
targetBranch = branches[0]
slog.Warn("default branch not found in git repository, using first available branch",
"owner", owner,
"repo", repoName,
"defaultBranch", repo.DefaultBranch,
"actualBranches", branches,
"using", targetBranch)
}
// Prevent infinite redirect: don't redirect if we're already on the target branch
if targetBranch != ref {
redirectPath := fmt.Sprintf("/%s/%s/tree/%s", owner, repoName, targetBranch)
if treePath != "" {
redirectPath = fmt.Sprintf("/%s/%s/tree/%s/%s", owner, repoName, targetBranch, treePath)
}
http.Redirect(w, r, redirectPath, http.StatusSeeOther)
return nil
}
}
// Check if treePath points to a file or directory
if treePath != "" {
isFile, err := c.gitService.IsFile(repoPath, ref, treePath)
if err == nil && isFile {
// It's a file - display file content
fileContent, err := c.gitService.GetFileContent(repoPath, ref, treePath)
if err == nil && fileContent != nil {
data := &pages.RepositoryFileData{
User: user,
Repository: repo,
OwnerUsername: owner,
CanManage: canManage,
StarCount: starCount,
HasStarred: hasStarred,
Branches: branches,
CurrentBranch: ref,
CurrentPath: treePath,
FileContent: string(fileContent),
}
return pages.RepositoryFile(r, data).Render(w, r)
}
}
}
// List tree contents (directory)
entries, err := c.gitService.ListTree(repoPath, ref, treePath)
if err != nil {
slog.Error("failed to list tree", "error", err, "ref", ref, "path", treePath)
entries = []services.TreeEntry{}
}
data := &pages.RepositoryTreeData{
User: user,
Repository: repo,
OwnerUsername: owner,
CanManage: canManage,
StarCount: starCount,
HasStarred: hasStarred,
Branches: branches,
CurrentBranch: ref,
CurrentPath: treePath,
Entries: entries,
IsEmpty: len(branches) == 0,
}
return pages.RepositoryTree(r, data).Render(w, r)
}
func (c *repositoriesController) Settings(w http.ResponseWriter, r *http.Request) error {
owner := chi.URLParam(r, "owner")
repoName := chi.URLParam(r, "repo")
repo, err := c.repos.FindByOwnerAndName(owner, repoName)
if err != nil {
return httperror.NotFound("repository not found")
}
if repo == nil {
return httperror.NotFound("repository not found")
}
user, _ := c.authService.GetUserFromCookie(r)
if user == nil {
return httperror.Unauthorized("authentication required")
}
// Check if user can manage the repository
canManage := false
if repo.OwnerUserID != nil && *repo.OwnerUserID == user.ID {
canManage = true
} else {
contributor, err := c.contributors.FindByRepositoryAndUser(repo.ID, user.ID)
if err == nil && contributor != nil && contributor.Role == "admin" {
canManage = true
}
}
if !canManage {
return httperror.Forbidden("access denied")
}
// Get star information
starCount, _ := c.stars.CountByRepository(repo.ID)
hasStarred := false
if user != nil {
star, _ := c.stars.FindByUserAndRepository(repo.ID, user.ID)
hasStarred = star != nil
}
// Get collaborators
contributors, err := c.contributors.FindAllByRepository(repo.ID)
if err != nil {
slog.Error("failed to fetch contributors", "error", err)
contributors = []*models.Contributor{}
}
collaborators := make([]pages.CollaboratorData, 0, len(contributors))
for _, contrib := range contributors {
collabUser, err := c.users.FindByID(contrib.UserID)
if err == nil && collabUser != nil {
collaborators = append(collaborators, pages.CollaboratorData{
Contributor: contrib,
Username: collabUser.Username,
})
}
}
return pages.RepositorySettings(r, &pages.RepositorySettingsData{
User: user,
Repository: repo,
OwnerUsername: owner,
StarCount: starCount,
HasStarred: hasStarred,
Collaborators: collaborators,
}).Render(w, r)
}
func (c *repositoriesController) UpdateSettings(w http.ResponseWriter, r *http.Request) error {
owner := chi.URLParam(r, "owner")
repoName := chi.URLParam(r, "repo")
repo, err := c.repos.FindByOwnerAndName(owner, repoName)
if err != nil {
return httperror.NotFound("repository not found")
}
if repo == nil {
return httperror.NotFound("repository not found")
}
user, _ := c.authService.GetUserFromCookie(r)
if user == nil {
return httperror.Unauthorized("authentication required")
}
// Check if user can manage the repository
canManage := false
if repo.OwnerUserID != nil && *repo.OwnerUserID == user.ID {
canManage = true
} else {
contributor, err := c.contributors.FindByRepositoryAndUser(repo.ID, user.ID)
if err == nil && contributor != nil && contributor.Role == "admin" {
canManage = true
}
}
if !canManage {
return httperror.Forbidden("access denied")
}
if err := r.ParseForm(); err != nil {
return httperror.BadRequest("invalid form data")
}
name := r.FormValue("name")
defaultBranch := r.FormValue("default_branch")
visibility := r.FormValue("visibility")
// Get star information
starCount, _ := c.stars.CountByRepository(repo.ID)
hasStarred := false
if user != nil {
star, _ := c.stars.FindByUserAndRepository(repo.ID, user.ID)
hasStarred = star != nil
}
settingsData := &pages.RepositorySettingsData{
User: user,
Repository: repo,
OwnerUsername: owner,
Name: name,
DefaultBranch: defaultBranch,
Visibility: visibility,
StarCount: starCount,
HasStarred: hasStarred,
}
hasErrors := false
if name == "" {
settingsData.NameError = "Repository name is required"
hasErrors = true
}
if visibility == "" {
visibility = "public"
}
// Check if name changed and if new name is already taken
if name != repo.Name {
existingRepo, err := c.repos.FindByOwnerAndName(owner, name)
if err != nil {
slog.Error("failed to check for existing repository", "error", err)
}
if existingRepo != nil {
settingsData.NameError = "Repository name already exists"
hasErrors = true
}
}
if hasErrors {
return pages.RepositorySettings(r, settingsData).Render(w, r)
}
// Update repository
repo.Name = name
repo.DefaultBranch = defaultBranch
repo.Visibility = visibility
if err := c.repos.Update(repo); err != nil {
slog.Error("failed to update repository", "error", err)
settingsData.NameError = "Failed to update repository"
return pages.RepositorySettings(r, settingsData).Render(w, r)
}
settingsData.GeneralSuccess = "Settings updated successfully!"
// If name changed, redirect to new URL
if name != repoName {
http.Redirect(w, r, fmt.Sprintf("/%s/%s/settings", owner, name), http.StatusSeeOther)
return nil
}
return pages.RepositorySettings(r, settingsData).Render(w, r)
}
func (c *repositoriesController) Delete(w http.ResponseWriter, r *http.Request) error {
owner := chi.URLParam(r, "owner")
repoName := chi.URLParam(r, "repo")
repo, err := c.repos.FindByOwnerAndName(owner, repoName)
if err != nil {
return httperror.NotFound("repository not found")
}
if repo == nil {
return httperror.NotFound("repository not found")
}
user, _ := c.authService.GetUserFromCookie(r)
if user == nil {
return httperror.Unauthorized("authentication required")
}
// Check if user can manage the repository
canManage := false
if repo.OwnerUserID != nil && *repo.OwnerUserID == user.ID {
canManage = true
} else {
contributor, err := c.contributors.FindByRepositoryAndUser(repo.ID, user.ID)
if err == nil && contributor != nil && contributor.Role == "admin" {
canManage = true
}
}
if !canManage {
return httperror.Forbidden("access denied")
}
// Delete from database
if err := c.repos.Delete(repo.ID); err != nil {
slog.Error("failed to delete repository", "error", err)
return httperror.New(http.StatusInternalServerError, "failed to delete repository")
}
// Delete repository directory
var ownerIDForPath string
if repo.OwnerUserID != nil {
ownerIDForPath = fmt.Sprintf("%d", *repo.OwnerUserID)
} else if repo.OwnerOrgID != nil {
ownerIDForPath = fmt.Sprintf("org_%d", *repo.OwnerOrgID)
}
repoPath := filepath.Join(c.reposBasePath, ownerIDForPath, fmt.Sprintf("%d", repo.ID))
if err := os.RemoveAll(repoPath); err != nil {
slog.Error("failed to delete repository directory", "error", err)
}
slog.Info("repository deleted", "owner", owner, "name", repoName)
http.Redirect(w, r, "/", http.StatusSeeOther)
return nil
}
func (c *repositoriesController) Star(w http.ResponseWriter, r *http.Request) error {
owner := chi.URLParam(r, "owner")
repoName := chi.URLParam(r, "repo")
repo, err := c.repos.FindByOwnerAndName(owner, repoName)
if err != nil {
return httperror.NotFound("repository not found")
}
if repo == nil {
return httperror.NotFound("repository not found")
}
user, _ := c.authService.GetUserFromCookie(r)
if user == nil {
return httperror.Unauthorized("authentication required")
}
// Check if already starred
existingStar, err := c.stars.FindByUserAndRepository(repo.ID, user.ID)
if err != nil {
slog.Error("failed to check existing star", "error", err)
}
if existingStar != nil {
// Already starred, just redirect back
referer := r.Header.Get("Referer")
if referer == "" {
referer = fmt.Sprintf("/%s/%s", owner, repoName)
}
http.Redirect(w, r, referer, http.StatusSeeOther)
return nil
}
// Create star
_, err = c.stars.Create(repo.ID, user.ID)
if err != nil {
slog.Error("failed to star repository", "error", err)
return httperror.New(http.StatusInternalServerError, "failed to star repository")
}
slog.Info("repository starred", "user", user.Username, "repo", repoName)
referer := r.Header.Get("Referer")
if referer == "" {
referer = fmt.Sprintf("/%s/%s", owner, repoName)
}
http.Redirect(w, r, referer, http.StatusSeeOther)
return nil
}
func (c *repositoriesController) Unstar(w http.ResponseWriter, r *http.Request) error {
owner := chi.URLParam(r, "owner")
repoName := chi.URLParam(r, "repo")
repo, err := c.repos.FindByOwnerAndName(owner, repoName)
if err != nil {
return httperror.NotFound("repository not found")
}
if repo == nil {
return httperror.NotFound("repository not found")
}
user, _ := c.authService.GetUserFromCookie(r)
if user == nil {
return httperror.Unauthorized("authentication required")
}
// Delete star
err = c.stars.Delete(repo.ID, user.ID)
if err != nil {
slog.Error("failed to unstar repository", "error", err)
return httperror.New(http.StatusInternalServerError, "failed to unstar repository")
}
slog.Info("repository unstarred", "user", user.Username, "repo", repoName)
referer := r.Header.Get("Referer")
if referer == "" {
referer = fmt.Sprintf("/%s/%s", owner, repoName)
}
http.Redirect(w, r, referer, http.StatusSeeOther)
return nil
}
func (c *repositoriesController) AddCollaborator(w http.ResponseWriter, r *http.Request) error {
owner := chi.URLParam(r, "owner")
repoName := chi.URLParam(r, "repo")
repo, err := c.repos.FindByOwnerAndName(owner, repoName)
if err != nil || repo == nil {
return httperror.NotFound("repository not found")
}
user := custommiddleware.GetUserFromContext(r)
if user == nil {
return httperror.Unauthorized("authentication required")
}
// Check if user can manage the repository
canManage := false
if repo.OwnerUserID != nil && *repo.OwnerUserID == user.ID {
canManage = true
} else {
contributor, err := c.contributors.FindByRepositoryAndUser(repo.ID, user.ID)
if err == nil && contributor != nil && contributor.Role == "admin" {
canManage = true
}
}
if !canManage {
return httperror.Forbidden("access denied")
}
if err := r.ParseForm(); err != nil {
return httperror.BadRequest("invalid form data")
}
username := r.FormValue("username")
role := r.FormValue("role")
if username == "" {
http.Redirect(w, r, fmt.Sprintf("/%s/%s/settings", owner, repoName), http.StatusSeeOther)
return nil
}
// Find the user to add
collabUser, err := c.users.FindByUsername(username)
if err != nil || collabUser == nil {
http.Redirect(w, r, fmt.Sprintf("/%s/%s/settings?collaborator_error=User+not+found", owner, repoName), http.StatusSeeOther)
return nil
}
// Check if user is already a collaborator
existing, _ := c.contributors.FindByRepositoryAndUser(repo.ID, collabUser.ID)
if existing != nil {
http.Redirect(w, r, fmt.Sprintf("/%s/%s/settings?collaborator_error=User+is+already+a+collaborator", owner, repoName), http.StatusSeeOther)
return nil
}
// Add collaborator
_, err = c.contributors.Create(repo.ID, collabUser.ID, role)
if err != nil {
slog.Error("failed to add collaborator", "error", err)
http.Redirect(w, r, fmt.Sprintf("/%s/%s/settings?collaborator_error=Failed+to+add+collaborator", owner, repoName), http.StatusSeeOther)
return nil
}
slog.Info("collaborator added", "repo", repoName, "username", username, "role", role)
http.Redirect(w, r, fmt.Sprintf("/%s/%s/settings?collaborator_success=Collaborator+added+successfully", owner, repoName), http.StatusSeeOther)
return nil
}
func (c *repositoriesController) RemoveCollaborator(w http.ResponseWriter, r *http.Request) error {
owner := chi.URLParam(r, "owner")
repoName := chi.URLParam(r, "repo")
repo, err := c.repos.FindByOwnerAndName(owner, repoName)
if err != nil || repo == nil {
return httperror.NotFound("repository not found")
}
user := custommiddleware.GetUserFromContext(r)
if user == nil {
return httperror.Unauthorized("authentication required")
}
// Check if user can manage the repository
canManage := false
if repo.OwnerUserID != nil && *repo.OwnerUserID == user.ID {
canManage = true
} else {
contributor, err := c.contributors.FindByRepositoryAndUser(repo.ID, user.ID)
if err == nil && contributor != nil && contributor.Role == "admin" {
canManage = true
}
}
if !canManage {
return httperror.Forbidden("access denied")
}
if err := r.ParseForm(); err != nil {
return httperror.BadRequest("invalid form data")
}
userIDStr := r.FormValue("user_id")
var userID int64
fmt.Sscanf(userIDStr, "%d", &userID)
// Remove collaborator
err = c.contributors.DeleteByRepositoryAndUser(repo.ID, userID)
if err != nil {
slog.Error("failed to remove collaborator", "error", err)
http.Redirect(w, r, fmt.Sprintf("/%s/%s/settings?collaborator_error=Failed+to+remove+collaborator", owner, repoName), http.StatusSeeOther)
return nil
}
slog.Info("collaborator removed", "repo", repoName, "user_id", userID)
http.Redirect(w, r, fmt.Sprintf("/%s/%s/settings?collaborator_success=Collaborator+removed+successfully", owner, repoName), http.StatusSeeOther)
return nil
}
func (c *repositoriesController) UpdateCollaboratorRole(w http.ResponseWriter, r *http.Request) error {
owner := chi.URLParam(r, "owner")
repoName := chi.URLParam(r, "repo")
repo, err := c.repos.FindByOwnerAndName(owner, repoName)
if err != nil || repo == nil {
return httperror.NotFound("repository not found")
}
user := custommiddleware.GetUserFromContext(r)
if user == nil {
return httperror.Unauthorized("authentication required")
}
// Check if user can manage the repository
canManage := false
if repo.OwnerUserID != nil && *repo.OwnerUserID == user.ID {
canManage = true
} else {
contributor, err := c.contributors.FindByRepositoryAndUser(repo.ID, user.ID)
if err == nil && contributor != nil && contributor.Role == "admin" {
canManage = true
}
}
if !canManage {
return httperror.Forbidden("access denied")
}
if err := r.ParseForm(); err != nil {
return httperror.BadRequest("invalid form data")
}
userIDStr := r.FormValue("user_id")
role := r.FormValue("role")
var userID int64
fmt.Sscanf(userIDStr, "%d", &userID)
// Find the contributor
contributor, err := c.contributors.FindByRepositoryAndUser(repo.ID, userID)
if err != nil || contributor == nil {
http.Redirect(w, r, fmt.Sprintf("/%s/%s/settings?collaborator_error=Collaborator+not+found", owner, repoName), http.StatusSeeOther)
return nil
}
// Update role
err = c.contributors.UpdateRole(contributor.ID, role)
if err != nil {
slog.Error("failed to update collaborator role", "error", err)
http.Redirect(w, r, fmt.Sprintf("/%s/%s/settings?collaborator_error=Failed+to+update+role", owner, repoName), http.StatusSeeOther)
return nil
}
slog.Info("collaborator role updated", "repo", repoName, "user_id", userID, "new_role", role)
http.Redirect(w, r, fmt.Sprintf("/%s/%s/settings?collaborator_success=Role+updated+successfully", owner, repoName), http.StatusSeeOther)
return nil
}
