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>
441 lines
11 KiB
Go
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,
|
|
}
|
|
}
|