0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-04-04 05:45:23 +02:00

add api routes and functions for repository groups

This commit is contained in:
☙◦ The Tablet ❀ GamerGirlandCo ◦❧ 2025-08-13 20:54:32 -04:00
parent 401e633f01
commit 314be8ad26
No known key found for this signature in database
GPG Key ID: 924A5F6AF051E87C
5 changed files with 454 additions and 2 deletions

View File

@ -158,6 +158,8 @@ type CreateRepoOption struct {
// ObjectFormatName of the underlying git repository, empty string for default (sha1)
// enum: ["sha1","sha256"]
ObjectFormatName string `json:"object_format_name" binding:"MaxSize(6)"`
// GroupID of the group which will contain this repository. ignored if the repo owner is not an organization.
GroupID int64 `json:"group_id"`
}
// EditRepoOption options when editing a repository's properties

View File

@ -27,7 +27,7 @@ type NewGroupOption struct {
Visibility VisibleType `json:"visibility"`
}
// MoveGroupOption - options for changing a group's parent and sort order
// MoveGroupOption - options for changing a group or repo's parent and sort order
// swagger:model
type MoveGroupOption struct {
// the new parent group. can be 0 to specify no parent

View File

@ -68,11 +68,15 @@ import (
"net/http"
"strings"
actions_model "code.gitea.io/gitea/models/actions"
auth_model "code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
group_model "code.gitea.io/gitea/models/group"
shared_group_model "code.gitea.io/gitea/models/shared/group"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
@ -485,6 +489,60 @@ func reqOrgOwnership() func(ctx *context.APIContext) {
}
}
// reqGroupMembership user should be organization owner,
// a member of a team with access to the group, or site admin
func reqGroupMembership(mode perm.AccessMode, needsCreatePerm bool) func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
if ctx.IsUserSiteAdmin() {
return
}
gid := ctx.PathParamInt64("group_id")
g, err := group_model.GetGroupByID(ctx, gid)
if err != nil {
ctx.APIErrorInternal(err)
return
}
err = g.LoadOwner(ctx)
if err != nil {
ctx.APIErrorInternal(err)
return
}
var canAccess bool
if ctx.IsSigned {
canAccess, err = g.CanAccessAtLevel(ctx, ctx.Doer.ID, mode)
} else {
canAccess, err = g.CanAccessAtLevel(ctx, 0, mode)
}
if err != nil {
ctx.APIErrorInternal(err)
return
}
igm, err := shared_group_model.IsGroupMember(ctx, gid, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
}
if !igm && !canAccess {
ctx.APIErrorNotFound()
return
}
if needsCreatePerm {
canCreateIn := false
if ctx.IsSigned {
canCreateIn, err = g.CanCreateIn(ctx, ctx.Doer.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
}
if !canCreateIn {
ctx.APIError(http.StatusForbidden, fmt.Sprintf("User[%d] does not have permission to create new items in group[%d]", ctx.Doer.ID, gid))
return
}
}
}
}
// reqTeamMembership user should be an team member, or a site admin
func reqTeamMembership() func(ctx *context.APIContext) {
return func(ctx *context.APIContext) {
@ -1154,6 +1212,7 @@ func Routes() *web.Router {
m.Combo("").Get(reqAnyRepoReader(), repo.Get).
Delete(reqToken(), reqOwner(), repo.Delete).
Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), repo.Edit)
m.Post("/groups/move", reqToken(), bind(api.EditGroupOption{}), reqOrgMembership(), reqGroupMembership(perm.AccessModeWrite, false), repo.MoveRepoToGroup)
m.Post("/generate", reqToken(), reqRepoReader(unit.TypeCode), bind(api.GenerateRepoOption{}), repo.Generate)
m.Group("/transfer", func() {
m.Post("", reqOwner(), bind(api.TransferRepoOption{}), repo.Transfer)
@ -1661,6 +1720,10 @@ func Routes() *web.Router {
m.Delete("", org.UnblockUser)
})
}, reqToken(), reqOrgOwnership())
m.Group("/groups", func() {
m.Post("/new", reqToken(), reqGroupMembership(perm.AccessModeWrite, true), group.NewGroup)
m.Post("/{group_id}/move", reqToken(), reqGroupMembership(perm.AccessModeWrite, false), group.MoveGroup)
})
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true), checkTokenPublicOnly())
m.Group("/teams/{teamid}", func() {
m.Combo("").Get(reqToken(), org.GetTeam).
@ -1741,7 +1804,15 @@ func Routes() *web.Router {
m.Get("/search", repo.TopicSearch)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
}, sudo())
m.Group("/groups", func() {
m.Group("/{group_id}", func() {
m.Combo("").
Get(reqGroupMembership(perm.AccessModeRead, false), group.GetGroup).
Patch(reqToken(), reqGroupMembership(perm.AccessModeWrite, false), bind(api.EditGroupOption{}), group.EditGroup).
Delete(reqToken(), reqGroupMembership(perm.AccessModeAdmin, false), group.DeleteGroup)
m.Post("/new", reqToken(), reqGroupMembership(perm.AccessModeWrite, true), bind(api.NewGroupOption{}), group.NewSubGroup)
})
})
return m
}

View File

@ -0,0 +1,329 @@
package group
import (
"net/http"
"strings"
group_model "code.gitea.io/gitea/models/group"
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"
group_service "code.gitea.io/gitea/services/group"
)
func createCommonGroup(ctx *context.APIContext, parentGroupID int64) (*api.Group, error) {
form := web.GetForm(ctx).(*api.NewGroupOption)
group := &group_model.Group{
Name: form.Name,
Description: form.Description,
OwnerID: ctx.Org.Organization.ID,
LowerName: strings.ToLower(form.Name),
Visibility: form.Visibility,
ParentGroupID: parentGroupID,
}
if err := group_service.NewGroup(ctx, group); err != nil {
return nil, err
}
return convert.ToAPIGroup(ctx, group, ctx.Doer)
}
// NewGroup create a new root-level group in an organization
func NewGroup(ctx *context.APIContext) {
// swagger:operation POST /orgs/{org}/groups/new repository-group groupNew
// ---
// summary: create a root-level repository group for an organization
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateGroupOption"
// responses:
// "201":
// "$ref": "#/responses/Group"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
ag, err := createCommonGroup(ctx, 0)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusCreated, ag)
}
// NewSubGroup create a new subgroup inside a group
func NewSubGroup(ctx *context.APIContext) {
// swagger:operation POST /groups/{group_id}/new repository-group groupNewSubGroup
// ---
// summary: create a subgroup inside a group
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: group_id
// in: path
// description: id of the group to create a subgroup in
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateGroupOption"
// responses:
// "201":
// "$ref": "#/responses/Group"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
var (
group *api.Group
err error
)
gid := ctx.PathParamInt64("group_id")
group, err = createCommonGroup(ctx, gid)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusCreated, group)
}
// MoveGroup - move a group to a different group in the same organization, or to the root level if
func MoveGroup(ctx *context.APIContext) {
// swagger:operation POST /orgs/{org}/groups/{group_id}/move repository-group groupMove
// ---
// summary: move a group to a different parent group
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: group_id
// in: path
// description: id of the group to move
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/MoveGroupOption"
// responses:
// "200":
// "$ref": "#/responses/Group"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.MoveGroupOption)
id := ctx.PathParamInt64("group_id")
var err error
npos := -1
if form.NewPos != nil {
npos = *form.NewPos
}
err = group_service.MoveGroupItem(ctx, group_service.MoveGroupOptions{
form.NewParent, id, true, npos,
}, 3)
if group_model.IsErrGroupNotExist(err) {
ctx.APIErrorNotFound()
return
}
if err != nil {
ctx.APIErrorInternal(err)
return
}
var (
ng *group_model.Group
apiGroup *api.Group
)
ng, err = group_model.GetGroupByID(ctx, id)
if group_model.IsErrGroupNotExist(err) {
ctx.APIErrorNotFound()
return
}
if err != nil {
ctx.APIErrorInternal(err)
return
}
apiGroup, err = convert.ToAPIGroup(ctx, ng, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
}
ctx.JSON(http.StatusOK, apiGroup)
}
// EditGroup - update a group in an organization
func EditGroup(ctx *context.APIContext) {
// swagger:operation PATCH /orgs/{org}/groups/{group_id} repository-group groupEdit
// ---
// summary: edits a group in an organization. only fields that are set will be changed.
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: group_id
// in: path
// description: id of the group to edit
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditGroupOption"
// responses:
// "200":
// "$ref": "#/responses/Group"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
var (
err error
group *group_model.Group
)
form := web.GetForm(ctx).(*api.EditGroupOption)
gid := ctx.PathParamInt64("group_id")
group, err = group_model.GetGroupByID(ctx, gid)
if group_model.IsErrGroupNotExist(err) {
ctx.APIErrorNotFound()
return
}
if err != nil {
ctx.APIErrorInternal(err)
return
}
if form.Visibility != nil {
group.Visibility = *form.Visibility
}
if form.Description != nil {
group.Description = *form.Description
}
if form.Name != nil {
group.Name = *form.Name
}
err = group_model.UpdateGroup(ctx, group)
if err != nil {
ctx.APIErrorInternal(err)
return
}
var newAPIGroup *api.Group
newAPIGroup, err = convert.ToAPIGroup(ctx, group, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, newAPIGroup)
}
func GetGroup(ctx *context.APIContext) {
// swagger:operation GET /orgs/{org}/groups/{group_id} repository-group groupGet
// ---
// summary: gets a group in an organization
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: group_id
// in: path
// description: id of the group to retrieve
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditGroupOption"
// responses:
// "200":
// "$ref": "#/responses/Group"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
var (
err error
group *group_model.Group
)
group, err = group_model.GetGroupByID(ctx, ctx.PathParamInt64("group_id"))
if group_model.IsErrGroupNotExist(err) {
ctx.APIErrorNotFound()
return
}
if group.OwnerID != ctx.Org.Organization.ID {
ctx.APIErrorNotFound()
return
}
if err != nil {
ctx.APIErrorInternal(err)
return
}
apiGroup, err := convert.ToAPIGroup(ctx, group, ctx.Doer)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.JSON(http.StatusOK, apiGroup)
}
func DeleteGroup(ctx *context.APIContext) {
// swagger:operation DELETE /orgs/{org}/groups/{group_id} repositoryGroup groupDelete
// ---
// summary: Delete a repository group
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the group to delete
// type: string
// required: true
// - name: group_id
// in: path
// description: id of the group to delete
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
err := group_service.DeleteGroup(ctx, ctx.PathParamInt64("group_id"))
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}

View File

@ -36,6 +36,7 @@ import (
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
feed_service "code.gitea.io/gitea/services/feed"
group_service "code.gitea.io/gitea/services/group"
"code.gitea.io/gitea/services/issue"
repo_service "code.gitea.io/gitea/services/repository"
)
@ -263,6 +264,7 @@ func CreateUserRepo(ctx *context.APIContext, owner *user_model.User, opt api.Cre
TrustModel: repo_model.ToTrustModel(opt.TrustModel),
IsTemplate: opt.Template,
ObjectFormatName: opt.ObjectFormatName,
GroupID: opt.GroupID,
})
if err != nil {
if repo_model.IsErrRepoAlreadyExist(err) {
@ -1264,3 +1266,51 @@ func ListRepoActivityFeeds(ctx *context.APIContext) {
ctx.JSON(http.StatusOK, convert.ToActivities(ctx, feeds, ctx.Doer))
}
func MoveRepoToGroup(ctx *context.APIContext) {
// swagger:operation POST /repo/{owner}/{repo}/move
// ---
// summary: move a repository to another group
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true // - name: body
// in: body
// schema:
// "$ref": "#/definitions/MoveGroupOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
form := web.GetForm(ctx).(*api.MoveGroupOption)
npos := -1
if form.NewPos != nil {
npos = *form.NewPos
}
err := group_service.MoveGroupItem(ctx, group_service.MoveGroupOptions{
IsGroup: false, NewPos: npos,
ItemID: ctx.Repo.Repository.ID,
NewParent: form.NewParent,
}, ctx.Doer.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.Status(http.StatusNoContent)
}