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>
138 lines
3.9 KiB
Go
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
|
|
}
|