Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions models/packages/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,11 @@ func SetRepositoryLink(ctx context.Context, packageID, repoID int64) error {
return err
}

func UnlinkRepository(ctx context.Context, packageID int64) error {
_, err := db.GetEngine(ctx).ID(packageID).Cols("repo_id").Update(&Package{RepoID: 0})
return err
}

// UnlinkRepositoryFromAllPackages unlinks every package from the repository
func UnlinkRepositoryFromAllPackages(ctx context.Context, repoID int64) error {
_, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Cols("repo_id").Update(&Package{})
Expand Down
18 changes: 12 additions & 6 deletions routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -1510,13 +1510,19 @@ func Routes() *web.Router {

// NOTE: these are Gitea package management API - see packages.CommonRoutes and packages.DockerContainerRoutes for endpoints that implement package manager APIs
m.Group("/packages/{username}", func() {
m.Group("/{type}/{name}/{version}", func() {
m.Get("", reqToken(), packages.GetPackage)
m.Delete("", reqToken(), reqPackageAccess(perm.AccessModeWrite), packages.DeletePackage)
m.Get("/files", reqToken(), packages.ListPackageFiles)
m.Group("/{type}/{name}", func() {
m.Group("/{version}", func() {
m.Get("", packages.GetPackage)
m.Delete("", reqPackageAccess(perm.AccessModeWrite), packages.DeletePackage)
m.Get("/files", packages.ListPackageFiles)
})

m.Post("/-/link/{repo_name}", reqPackageAccess(perm.AccessModeWrite), packages.LinkPackage)
m.Post("/-/unlink", reqPackageAccess(perm.AccessModeWrite), packages.UnlinkPackage)
})
m.Get("/", reqToken(), packages.ListPackages)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead), checkTokenPublicOnly())

m.Get("/", packages.ListPackages)
}, reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead), checkTokenPublicOnly())

// Organizations
m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), org.ListMyOrgs)
Expand Down
116 changes: 116 additions & 0 deletions routers/api/v1/packages/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
package packages

import (
"errors"
"net/http"

"code.gitea.io/gitea/models/packages"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/optional"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/routers/api/v1/utils"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
Expand Down Expand Up @@ -213,3 +216,116 @@ func ListPackageFiles(ctx *context.APIContext) {

ctx.JSON(http.StatusOK, apiPackageFiles)
}

// LinkPackage sets a repository link for a package
func LinkPackage(ctx *context.APIContext) {
// swagger:operation POST /packages/{owner}/{type}/{name}/-/link/{repo_name} package linkPackage
// ---
// summary: Link a package to a repository
// parameters:
// - name: owner
// in: path
// description: owner of the package
// type: string
// required: true
// - name: type
// in: path
// description: type of the package
// type: string
// required: true
// - name: name
// in: path
// description: name of the package
// type: string
// required: true
// - name: repo_name
// in: path
// description: name of the repository to link.
// type: string
// required: true
// responses:
// "201":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"

pkg, err := packages.GetPackageByName(ctx, ctx.ContextUser.ID, packages.Type(ctx.PathParam("type")), ctx.PathParam("name"))
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusNotFound, "GetPackageByName", err)
} else {
ctx.Error(http.StatusInternalServerError, "GetPackageByName", err)
}
return
}

repo, err := repo_model.GetRepositoryByName(ctx, ctx.ContextUser.ID, ctx.PathParam("repo_name"))
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusNotFound, "GetRepositoryByName", err)
} else {
ctx.Error(http.StatusInternalServerError, "GetRepositoryByName", err)
}
return
}

err = packages_service.LinkToRepository(ctx, pkg, repo, ctx.Doer)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusNotFound, "GetPackageByName", err)
} else {
ctx.Error(http.StatusInternalServerError, "GetPackageByName", err)
}
return
}
ctx.Status(http.StatusCreated)
}

// UnlinkPackage sets a repository link for a package
func UnlinkPackage(ctx *context.APIContext) {
// swagger:operation POST /packages/{owner}/{type}/{name}/-/unlink package unlinkPackage
// ---
// summary: Unlink a package from a repository
// parameters:
// - name: owner
// in: path
// description: owner of the package
// type: string
// required: true
// - name: type
// in: path
// description: type of the package
// type: string
// required: true
// - name: name
// in: path
// description: name of the package
// type: string
// required: true
// responses:
// "201":
// "$ref": "#/responses/empty"
// "404":
// "$ref": "#/responses/notFound"

pkg, err := packages.GetPackageByName(ctx, ctx.ContextUser.ID, packages.Type(ctx.PathParam("type")), ctx.PathParam("name"))
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusNotFound, "GetPackageByName", err)
} else {
ctx.Error(http.StatusInternalServerError, "GetPackageByName", err)
}
return
}

err = packages_service.UnlinkFromRepository(ctx, pkg, ctx.Doer)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
ctx.Error(http.StatusNotFound, "UnlinkFromRepository", err)
} else {
ctx.Error(http.StatusInternalServerError, "UnlinkFromRepository", err)
}
return
}
ctx.Status(http.StatusNoContent)
}
59 changes: 59 additions & 0 deletions services/packages/package_update.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package packages

import (
"context"
"fmt"

org_model "code.gitea.io/gitea/models/organization"
packages_model "code.gitea.io/gitea/models/packages"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/util"
)

func LinkToRepository(ctx context.Context, pkg *packages_model.Package, repo *repo_model.Repository, doer *user_model.User) error {
if pkg.OwnerID != repo.OwnerID {
return util.ErrNotExist
}

perms, err := access_model.GetUserRepoPermission(ctx, repo, doer)
if err != nil {
return fmt.Errorf("error getting permissions for user %d on repository %d: %w", doer.ID, repo.ID, err)
}

if !perms.CanWrite(unit.TypePackages) {
return fmt.Errorf("no permission to link this package and repository, or packages are disabled")
}

if err := packages_model.SetRepositoryLink(ctx, pkg.ID, repo.ID); err != nil {
return fmt.Errorf("error updating package: %w", err)
}
return nil
}

func UnlinkFromRepository(ctx context.Context, pkg *packages_model.Package, doer *user_model.User) error {
user, err := user_model.GetUserByID(ctx, pkg.OwnerID)
if err != nil {
return err
}
if !user.IsAdmin {
if !user.IsOrganization() {
if doer.ID != pkg.OwnerID {
return fmt.Errorf("No permission to unlink this package and repository, or packages are disabled")
}
} else {
isOrgAdmin, err := org_model.OrgFromUser(user).IsOrgAdmin(ctx, doer.ID)
if err != nil {
return err
} else if !isOrgAdmin {
return fmt.Errorf("No permission to unlink this package and repository, or packages are disabled")
}
}
}
return packages_model.UnlinkRepository(ctx, pkg.ID)
}
87 changes: 87 additions & 0 deletions templates/swagger/v1_json.tmpl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading