Phase 3: Organization Public Profile Page - Pinned repositories with groups - Public members display with roles - API endpoints for pinned repos and groups Phase 4: Gitea Pages Foundation - Landing page templates (simple, docs, product, portfolio) - Custom domain support with verification - YAML configuration parser (.gitea/landing.yaml) - Repository settings UI for pages Phase 5: Enhanced Wiki System with V2 API - Full CRUD operations via v2 API - Full-text search with WikiIndex table - Link graph visualization - Wiki health metrics (orphaned, dead links, outdated) - Designed for external AI plugin integration - Developer guide for .NET integration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
274 lines
7.8 KiB
Go
274 lines
7.8 KiB
Go
// 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"
|
|
}
|