gitea/routers/api/v1/org/pinned.go
logikonline b816ee4eec feat: add Phases 3-5 enhancements (org profiles, pages, wiki v2 API)
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>
2026-01-09 15:14:27 -05:00

441 lines
11 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package org
import (
"net/http"
access_model "code.gitea.io/gitea/models/perm/access"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
org_service "code.gitea.io/gitea/services/org"
)
// ListPinnedRepos returns the pinned repositories for an organization
func ListPinnedRepos(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/pinned organization orgListPinnedRepos
// ---
// summary: List an organization's pinned repositories
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/OrgPinnedRepoList"
// "404":
// "$ref": "#/responses/notFound"
pinnedRepos, err := org_service.GetOrgPinnedReposWithDetails(ctx, ctx.Org.Organization.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
apiPinnedRepos := make([]*api.OrgPinnedRepo, 0, len(pinnedRepos))
for _, p := range pinnedRepos {
if p.Repo == nil {
continue
}
apiPinnedRepos = append(apiPinnedRepos, convertOrgPinnedRepo(ctx, p))
}
ctx.JSON(http.StatusOK, apiPinnedRepos)
}
// AddPinnedRepo pins a repository to an organization
func AddPinnedRepo(ctx *context.APIContext) {
// swagger:operation POST /orgs/{org}/pinned organization orgAddPinnedRepo
// ---
// summary: Pin a repository to an organization
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: body
// in: body
// required: true
// schema:
// "$ref": "#/definitions/AddOrgPinnedRepoOption"
// responses:
// "201":
// "$ref": "#/responses/OrgPinnedRepo"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.AddOrgPinnedRepoOption)
// Get the repository
repo, err := repo_model.GetRepositoryByName(ctx, ctx.Org.Organization.ID, form.RepoName)
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
ctx.APIErrorNotFound("GetRepositoryByName", err)
return
}
ctx.APIErrorInternal(err)
return
}
// Create pinned repo
pinned := &organization.OrgPinnedRepo{
OrgID: ctx.Org.Organization.ID,
RepoID: repo.ID,
GroupID: form.GroupID,
DisplayOrder: form.DisplayOrder,
}
if err := organization.CreateOrgPinnedRepo(ctx, pinned); err != nil {
if _, ok := err.(organization.ErrOrgPinnedRepoAlreadyExist); ok {
ctx.APIError(http.StatusUnprocessableEntity, "Repository is already pinned")
return
}
ctx.APIErrorInternal(err)
return
}
// Load the repo details
pinned.Repo = repo
if pinned.GroupID > 0 {
pinned.Group, _ = organization.GetOrgPinnedGroup(ctx, pinned.GroupID)
}
ctx.JSON(http.StatusCreated, convertOrgPinnedRepo(ctx, pinned))
}
// DeletePinnedRepo unpins a repository from an organization
func DeletePinnedRepo(ctx *context.APIContext) {
// swagger:operation DELETE /orgs/{org}/pinned/{repo} organization orgDeletePinnedRepo
// ---
// summary: Unpin a repository from an organization
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repository
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
repoName := ctx.PathParam("repo")
// Get the repository
repo, err := repo_model.GetRepositoryByName(ctx, ctx.Org.Organization.ID, repoName)
if err != nil {
if repo_model.IsErrRepoNotExist(err) {
ctx.APIErrorNotFound("GetRepositoryByName", err)
return
}
ctx.APIErrorInternal(err)
return
}
if err := organization.DeleteOrgPinnedRepo(ctx, ctx.Org.Organization.ID, repo.ID); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// ReorderPinnedRepos updates the order of pinned repositories
func ReorderPinnedRepos(ctx *context.APIContext) {
// swagger:operation PUT /orgs/{org}/pinned/reorder organization orgReorderPinnedRepos
// ---
// summary: Reorder pinned repositories
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: body
// in: body
// required: true
// schema:
// "$ref": "#/definitions/ReorderOrgPinnedReposOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
form := web.GetForm(ctx).(*api.ReorderOrgPinnedReposOption)
// Convert API order to model order
orders := make([]organization.PinnedRepoOrder, len(form.Orders))
for i, o := range form.Orders {
orders[i] = organization.PinnedRepoOrder{
RepoID: o.RepoID,
GroupID: o.GroupID,
DisplayOrder: o.DisplayOrder,
}
}
if err := organization.ReorderOrgPinnedRepos(ctx, ctx.Org.Organization.ID, orders); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// ListPinnedGroups returns the pinned groups for an organization
func ListPinnedGroups(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/pinned/groups organization orgListPinnedGroups
// ---
// summary: List an organization's pinned repository groups
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/OrgPinnedGroupList"
// "404":
// "$ref": "#/responses/notFound"
groups, err := organization.GetOrgPinnedGroups(ctx, ctx.Org.Organization.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
apiGroups := make([]*api.OrgPinnedGroup, len(groups))
for i, g := range groups {
apiGroups[i] = convertOrgPinnedGroup(g)
}
ctx.JSON(http.StatusOK, apiGroups)
}
// CreatePinnedGroup creates a new pinned group for an organization
func CreatePinnedGroup(ctx *context.APIContext) {
// swagger:operation POST /orgs/{org}/pinned/groups organization orgCreatePinnedGroup
// ---
// summary: Create a pinned repository group
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: body
// in: body
// required: true
// schema:
// "$ref": "#/definitions/CreateOrgPinnedGroupOption"
// responses:
// "201":
// "$ref": "#/responses/OrgPinnedGroup"
// "403":
// "$ref": "#/responses/forbidden"
form := web.GetForm(ctx).(*api.CreateOrgPinnedGroupOption)
group := &organization.OrgPinnedGroup{
OrgID: ctx.Org.Organization.ID,
Name: form.Name,
DisplayOrder: form.DisplayOrder,
Collapsed: form.Collapsed,
}
if err := organization.CreateOrgPinnedGroup(ctx, group); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusCreated, convertOrgPinnedGroup(group))
}
// UpdatePinnedGroup updates a pinned group
func UpdatePinnedGroup(ctx *context.APIContext) {
// swagger:operation PUT /orgs/{org}/pinned/groups/{id} organization orgUpdatePinnedGroup
// ---
// summary: Update a pinned repository group
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: id
// in: path
// description: id of the group
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// required: true
// schema:
// "$ref": "#/definitions/UpdateOrgPinnedGroupOption"
// responses:
// "200":
// "$ref": "#/responses/OrgPinnedGroup"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
form := web.GetForm(ctx).(*api.UpdateOrgPinnedGroupOption)
groupID := ctx.PathParamInt64("id")
group, err := organization.GetOrgPinnedGroup(ctx, groupID)
if err != nil {
if _, ok := err.(organization.ErrOrgPinnedGroupNotExist); ok {
ctx.APIErrorNotFound("GetOrgPinnedGroup", err)
return
}
ctx.APIErrorInternal(err)
return
}
// Verify group belongs to this org
if group.OrgID != ctx.Org.Organization.ID {
ctx.APIErrorNotFound()
return
}
if form.Name != nil {
group.Name = *form.Name
}
if form.DisplayOrder != nil {
group.DisplayOrder = *form.DisplayOrder
}
if form.Collapsed != nil {
group.Collapsed = *form.Collapsed
}
if err := organization.UpdateOrgPinnedGroup(ctx, group); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, convertOrgPinnedGroup(group))
}
// DeletePinnedGroup deletes a pinned group
func DeletePinnedGroup(ctx *context.APIContext) {
// swagger:operation DELETE /orgs/{org}/pinned/groups/{id} organization orgDeletePinnedGroup
// ---
// summary: Delete a pinned repository group
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: id
// in: path
// description: id of the group
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
groupID := ctx.PathParamInt64("id")
group, err := organization.GetOrgPinnedGroup(ctx, groupID)
if err != nil {
if _, ok := err.(organization.ErrOrgPinnedGroupNotExist); ok {
ctx.APIErrorNotFound("GetOrgPinnedGroup", err)
return
}
ctx.APIErrorInternal(err)
return
}
// Verify group belongs to this org
if group.OrgID != ctx.Org.Organization.ID {
ctx.APIErrorNotFound()
return
}
if err := organization.DeleteOrgPinnedGroup(ctx, groupID); err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}
// convertOrgPinnedRepo converts a pinned repo to API format
func convertOrgPinnedRepo(ctx *context.APIContext, p *organization.OrgPinnedRepo) *api.OrgPinnedRepo {
result := &api.OrgPinnedRepo{
ID: p.ID,
RepoID: p.RepoID,
GroupID: p.GroupID,
DisplayOrder: p.DisplayOrder,
}
if p.Repo != nil {
if repo, ok := p.Repo.(*repo_model.Repository); ok {
result.Repo = convert.ToRepo(ctx, repo, access_model.Permission{AccessMode: perm.AccessModeRead})
}
}
if p.Group != nil {
result.Group = convertOrgPinnedGroup(p.Group)
}
return result
}
// convertOrgPinnedGroup converts a pinned group to API format
func convertOrgPinnedGroup(g *organization.OrgPinnedGroup) *api.OrgPinnedGroup {
return &api.OrgPinnedGroup{
ID: g.ID,
Name: g.Name,
DisplayOrder: g.DisplayOrder,
Collapsed: g.Collapsed,
}
}