// Copyright 2026 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package organization import ( "context" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/modules/timeutil" ) // OrgPinnedGroup represents a named group of pinned repositories for an organization type OrgPinnedGroup struct { ID int64 `xorm:"pk autoincr"` OrgID int64 `xorm:"INDEX NOT NULL"` Name string `xorm:"NOT NULL"` DisplayOrder int `xorm:"DEFAULT 0"` Collapsed bool `xorm:"DEFAULT false"` CreatedUnix timeutil.TimeStamp `xorm:"created"` UpdatedUnix timeutil.TimeStamp `xorm:"updated"` } // TableName returns the table name for OrgPinnedGroup func (g *OrgPinnedGroup) TableName() string { return "org_pinned_group" } func init() { db.RegisterModel(new(OrgPinnedGroup)) db.RegisterModel(new(OrgPinnedRepo)) } // OrgPinnedRepo represents a pinned repository for an organization type OrgPinnedRepo struct { ID int64 `xorm:"pk autoincr"` OrgID int64 `xorm:"INDEX NOT NULL"` RepoID int64 `xorm:"INDEX NOT NULL"` GroupID int64 `xorm:"INDEX"` // 0 = ungrouped DisplayOrder int `xorm:"DEFAULT 0"` CreatedUnix timeutil.TimeStamp `xorm:"created"` Repo interface{} `xorm:"-"` // Will be set by caller (repo_model.Repository) Group *OrgPinnedGroup `xorm:"-"` } // TableName returns the table name for OrgPinnedRepo func (p *OrgPinnedRepo) TableName() string { return "org_pinned_repo" } // GetOrgPinnedGroups returns all pinned groups for an organization func GetOrgPinnedGroups(ctx context.Context, orgID int64) ([]*OrgPinnedGroup, error) { groups := make([]*OrgPinnedGroup, 0, 10) return groups, db.GetEngine(ctx). Where("org_id = ?", orgID). OrderBy("display_order ASC, id ASC"). Find(&groups) } // GetOrgPinnedGroup returns a pinned group by ID func GetOrgPinnedGroup(ctx context.Context, id int64) (*OrgPinnedGroup, error) { group := new(OrgPinnedGroup) has, err := db.GetEngine(ctx).ID(id).Get(group) if err != nil { return nil, err } if !has { return nil, ErrOrgPinnedGroupNotExist{ID: id} } return group, nil } // CreateOrgPinnedGroup creates a new pinned group for an organization func CreateOrgPinnedGroup(ctx context.Context, group *OrgPinnedGroup) error { _, err := db.GetEngine(ctx).Insert(group) return err } // UpdateOrgPinnedGroup updates a pinned group func UpdateOrgPinnedGroup(ctx context.Context, group *OrgPinnedGroup) error { _, err := db.GetEngine(ctx).ID(group.ID).Cols("name", "display_order", "collapsed").Update(group) return err } // DeleteOrgPinnedGroup deletes a pinned group and moves its repos to ungrouped func DeleteOrgPinnedGroup(ctx context.Context, groupID int64) error { ctx, committer, err := db.TxContext(ctx) if err != nil { return err } defer committer.Close() // Move all repos in this group to ungrouped (group_id = 0) if _, err := db.GetEngine(ctx). Where("group_id = ?", groupID). Cols("group_id"). Update(&OrgPinnedRepo{GroupID: 0}); err != nil { return err } // Delete the group if _, err := db.GetEngine(ctx).ID(groupID).Delete(new(OrgPinnedGroup)); err != nil { return err } return committer.Commit() } // GetOrgPinnedRepos returns all pinned repos for an organization func GetOrgPinnedRepos(ctx context.Context, orgID int64) ([]*OrgPinnedRepo, error) { pinnedRepos := make([]*OrgPinnedRepo, 0, 20) return pinnedRepos, db.GetEngine(ctx). Where("org_id = ?", orgID). OrderBy("group_id ASC, display_order ASC, id ASC"). Find(&pinnedRepos) } // GetOrgPinnedRepoIDs returns the repo IDs of all pinned repos for an organization func GetOrgPinnedRepoIDs(ctx context.Context, orgID int64) ([]int64, error) { pinnedRepos, err := GetOrgPinnedRepos(ctx, orgID) if err != nil { return nil, err } repoIDs := make([]int64, len(pinnedRepos)) for i, p := range pinnedRepos { repoIDs[i] = p.RepoID } return repoIDs, nil } // LoadPinnedRepoGroups loads the groups for pinned repos func LoadPinnedRepoGroups(ctx context.Context, pinnedRepos []*OrgPinnedRepo, orgID int64) error { if len(pinnedRepos) == 0 { return nil } groups, err := GetOrgPinnedGroups(ctx, orgID) if err != nil { return err } groupMap := make(map[int64]*OrgPinnedGroup) for _, g := range groups { groupMap[g.ID] = g } for _, p := range pinnedRepos { if p.GroupID > 0 { p.Group = groupMap[p.GroupID] } } return nil } // IsRepoPinned checks if a repo is already pinned for an organization func IsRepoPinned(ctx context.Context, orgID, repoID int64) (bool, error) { return db.GetEngine(ctx). Where("org_id = ? AND repo_id = ?", orgID, repoID). Exist(new(OrgPinnedRepo)) } // CreateOrgPinnedRepo pins a repository to an organization func CreateOrgPinnedRepo(ctx context.Context, pinned *OrgPinnedRepo) error { // Check if already pinned exists, err := IsRepoPinned(ctx, pinned.OrgID, pinned.RepoID) if err != nil { return err } if exists { return ErrOrgPinnedRepoAlreadyExist{OrgID: pinned.OrgID, RepoID: pinned.RepoID} } _, err = db.GetEngine(ctx).Insert(pinned) return err } // UpdateOrgPinnedRepo updates a pinned repo's group or order func UpdateOrgPinnedRepo(ctx context.Context, pinned *OrgPinnedRepo) error { _, err := db.GetEngine(ctx).ID(pinned.ID).Cols("group_id", "display_order").Update(pinned) return err } // DeleteOrgPinnedRepo unpins a repository from an organization func DeleteOrgPinnedRepo(ctx context.Context, orgID, repoID int64) error { _, err := db.GetEngine(ctx). Where("org_id = ? AND repo_id = ?", orgID, repoID). Delete(new(OrgPinnedRepo)) return err } // DeleteOrgPinnedRepoByID unpins a repository by pinned ID func DeleteOrgPinnedRepoByID(ctx context.Context, id int64) error { _, err := db.GetEngine(ctx).ID(id).Delete(new(OrgPinnedRepo)) return err } // ReorderOrgPinnedRepos updates the display order of pinned repos func ReorderOrgPinnedRepos(ctx context.Context, orgID int64, repoOrders []PinnedRepoOrder) error { ctx, committer, err := db.TxContext(ctx) if err != nil { return err } defer committer.Close() for _, order := range repoOrders { if _, err := db.GetEngine(ctx). Where("org_id = ? AND repo_id = ?", orgID, order.RepoID). Cols("group_id", "display_order"). Update(&OrgPinnedRepo{ GroupID: order.GroupID, DisplayOrder: order.DisplayOrder, }); err != nil { return err } } return committer.Commit() } // ReorderOrgPinnedGroups updates the display order of pinned groups func ReorderOrgPinnedGroups(ctx context.Context, orgID int64, groupOrders []PinnedGroupOrder) error { ctx, committer, err := db.TxContext(ctx) if err != nil { return err } defer committer.Close() for _, order := range groupOrders { if _, err := db.GetEngine(ctx). Where("org_id = ? AND id = ?", orgID, order.GroupID). Cols("display_order"). Update(&OrgPinnedGroup{DisplayOrder: order.DisplayOrder}); err != nil { return err } } return committer.Commit() } // PinnedRepoOrder represents the order for a pinned repo type PinnedRepoOrder struct { RepoID int64 `json:"repo_id"` GroupID int64 `json:"group_id"` DisplayOrder int `json:"display_order"` } // PinnedGroupOrder represents the order for a pinned group type PinnedGroupOrder struct { GroupID int64 `json:"group_id"` DisplayOrder int `json:"display_order"` } // ErrOrgPinnedGroupNotExist represents a "pinned group not exist" error type ErrOrgPinnedGroupNotExist struct { ID int64 } func (err ErrOrgPinnedGroupNotExist) Error() string { return "pinned group does not exist" } // ErrOrgPinnedRepoAlreadyExist represents a "repo already pinned" error type ErrOrgPinnedRepoAlreadyExist struct { OrgID int64 RepoID int64 } func (err ErrOrgPinnedRepoAlreadyExist) Error() string { return "repository is already pinned" }