gitea/routers/web/repo/pin.go
Admin 1986d90df0
Some checks failed
Build and Release / Create Release (push) Has been skipped
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 1m33s
Build and Release / Lint (push) Failing after 1m54s
Build and Release / Build Binaries (amd64, darwin) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux) (push) Has been skipped
Build and Release / Build Binaries (amd64, windows) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin) (push) Has been skipped
Build and Release / Build Binaries (arm64, linux) (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 2m4s
feat: Pin repos to user profile or organization
- Add UserPinnedRepo model for pinning repos to user profiles
- Add Pin dropdown in repo header with options for profile/org
- Add pin/unpin routes and handlers
- Update user profile to show pinned repos with nice cards
- User overview tab always visible (like org overview)
- Shows empty state with instructions when no pinned repos
- Limit of 6 pinned repos per user
- Org members can pin repos to organization

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 02:09:07 +00:00

129 lines
3.6 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/services/context"
)
// Pin handles pinning a repo to user profile or organization
func Pin(ctx *context.Context) {
pinType := ctx.FormString("type")
redirectTo := ctx.FormString("redirect_to")
if redirectTo == "" {
redirectTo = ctx.Repo.RepoLink
}
switch pinType {
case "user":
if err := user_model.PinRepoToUser(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID); err != nil {
if user_model.IsErrUserPinnedRepoLimit(err) {
ctx.Flash.Error(ctx.Tr("repo.pin.error_limit"))
} else {
log.Error("PinRepoToUser failed: %v", err)
ctx.Flash.Error(ctx.Tr("repo.pin.error_generic"))
}
} else {
ctx.Flash.Success(ctx.Tr("repo.pin.success_profile"))
}
case "org":
if !ctx.Repo.Repository.Owner.IsOrganization() {
ctx.Flash.Error(ctx.Tr("repo.pin.error_not_org"))
ctx.Redirect(redirectTo)
return
}
// Check if user is a member of the org
isMember, err := organization.IsOrganizationMember(ctx, ctx.Repo.Repository.OwnerID, ctx.Doer.ID)
if err != nil {
log.Error("IsOrganizationMember failed: %v", err)
ctx.Flash.Error(ctx.Tr("repo.pin.error_generic"))
ctx.Redirect(redirectTo)
return
}
if !isMember {
ctx.Flash.Error(ctx.Tr("repo.pin.error_not_member"))
ctx.Redirect(redirectTo)
return
}
// Use CreateOrgPinnedRepo
pinnedRepo := &organization.OrgPinnedRepo{
OrgID: ctx.Repo.Repository.OwnerID,
RepoID: ctx.Repo.Repository.ID,
}
if err := organization.CreateOrgPinnedRepo(ctx, pinnedRepo); err != nil {
if _, ok := err.(organization.ErrOrgPinnedRepoAlreadyExist); ok {
ctx.Flash.Info(ctx.Tr("repo.pin.already_pinned_org"))
} else {
log.Error("CreateOrgPinnedRepo failed: %v", err)
ctx.Flash.Error(ctx.Tr("repo.pin.error_generic"))
}
} else {
ctx.Flash.Success(ctx.Tr("repo.pin.success_org"))
}
default:
ctx.Flash.Error(ctx.Tr("repo.pin.error_invalid_type"))
}
ctx.Redirect(redirectTo)
}
// Unpin handles unpinning a repo from user profile or organization
func Unpin(ctx *context.Context) {
pinType := ctx.FormString("type")
redirectTo := ctx.FormString("redirect_to")
if redirectTo == "" {
redirectTo = ctx.Repo.RepoLink
}
switch pinType {
case "user":
if err := user_model.UnpinRepoFromUser(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID); err != nil {
log.Error("UnpinRepoFromUser failed: %v", err)
ctx.Flash.Error(ctx.Tr("repo.pin.error_generic"))
} else {
ctx.Flash.Success(ctx.Tr("repo.pin.unpin_success_profile"))
}
case "org":
if !ctx.Repo.Repository.Owner.IsOrganization() {
ctx.Flash.Error(ctx.Tr("repo.pin.error_not_org"))
ctx.Redirect(redirectTo)
return
}
// Check if user is a member of the org
isMember, err := organization.IsOrganizationMember(ctx, ctx.Repo.Repository.OwnerID, ctx.Doer.ID)
if err != nil {
log.Error("IsOrganizationMember failed: %v", err)
ctx.Flash.Error(ctx.Tr("repo.pin.error_generic"))
ctx.Redirect(redirectTo)
return
}
if !isMember {
ctx.Flash.Error(ctx.Tr("repo.pin.error_not_member"))
ctx.Redirect(redirectTo)
return
}
if err := organization.DeleteOrgPinnedRepo(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.ID); err != nil {
log.Error("DeleteOrgPinnedRepo failed: %v", err)
ctx.Flash.Error(ctx.Tr("repo.pin.error_generic"))
} else {
ctx.Flash.Success(ctx.Tr("repo.pin.unpin_success_org"))
}
default:
ctx.Flash.Error(ctx.Tr("repo.pin.error_invalid_type"))
}
ctx.Redirect(redirectTo)
}