// 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, } }