0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-04-04 14:36:16 +02:00
gitea/models/group/group.go
☙◦ The Tablet ❀ GamerGirlandCo ◦❧ ab1eb20ab3
run formatter and fix lint errors
2026-04-02 20:44:11 -04:00

418 lines
11 KiB
Go

// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package group
import (
"context"
"fmt"
"net/url"
"slices"
"strconv"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"xorm.io/builder"
)
// Group represents a group of repositories for a user or organization
type Group struct {
ID int64 `xorm:"pk autoincr"`
OwnerID int64 `xorm:"INDEX NOT NULL"`
OwnerName string
Owner *user_model.User `xorm:"-"`
LowerName string `xorm:"TEXT NOT NULL"`
Name string `xorm:"TEXT NOT NULL"`
Description string `xorm:"TEXT"`
Visibility structs.VisibleType `xorm:"NOT NULL DEFAULT 0"`
Avatar string `xorm:"VARCHAR(64)"`
ParentGroupID int64 `xorm:"INDEX DEFAULT NULL"`
ParentGroup *Group `xorm:"-"`
Subgroups RepoGroupList `xorm:"-"`
SortOrder int `xorm:"INDEX"`
}
// GroupLink returns the link to this group
func (g *Group) GroupLink() string {
return setting.AppSubURL + "/" + url.PathEscape(g.OwnerName) + "/groups/" + strconv.FormatInt(g.ID, 10)
}
func (g *Group) OrgGroupLink() string {
return setting.AppSubURL + "/org/" + url.PathEscape(g.OwnerName) + "/groups/" + strconv.FormatInt(g.ID, 10)
}
func (Group) TableName() string { return "repo_group" }
func init() {
db.RegisterModel(new(Group))
db.RegisterModel(new(RepoGroupTeam))
db.RegisterModel(new(RepoGroupUnit))
}
func (g *Group) doLoadSubgroups(ctx context.Context, recursive bool, cond builder.Cond, currentLevel int) error {
if currentLevel >= 20 {
return ErrGroupTooDeep{
g.ID,
}
}
if g.Subgroups != nil {
return nil
}
var err error
g.Subgroups, err = FindGroupsByCond(ctx, &FindGroupsOptions{
ParentGroupID: g.ID,
}, cond)
if err != nil {
return err
}
slices.SortStableFunc(g.Subgroups, func(a, b *Group) int {
return a.SortOrder - b.SortOrder
})
if recursive {
for _, group := range g.Subgroups {
err = group.doLoadSubgroups(ctx, recursive, cond, currentLevel+1)
if err != nil {
return err
}
}
}
return nil
}
func (g *Group) LoadSubgroups(ctx context.Context, recursive bool) error {
fgo := &FindGroupsOptions{
ParentGroupID: g.ID,
}
return g.doLoadSubgroups(ctx, recursive, fgo.ToConds(), 0)
}
func (g *Group) LoadAccessibleSubgroups(ctx context.Context, recursive bool, doer *user_model.User) error {
return g.doLoadSubgroups(ctx, recursive, AccessibleGroupCondition(doer, unit.TypeInvalid, perm.AccessModeRead), 0)
}
func (g *Group) LoadAttributes(ctx context.Context) error {
err := g.LoadOwner(ctx)
if err != nil {
return err
}
return g.LoadParentGroup(ctx)
}
func (g *Group) LoadParentGroup(ctx context.Context) error {
if g.ParentGroup != nil {
return nil
}
if g.ParentGroupID == 0 {
return nil
}
parentGroup, err := GetGroupByID(ctx, g.ParentGroupID)
if err != nil {
return err
}
g.ParentGroup = parentGroup
return nil
}
func (g *Group) LoadOwner(ctx context.Context) error {
if g.Owner != nil {
return nil
}
var err error
g.Owner, err = user_model.GetUserByID(ctx, g.OwnerID)
return err
}
func (g *Group) CanAccess(ctx context.Context, user *user_model.User) (bool, error) {
return g.CanAccessAtLevel(ctx, user, perm.AccessModeRead)
}
func (g *Group) CanAccessAtLevel(ctx context.Context, user *user_model.User, level perm.AccessMode) (bool, error) {
orCond := builder.Or(AccessibleGroupCondition(user, unit.TypeInvalid, level))
if level == perm.AccessModeRead {
orCond = orCond.Or(builder.Eq{"`repo_group`.visibility": structs.VisibleTypePublic})
}
return db.GetEngine(ctx).Table(g.TableName()).Where(orCond.And(builder.Eq{"`repo_group`.id": g.ID})).Exist()
}
func (g *Group) IsOwnedBy(ctx context.Context, userID int64) (bool, error) {
return db.GetEngine(ctx).
Where("team_user.uid = ?", userID).
Join("INNER", "team_user", "team_user.team_id = repo_group_team.team_id").
And("repo_group_team.access_mode = ?", perm.AccessModeOwner).
And("repo_group_team.group_id = ?", g.ID).
Table("repo_group_team").
Exist()
}
func (g *Group) CanCreateIn(ctx context.Context, userID int64) (bool, error) {
return db.GetEngine(ctx).
Where("team_user.uid = ?", userID).
Join("INNER", "team_user", "team_user.team_id = repo_group_team.team_id").
And("repo_group_team.group_id = ?", g.ID).
And("repo_group_team.can_create_in = ?", true).
Table("repo_group_team").
Exist()
}
func (g *Group) IsAdminOf(ctx context.Context, userID int64) (bool, error) {
return db.GetEngine(ctx).
Where("team_user.uid = ?", userID).
Join("INNER", "team_user", "team_user.team_id = repo_group_team.team_id").
And("repo_group_team.group_id = ?", g.ID).
And("repo_group_team.access_mode >= ?", perm.AccessModeAdmin).
Table("repo_group_team").
Exist()
}
func (g *Group) ShortName(length int) string {
return util.EllipsisDisplayString(g.Name, length)
}
// Depth retrieves the depth/nesting level of this group
func (g *Group) Depth(ctx context.Context) (d int) {
err := g.LoadParentGroup(ctx)
if err != nil {
return 0
}
pg := g.ParentGroup
for {
if pg == nil {
break
}
if pg.ParentGroup == nil {
err = pg.LoadParentGroup(ctx)
if err != nil {
return 0
}
}
d++
pg = pg.ParentGroup
}
return d
}
// DisplayLeftMargin generates a value for the left margin
// displayed on the frontend beside this group
func (g *Group) DisplayLeftMargin(ctx context.Context) string {
return fmt.Sprintf("%drem", g.Depth(ctx)+1)
}
func GetGroupByID(ctx context.Context, id int64) (*Group, error) {
group := new(Group)
has, err := db.GetEngine(ctx).ID(id).Get(group)
if err != nil {
return nil, err
} else if !has {
return nil, ErrGroupNotExist{id}
}
return group, nil
}
func GetGroupByRepoID(ctx context.Context, repoID int64) (*Group, error) {
group := new(Group)
_, err := db.GetEngine(ctx).
In("id", builder.
Select("group_id").
From("repo").
Where(builder.Eq{"id": repoID})).
Get(group)
return group, err
}
func ParentGroupCondByRepoID(ctx context.Context, repoID int64, idStr string) builder.Cond {
g, err := GetGroupByRepoID(ctx, repoID)
if err != nil {
return builder.In(idStr)
}
return ParentGroupCond(ctx, idStr, g.ID)
}
type FindGroupsOptions struct {
db.ListOptions
OwnerID int64
ParentGroupID int64
CanCreateIn optional.Option[bool]
ActorID int64
Name string
}
func (opts FindGroupsOptions) ToConds() builder.Cond {
cond := builder.NewCond()
if opts.OwnerID != 0 {
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
}
if opts.ParentGroupID > 0 {
cond = cond.And(builder.Eq{"parent_group_id": opts.ParentGroupID})
} else if opts.ParentGroupID == 0 {
cond = cond.And(builder.Eq{"parent_group_id": 0})
}
if opts.CanCreateIn.Has() && opts.ActorID > 0 {
cond = cond.And(builder.In("id",
builder.Select("repo_group_team.group_id").
From("repo_group_team").
Where(builder.Eq{"team_user.uid": opts.ActorID}).
Join("INNER", "team_user", "team_user.team_id = repo_group_team.team_id").
And(builder.Eq{"repo_group_team.can_create_in": true})))
}
if opts.Name != "" {
cond = cond.And(builder.Eq{"lower_name": opts.Name})
}
return cond
}
func FindGroups(ctx context.Context, opts *FindGroupsOptions) (RepoGroupList, error) {
sess := db.GetEngine(ctx).Where(opts.ToConds())
if opts.Page > 0 {
sess = db.SetSessionPagination(sess, opts)
}
groups := make([]*Group, 0, 10)
return groups, sess.
Asc("repo_group.sort_order").
Find(&groups)
}
func findGroupsByCond(ctx context.Context, opts *FindGroupsOptions, cond builder.Cond) db.Engine {
if opts.Page <= 0 {
opts.Page = 1
}
sess := db.GetEngine(ctx).Where(cond.And(opts.ToConds()))
if opts.PageSize > 0 {
sess = sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize)
}
return sess.Asc("sort_order")
}
func FindGroupsByCond(ctx context.Context, opts *FindGroupsOptions, cond builder.Cond) (RepoGroupList, error) {
defaultSize := 50
if opts.PageSize > 0 {
defaultSize = opts.PageSize
}
sess := findGroupsByCond(ctx, opts, cond)
groups := make([]*Group, 0, defaultSize)
if err := sess.Find(&groups); err != nil {
return nil, err
}
return groups, nil
}
func CountGroups(ctx context.Context, opts *FindGroupsOptions) (int64, error) {
return db.GetEngine(ctx).Where(opts.ToConds()).Count(new(Group))
}
func UpdateGroupOwnerName(ctx context.Context, oldUser, newUser string) error {
if _, err := db.GetEngine(ctx).Exec("UPDATE `repo_group` SET owner_name=? WHERE owner_name=?", newUser, oldUser); err != nil {
return fmt.Errorf("change group owner name: %w", err)
}
return nil
}
// GetParentGroupChain returns a slice containing a group and its ancestors
func GetParentGroupChain(ctx context.Context, groupID int64) (RepoGroupList, error) {
groupList := make([]*Group, 0, 20)
currentGroupID := groupID
for {
if currentGroupID < 1 {
break
}
if len(groupList) >= 20 {
return nil, ErrGroupTooDeep{currentGroupID}
}
currentGroup, err := GetGroupByID(ctx, currentGroupID)
if err != nil {
return nil, err
}
groupList = append(groupList, currentGroup)
currentGroupID = currentGroup.ParentGroupID
}
slices.Reverse(groupList)
return groupList, nil
}
func GetParentGroupIDChain(ctx context.Context, groupID int64) ([]int64, error) {
var ids []int64
groupList, err := GetParentGroupChain(ctx, groupID)
if err != nil {
return nil, err
}
ids = util.SliceMap(groupList, func(g *Group) int64 {
return g.ID
})
return ids, err
}
// ParentGroupCond returns a condition matching a group and its ancestors
func ParentGroupCond(ctx context.Context, idStr string, groupID int64) builder.Cond {
groupList, err := GetParentGroupIDChain(ctx, groupID)
if err != nil {
log.Info("Error building group cond: %w", err)
return builder.NotIn(idStr)
}
return builder.In(idStr, groupList)
}
// ChildGroupCond returns a condition recursively matching a group and its descendants
func ChildGroupCond(firstParent int64) builder.Cond {
if firstParent < 0 {
firstParent = 0
}
return builder.Expr(`with recursive groups as (
select * from repo_group
WHERE parent_group_id = ?
union all
select subgroup.*
from repo_group subgroup
join groups g on g.id = subgroup.parent_group_id
) select g.id from groups g`, firstParent)
}
func UpdateGroup(ctx context.Context, group *Group) error {
sess := db.GetEngine(ctx)
_, err := sess.Table(group.TableName()).ID(group.ID).Update(group)
return err
}
func MoveGroup(ctx context.Context, group *Group, newParent int64, newSortOrder int) error {
sess := db.GetEngine(ctx)
ng, err := GetGroupByID(ctx, newParent)
if err != nil && !IsErrGroupNotExist(err) {
return err
}
if ng != nil {
if ng.OwnerID != group.OwnerID {
return fmt.Errorf("group[%d]'s ownerID is not equal to new parent group[%d]'s owner ID", group.ID, ng.ID)
}
}
group.ParentGroupID = newParent
group.SortOrder = newSortOrder
if _, err = sess.Table(group.TableName()).
ID(group.ID).
AllCols().
Update(group); err != nil {
return err
}
if group.ParentGroup != nil && newParent != 0 {
group.ParentGroup = nil
if err = group.LoadParentGroup(ctx); err != nil {
return err
}
}
return nil
}