Files
gitea/models/user/user_pinned.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

138 lines
3.9 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package user
import (
"context"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/timeutil"
)
// UserPinnedRepo represents a pinned repository for a user's profile
type UserPinnedRepo struct {
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"INDEX NOT NULL"`
RepoID int64 `xorm:"INDEX NOT NULL"`
DisplayOrder int `xorm:"DEFAULT 0"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
Repo any `xorm:"-"` // Will be loaded by caller to avoid import cycle
}
// TableName returns the table name for UserPinnedRepo
func (p *UserPinnedRepo) TableName() string {
return "user_pinned_repo"
}
func init() {
db.RegisterModel(new(UserPinnedRepo))
}
// MaxUserPinnedRepos is the maximum number of repos a user can pin
const MaxUserPinnedRepos = 6
// GetUserPinnedRepos returns all pinned repos for a user
func GetUserPinnedRepos(ctx context.Context, userID int64) ([]*UserPinnedRepo, error) {
pinnedRepos := make([]*UserPinnedRepo, 0, MaxUserPinnedRepos)
err := db.GetEngine(ctx).
Where("user_id = ?", userID).
OrderBy("display_order ASC, id ASC").
Find(&pinnedRepos)
return pinnedRepos, err
}
// CountUserPinnedRepos returns the count of pinned repos for a user
func CountUserPinnedRepos(ctx context.Context, userID int64) (int64, error) {
return db.GetEngine(ctx).Where("user_id = ?", userID).Count(new(UserPinnedRepo))
}
// IsRepoPinnedByUser checks if a repo is pinned by a user
func IsRepoPinnedByUser(ctx context.Context, userID, repoID int64) (bool, error) {
return db.GetEngine(ctx).Where("user_id = ? AND repo_id = ?", userID, repoID).Exist(new(UserPinnedRepo))
}
// PinRepoToUser pins a repo to a user's profile
func PinRepoToUser(ctx context.Context, userID, repoID int64) error {
// Check if already pinned
exists, err := IsRepoPinnedByUser(ctx, userID, repoID)
if err != nil {
return err
}
if exists {
return nil // Already pinned
}
// Check max limit
count, err := CountUserPinnedRepos(ctx, userID)
if err != nil {
return err
}
if count >= MaxUserPinnedRepos {
return ErrUserPinnedRepoLimit{UserID: userID, Limit: MaxUserPinnedRepos}
}
// Get next display order
var maxOrder int
_, err = db.GetEngine(ctx).
Table("user_pinned_repo").
Where("user_id = ?", userID).
Select("COALESCE(MAX(display_order), 0)").
Get(&maxOrder)
if err != nil {
return err
}
pinnedRepo := &UserPinnedRepo{
UserID: userID,
RepoID: repoID,
DisplayOrder: maxOrder + 1,
}
_, err = db.GetEngine(ctx).Insert(pinnedRepo)
return err
}
// UnpinRepoFromUser unpins a repo from a user's profile
func UnpinRepoFromUser(ctx context.Context, userID, repoID int64) error {
_, err := db.GetEngine(ctx).Where("user_id = ? AND repo_id = ?", userID, repoID).Delete(new(UserPinnedRepo))
return err
}
// UpdateUserPinnedRepoOrder updates the display order of pinned repos
func UpdateUserPinnedRepoOrder(ctx context.Context, userID int64, repoIDs []int64) error {
for i, repoID := range repoIDs {
_, err := db.GetEngine(ctx).
Where("user_id = ? AND repo_id = ?", userID, repoID).
Cols("display_order").
Update(&UserPinnedRepo{DisplayOrder: i})
if err != nil {
return err
}
}
return nil
}
// DeleteUserPinnedReposByRepoID deletes all pins for a repo (when repo is deleted)
func DeleteUserPinnedReposByRepoID(ctx context.Context, repoID int64) error {
_, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Delete(new(UserPinnedRepo))
return err
}
// ErrUserPinnedRepoLimit represents an error when user has reached pin limit
type ErrUserPinnedRepoLimit struct {
UserID int64
Limit int
}
func (err ErrUserPinnedRepoLimit) Error() string {
return "user has reached the maximum number of pinned repositories"
}
// IsErrUserPinnedRepoLimit checks if error is ErrUserPinnedRepoLimit
func IsErrUserPinnedRepoLimit(err error) bool {
_, ok := err.(ErrUserPinnedRepoLimit)
return ok
}