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
- 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>
129 lines
3.6 KiB
Go
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)
|
|
}
|