mirror of
https://github.com/go-gitea/gitea.git
synced 2026-04-04 14:36:16 +02:00
changes
* move error-related code for groups to its own file * update group avatar logic remove unused/duplicate logic * update `FindGroupsOptions.ToConds()` allow passing `-1` as the `ParentGroupID`, meaning "find matching groups regardless of the parent group id" * add `DedupeBy` function to container module this removes duplicate items from a slice using a custom function * add `SliceMap` util works like javascripts's `Array.prototoype.map`, taking in a slice and transforming each element with the provided function * add group service functions included so far: - avatar uploading/deletion - group deletion - group creation - group moving (including moving item inside a group) - group update - team management - add team - remove team - update team permissions - recalculating team access (in event of group move) - group searching (only used in frontend/web components for now)
This commit is contained in:
parent
1246721523
commit
96feb682fe
@ -1,66 +1,22 @@
|
||||
package group
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models/avatars"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/avatar"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"context"
|
||||
"fmt"
|
||||
"image/png"
|
||||
"io"
|
||||
"net/url"
|
||||
|
||||
"code.gitea.io/gitea/models/avatars"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
func (g *Group) CustomAvatarRelativePath() string {
|
||||
return g.Avatar
|
||||
}
|
||||
func generateRandomAvatar(ctx context.Context, group *Group) error {
|
||||
idToString := fmt.Sprintf("%d", group.ID)
|
||||
|
||||
seed := idToString
|
||||
img, err := avatar.RandomImage([]byte(seed))
|
||||
if err != nil {
|
||||
return fmt.Errorf("RandomImage: %w", err)
|
||||
}
|
||||
|
||||
group.Avatar = idToString
|
||||
|
||||
if err = storage.SaveFrom(storage.RepoAvatars, group.CustomAvatarRelativePath(), func(w io.Writer) error {
|
||||
if err = png.Encode(w, img); err != nil {
|
||||
log.Error("Encode: %v", err)
|
||||
}
|
||||
return err
|
||||
}); err != nil {
|
||||
return fmt.Errorf("Failed to create dir %s: %w", group.CustomAvatarRelativePath(), err)
|
||||
}
|
||||
|
||||
log.Info("New random avatar created for repository: %d", group.ID)
|
||||
|
||||
if _, err = db.GetEngine(ctx).ID(group.ID).Cols("avatar").NoAutoTime().Update(group); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
func (g *Group) relAvatarLink(ctx context.Context) string {
|
||||
// If no avatar - path is empty
|
||||
avatarPath := g.CustomAvatarRelativePath()
|
||||
if len(avatarPath) == 0 {
|
||||
switch mode := setting.RepoAvatar.Fallback; mode {
|
||||
case "image":
|
||||
return setting.RepoAvatar.FallbackImage
|
||||
case "random":
|
||||
if err := generateRandomAvatar(ctx, g); err != nil {
|
||||
log.Error("generateRandomAvatar: %v", err)
|
||||
}
|
||||
default:
|
||||
// default behaviour: do not display avatar
|
||||
return ""
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return setting.AppSubURL + "/group-avatars/" + url.PathEscape(g.Avatar)
|
||||
}
|
||||
|
||||
41
models/group/errors.go
Normal file
41
models/group/errors.go
Normal file
@ -0,0 +1,41 @@
|
||||
package group
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
type ErrGroupNotExist struct {
|
||||
ID int64
|
||||
}
|
||||
|
||||
// IsErrGroupNotExist checks if an error is a ErrCommentNotExist.
|
||||
func IsErrGroupNotExist(err error) bool {
|
||||
var errGroupNotExist ErrGroupNotExist
|
||||
ok := errors.As(err, &errGroupNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrGroupNotExist) Error() string {
|
||||
return fmt.Sprintf("group does not exist [id: %d]", err.ID)
|
||||
}
|
||||
|
||||
func (err ErrGroupNotExist) Unwrap() error {
|
||||
return util.ErrNotExist
|
||||
}
|
||||
|
||||
type ErrGroupTooDeep struct {
|
||||
ID int64
|
||||
}
|
||||
|
||||
func IsErrGroupTooDeep(err error) bool {
|
||||
var errGroupTooDeep ErrGroupTooDeep
|
||||
ok := errors.As(err, &errGroupTooDeep)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrGroupTooDeep) Error() string {
|
||||
return fmt.Sprintf("group has reached or exceeded the subgroup nesting limit [id: %d]", err.ID)
|
||||
}
|
||||
@ -136,10 +136,21 @@ func (opts FindGroupsOptions) ToConds() builder.Cond {
|
||||
if opts.OwnerID != 0 {
|
||||
cond = cond.And(builder.Eq{"owner_id": opts.OwnerID})
|
||||
}
|
||||
if opts.ParentGroupID != 0 {
|
||||
if opts.ParentGroupID > 0 {
|
||||
cond = cond.And(builder.Eq{"parent_group_id": opts.ParentGroupID})
|
||||
} else {
|
||||
cond = cond.And(builder.IsNull{"parent_group_id"})
|
||||
} 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("group_team.group_id").
|
||||
From("group_team").
|
||||
Where(builder.Eq{"team_user.uid": opts.ActorID}).
|
||||
Join("INNER", "team_user", "team_user.team_id = group_team.team_id").
|
||||
And(builder.Eq{"group_team.can_create_in": true})))
|
||||
}
|
||||
if opts.Name != "" {
|
||||
cond = cond.And(builder.Eq{"lower_name": opts.Name})
|
||||
}
|
||||
return cond
|
||||
}
|
||||
@ -186,53 +197,55 @@ func GetParentGroupChain(ctx context.Context, groupID int64) (GroupList, error)
|
||||
groupList = append(groupList, currentGroup)
|
||||
currentGroupID = currentGroup.ParentGroupID
|
||||
}
|
||||
slices.Reverse(groupList)
|
||||
return groupList, nil
|
||||
}
|
||||
|
||||
func GetParentGroupIDChain(ctx context.Context, groupID int64) (ids []int64, err error) {
|
||||
groupList, err := GetParentGroupChain(ctx, groupID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids = util.SliceMap(groupList, func(g *Group) int64 {
|
||||
return g.ID
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// ParentGroupCond returns a condition matching a group and its ancestors
|
||||
func ParentGroupCond(idStr string, groupID int64) builder.Cond {
|
||||
groupList, err := GetParentGroupChain(db.DefaultContext, groupID)
|
||||
groupList, err := GetParentGroupIDChain(db.DefaultContext, groupID)
|
||||
if err != nil {
|
||||
log.Info("Error building group cond: %w", err)
|
||||
return builder.NotIn(idStr)
|
||||
}
|
||||
return builder.In(
|
||||
idStr,
|
||||
util.SliceMap[*Group, int64](groupList, func(it *Group) int64 {
|
||||
return it.ID
|
||||
}),
|
||||
)
|
||||
return builder.In(idStr, groupList)
|
||||
}
|
||||
|
||||
type ErrGroupNotExist struct {
|
||||
ID int64
|
||||
}
|
||||
|
||||
// IsErrGroupNotExist checks if an error is a ErrCommentNotExist.
|
||||
func IsErrGroupNotExist(err error) bool {
|
||||
var errGroupNotExist ErrGroupNotExist
|
||||
ok := errors.As(err, &errGroupNotExist)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrGroupNotExist) Error() string {
|
||||
return fmt.Sprintf("group does not exist [id: %d]", err.ID)
|
||||
}
|
||||
|
||||
func (err ErrGroupNotExist) Unwrap() error {
|
||||
return util.ErrNotExist
|
||||
}
|
||||
|
||||
type ErrGroupTooDeep struct {
|
||||
ID int64
|
||||
}
|
||||
|
||||
func IsErrGroupTooDeep(err error) bool {
|
||||
var errGroupTooDeep ErrGroupTooDeep
|
||||
ok := errors.As(err, &errGroupTooDeep)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrGroupTooDeep) Error() string {
|
||||
return fmt.Sprintf("group has reached or exceeded the subgroup nesting limit [id: %d]", err.ID)
|
||||
func MoveGroup(ctx context.Context, group *Group, newParent int64, newSortOrder int) error {
|
||||
sess := db.GetEngine(ctx)
|
||||
ng, err := GetGroupByID(ctx, newParent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ng.OwnerID != group.OwnerID {
|
||||
return fmt.Errorf("group[%d]'s ownerID is not equal to new paretn group[%d]'s owner ID", group.ID, ng.ID)
|
||||
}
|
||||
group.ParentGroupID = newParent
|
||||
group.SortOrder = newSortOrder
|
||||
if _, err = sess.Table(group.TableName()).
|
||||
Where("id = ?", group.ID).
|
||||
MustCols("parent_group_id").
|
||||
Update(group, &Group{
|
||||
ID: group.ID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if group.ParentGroup != nil && newParent != 0 {
|
||||
group.ParentGroup = nil
|
||||
if err = group.LoadParentGroup(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -19,3 +19,16 @@ func FilterSlice[E any, T comparable](s []E, include func(E) (T, bool)) []T {
|
||||
}
|
||||
return slices.Clip(filtered)
|
||||
}
|
||||
|
||||
func DedupeBy[E any, I comparable](s []E, id func(E) I) []E {
|
||||
filtered := make([]E, 0, len(s)) // slice will be clipped before returning
|
||||
seen := make(map[I]bool, len(s))
|
||||
for i := range s {
|
||||
itemId := id(s[i])
|
||||
if _, ok := seen[itemId]; !ok {
|
||||
filtered = append(filtered, s[i])
|
||||
seen[itemId] = true
|
||||
}
|
||||
}
|
||||
return slices.Clip(filtered)
|
||||
}
|
||||
|
||||
@ -77,3 +77,11 @@ func SliceNilAsEmpty[T any](a []T) []T {
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func SliceMap[T any, R any](slice []T, mapper func(it T) R) []R {
|
||||
ret := make([]R, 0)
|
||||
for _, it := range slice {
|
||||
ret = append(ret, mapper(it))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
67
services/group/avatar.go
Normal file
67
services/group/avatar.go
Normal file
@ -0,0 +1,67 @@
|
||||
package group
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
group_model "code.gitea.io/gitea/models/group"
|
||||
"code.gitea.io/gitea/modules/avatar"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// UploadAvatar saves custom icon for group.
|
||||
func UploadAvatar(ctx context.Context, g *group_model.Group, data []byte) error {
|
||||
avatarData, err := avatar.ProcessAvatarImage(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
g.Avatar = avatar.HashAvatar(g.ID, data)
|
||||
if err = UpdateGroup(ctx, g, &UpdateOptions{}); err != nil {
|
||||
return fmt.Errorf("updateGroup: %w", err)
|
||||
}
|
||||
|
||||
if err = storage.SaveFrom(storage.Avatars, g.CustomAvatarRelativePath(), func(w io.Writer) error {
|
||||
_, err = w.Write(avatarData)
|
||||
return err
|
||||
}); err != nil {
|
||||
return fmt.Errorf("Failed to create dir %s: %w", g.CustomAvatarRelativePath(), err)
|
||||
}
|
||||
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
// DeleteAvatar deletes the user's custom avatar.
|
||||
func DeleteAvatar(ctx context.Context, g *group_model.Group) error {
|
||||
aPath := g.CustomAvatarRelativePath()
|
||||
log.Trace("DeleteAvatar[%d]: %s", g.ID, aPath)
|
||||
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
hasAvatar := len(g.Avatar) > 0
|
||||
g.Avatar = ""
|
||||
if _, err := db.GetEngine(ctx).ID(g.ID).Cols("avatar, use_custom_avatar").Update(g); err != nil {
|
||||
return fmt.Errorf("DeleteAvatar: %w", err)
|
||||
}
|
||||
|
||||
if hasAvatar {
|
||||
if err := storage.Avatars.Delete(aPath); err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("failed to remove %s: %w", aPath, err)
|
||||
}
|
||||
log.Warn("Deleting avatar %s but it doesn't exist", aPath)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
84
services/group/delete.go
Normal file
84
services/group/delete.go
Normal file
@ -0,0 +1,84 @@
|
||||
package group
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
group_model "code.gitea.io/gitea/models/group"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
)
|
||||
|
||||
func DeleteGroup(ctx context.Context, gid int64) error {
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
sess := db.GetEngine(ctx)
|
||||
|
||||
toDelete, err := group_model.GetGroupByID(ctx, gid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove team permissions and units for deleted group
|
||||
if _, err = sess.Where("group_id = ?", gid).Delete(new(group_model.GroupTeam)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = sess.Where("group_id = ?", gid).Delete(new(group_model.GroupUnit)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// move all repos in the deleted group to its immediate parent
|
||||
repos, cnt, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
|
||||
GroupID: gid,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, inParent, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
|
||||
GroupID: toDelete.ParentGroupID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cnt > 0 {
|
||||
for i, repo := range repos {
|
||||
repo.GroupID = toDelete.ParentGroupID
|
||||
repo.GroupSortOrder = int(inParent + int64(i) + 1)
|
||||
}
|
||||
if _, err = sess.Where("group_id = ?", gid).Update(&repos); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// move all child groups to the deleted group's immediate parent
|
||||
childGroups, err := group_model.FindGroups(ctx, &group_model.FindGroupsOptions{
|
||||
ParentGroupID: gid,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(childGroups) > 0 {
|
||||
inParent, err = group_model.CountGroups(ctx, &group_model.FindGroupsOptions{
|
||||
ParentGroupID: toDelete.ParentGroupID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i, group := range childGroups {
|
||||
group.ParentGroupID = toDelete.ParentGroupID
|
||||
group.SortOrder = int(inParent) + i + 1
|
||||
}
|
||||
if _, err = sess.Where("parent_group_id = ?", gid).Update(&childGroups); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// finally, delete the group itself
|
||||
if _, err = sess.ID(gid).Delete(new(group_model.Group)); err != nil {
|
||||
return err
|
||||
}
|
||||
return committer.Commit()
|
||||
}
|
||||
90
services/group/group.go
Normal file
90
services/group/group.go
Normal file
@ -0,0 +1,90 @@
|
||||
package group
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
group_model "code.gitea.io/gitea/models/group"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
func NewGroup(ctx context.Context, g *group_model.Group) (err error) {
|
||||
if len(g.Name) == 0 {
|
||||
return util.NewInvalidArgumentErrorf("empty group name")
|
||||
}
|
||||
has, err := db.ExistByID[user_model.User](ctx, g.OwnerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !has {
|
||||
return organization.ErrOrgNotExist{ID: g.OwnerID}
|
||||
}
|
||||
g.LowerName = strings.ToLower(g.Name)
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
if err = db.Insert(ctx, g); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = RecalculateGroupAccess(ctx, g, true); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
func MoveRepositoryToGroup(ctx context.Context, repo *repo_model.Repository, newGroupID int64, groupSortOrder int) error {
|
||||
sess := db.GetEngine(ctx)
|
||||
repo.GroupID = newGroupID
|
||||
repo.GroupSortOrder = groupSortOrder
|
||||
cnt, err := sess.
|
||||
Table("repository").
|
||||
ID(repo.ID).
|
||||
MustCols("group_id").
|
||||
Update(repo)
|
||||
log.Info("updated %d rows?", cnt)
|
||||
return err
|
||||
}
|
||||
|
||||
func MoveGroupItem(ctx context.Context, itemID, newParent int64, isGroup bool, newPos int) (err error) {
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
if isGroup {
|
||||
group, err := group_model.GetGroupByID(ctx, itemID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if group.ParentGroupID != newParent || group.SortOrder != newPos {
|
||||
if err = group_model.MoveGroup(ctx, group, newParent, newPos); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = RecalculateGroupAccess(ctx, group, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
repo, err := repo_model.GetRepositoryByID(ctx, itemID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if repo.GroupID != newParent || repo.GroupSortOrder != newPos {
|
||||
if err = MoveRepositoryToGroup(ctx, repo, newParent, newPos); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return committer.Commit()
|
||||
}
|
||||
199
services/group/search.go
Normal file
199
services/group/search.go
Normal file
@ -0,0 +1,199 @@
|
||||
package group
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"code.gitea.io/gitea/models/git"
|
||||
group_model "code.gitea.io/gitea/models/group"
|
||||
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/log"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/translation"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
commitstatus_service "code.gitea.io/gitea/services/repository/commitstatus"
|
||||
)
|
||||
|
||||
type WebSearchGroup struct {
|
||||
Group *structs.Group `json:"group,omitempty"`
|
||||
LatestCommitStatus *git.CommitStatus `json:"latest_commit_status"`
|
||||
LocaleLatestCommitStatus string `json:"locale_latest_commit_status"`
|
||||
Subgroups []*WebSearchGroup `json:"subgroups"`
|
||||
Repos []*repo_service.WebSearchRepository `json:"repos"`
|
||||
}
|
||||
|
||||
type GroupWebSearchResult struct {
|
||||
OK bool `json:"ok"`
|
||||
Data *WebSearchGroup `json:"data"`
|
||||
}
|
||||
|
||||
type GroupWebSearchOptions struct {
|
||||
Ctx context.Context
|
||||
Locale translation.Locale
|
||||
Recurse bool
|
||||
Actor *user_model.User
|
||||
RepoOpts *repo_model.SearchRepoOptions
|
||||
GroupOpts *group_model.FindGroupsOptions
|
||||
OrgID int64
|
||||
}
|
||||
|
||||
// results for root-level queries //
|
||||
|
||||
type WebSearchGroupRoot struct {
|
||||
Groups []*WebSearchGroup
|
||||
Repos []*repo_service.WebSearchRepository
|
||||
}
|
||||
|
||||
type GroupWebSearchRootResult struct {
|
||||
OK bool `json:"ok"`
|
||||
Data *WebSearchGroupRoot `json:"data"`
|
||||
}
|
||||
|
||||
func ToWebSearchRepo(ctx context.Context, repo *repo_model.Repository) *repo_service.WebSearchRepository {
|
||||
return &repo_service.WebSearchRepository{
|
||||
Repository: &structs.Repository{
|
||||
ID: repo.ID,
|
||||
FullName: repo.FullName(),
|
||||
Fork: repo.IsFork,
|
||||
Private: repo.IsPrivate,
|
||||
Template: repo.IsTemplate,
|
||||
Mirror: repo.IsMirror,
|
||||
Stars: repo.NumStars,
|
||||
HTMLURL: repo.HTMLURL(ctx),
|
||||
Link: repo.Link(),
|
||||
Internal: !repo.IsPrivate && repo.Owner.Visibility == structs.VisibleTypePrivate,
|
||||
GroupSortOrder: repo.GroupSortOrder,
|
||||
GroupID: repo.GroupID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (w *WebSearchGroup) doLoadChildren(opts *GroupWebSearchOptions) error {
|
||||
opts.RepoOpts.OwnerID = opts.OrgID
|
||||
opts.RepoOpts.GroupID = 0
|
||||
opts.GroupOpts.OwnerID = opts.OrgID
|
||||
opts.GroupOpts.ParentGroupID = 0
|
||||
|
||||
if w.Group != nil {
|
||||
opts.RepoOpts.GroupID = w.Group.ID
|
||||
opts.RepoOpts.ListAll = true
|
||||
opts.GroupOpts.ParentGroupID = w.Group.ID
|
||||
opts.GroupOpts.ListAll = true
|
||||
}
|
||||
repos, _, err := repo_model.SearchRepository(opts.Ctx, opts.RepoOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
slices.SortStableFunc(repos, func(a, b *repo_model.Repository) int {
|
||||
return a.GroupSortOrder - b.GroupSortOrder
|
||||
})
|
||||
latestCommitStatuses, err := commitstatus_service.FindReposLastestCommitStatuses(opts.Ctx, repos)
|
||||
if err != nil {
|
||||
log.Error("FindReposLastestCommitStatuses: %v", err)
|
||||
return err
|
||||
}
|
||||
latestIdx := -1
|
||||
for i, r := range repos {
|
||||
wsr := ToWebSearchRepo(opts.Ctx, r)
|
||||
if latestCommitStatuses[i] != nil {
|
||||
wsr.LatestCommitStatus = latestCommitStatuses[i]
|
||||
wsr.LocaleLatestCommitStatus = latestCommitStatuses[i].LocaleString(opts.Locale)
|
||||
if latestIdx > -1 {
|
||||
if latestCommitStatuses[i].UpdatedUnix.AsLocalTime().Unix() > int64(latestCommitStatuses[latestIdx].UpdatedUnix.AsLocalTime().Unix()) {
|
||||
latestIdx = i
|
||||
}
|
||||
} else {
|
||||
latestIdx = i
|
||||
}
|
||||
}
|
||||
w.Repos = append(w.Repos, wsr)
|
||||
}
|
||||
if w.Group != nil && latestIdx > -1 {
|
||||
w.LatestCommitStatus = latestCommitStatuses[latestIdx]
|
||||
}
|
||||
w.Subgroups = make([]*WebSearchGroup, 0)
|
||||
groups, err := group_model.FindGroupsByCond(opts.Ctx, opts.GroupOpts, group_model.AccessibleGroupCondition(opts.Actor, unit.TypeInvalid))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, g := range groups {
|
||||
toAppend, err := ToWebSearchGroup(g, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.Subgroups = append(w.Subgroups, toAppend)
|
||||
}
|
||||
|
||||
if opts.Recurse {
|
||||
for _, sg := range w.Subgroups {
|
||||
err = sg.doLoadChildren(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ToWebSearchGroup(group *group_model.Group, opts *GroupWebSearchOptions) (*WebSearchGroup, error) {
|
||||
res := new(WebSearchGroup)
|
||||
|
||||
res.Repos = make([]*repo_service.WebSearchRepository, 0)
|
||||
res.Subgroups = make([]*WebSearchGroup, 0)
|
||||
var err error
|
||||
if group != nil {
|
||||
if res.Group, err = convert.ToAPIGroup(opts.Ctx, group, opts.Actor); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func SearchRepoGroupWeb(group *group_model.Group, opts *GroupWebSearchOptions) (*GroupWebSearchResult, error) {
|
||||
res := new(WebSearchGroup)
|
||||
var err error
|
||||
res, err = ToWebSearchGroup(group, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = res.doLoadChildren(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &GroupWebSearchResult{
|
||||
Data: res,
|
||||
OK: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
/* func SearchRootItems(ctx context.Context, oid int64, groupSearchOptions *group_model.FindGroupsOptions, repoSearchOptions *repo_model.SearchRepoOptions, actor *user_model.User, recursive bool) (*WebSearchGroupRoot, error) {
|
||||
root := &WebSearchGroupRoot{
|
||||
Repos: make([]*repo_service.WebSearchRepository, 0),
|
||||
Groups: make([]*WebSearchGroup, 0),
|
||||
}
|
||||
groupSearchOptions.ParentGroupID = 0
|
||||
groups, err := group_model.FindGroupsByCond(ctx, groupSearchOptions, group_model.AccessibleGroupCondition(actor, unit.TypeInvalid))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, g := range groups {
|
||||
toAppend, err := ToWebSearchGroup(ctx, g, actor, oid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
root.Groups = append(root.Groups, toAppend)
|
||||
}
|
||||
repos, _, err := repo_model.SearchRepositoryByCondition(ctx, repoSearchOptions, repo_model.AccessibleRepositoryCondition(actor, unit.TypeInvalid), true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range repos {
|
||||
root.Repos = append(root.Repos, ToWebSearchRepo(ctx, r))
|
||||
}
|
||||
|
||||
return root, nil
|
||||
}
|
||||
*/
|
||||
147
services/group/team.go
Normal file
147
services/group/team.go
Normal file
@ -0,0 +1,147 @@
|
||||
package group
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
group_model "code.gitea.io/gitea/models/group"
|
||||
org_model "code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
func AddTeamToGroup(ctx context.Context, group *group_model.Group, tname string) error {
|
||||
t, err := org_model.GetTeam(ctx, group.OwnerID, tname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
has := group_model.HasTeamGroup(ctx, group.OwnerID, t.ID, group.ID)
|
||||
if has {
|
||||
return fmt.Errorf("team '%s' already exists in group[%d]", tname, group.ID)
|
||||
} else {
|
||||
parentGroup, err := group_model.FindGroupTeamByTeamID(ctx, group.ID, t.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mode := t.AccessMode
|
||||
canCreateIn := t.CanCreateOrgRepo
|
||||
if parentGroup != nil {
|
||||
mode = max(t.AccessMode, parentGroup.AccessMode)
|
||||
canCreateIn = parentGroup.CanCreateIn || t.CanCreateOrgRepo
|
||||
}
|
||||
if err = group.LoadParentGroup(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
err = group_model.AddTeamGroup(ctx, group.ID, t.ID, group.ID, mode, canCreateIn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteTeamFromGroup(ctx context.Context, group *group_model.Group, org int64, teamName string) error {
|
||||
team, err := org_model.GetTeam(ctx, org, teamName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = group_model.RemoveTeamGroup(ctx, org, team.ID, group.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func UpdateGroupTeam(ctx context.Context, gt *group_model.GroupTeam) (err error) {
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
sess := db.GetEngine(ctx)
|
||||
|
||||
if _, err = sess.ID(gt.ID).AllCols().Update(gt); err != nil {
|
||||
return fmt.Errorf("update: %w", err)
|
||||
}
|
||||
for _, unit := range gt.Units {
|
||||
unit.TeamID = gt.TeamID
|
||||
if _, err = sess.
|
||||
Where("team_id=?", gt.TeamID).
|
||||
And("group_id=?", gt.GroupID).
|
||||
And("type = ?", unit.Type).
|
||||
Update(unit); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return committer.Commit()
|
||||
}
|
||||
|
||||
// RecalculateGroupAccess recalculates team access to a group.
|
||||
// should only be called if and only if a group was moved from another group.
|
||||
func RecalculateGroupAccess(ctx context.Context, g *group_model.Group, isNew bool) (err error) {
|
||||
sess := db.GetEngine(ctx)
|
||||
if err = g.LoadParentGroup(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
var teams []*org_model.Team
|
||||
if g.ParentGroup == nil {
|
||||
teams, err = org_model.FindOrgTeams(ctx, g.OwnerID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
teams, err = org_model.GetTeamsWithAccessToGroup(ctx, g.OwnerID, g.ParentGroupID, perm.AccessModeRead)
|
||||
}
|
||||
for _, t := range teams {
|
||||
|
||||
var gt *group_model.GroupTeam = nil
|
||||
if gt, err = group_model.FindGroupTeamByTeamID(ctx, g.ParentGroupID, t.ID); err != nil {
|
||||
return
|
||||
}
|
||||
if gt != nil {
|
||||
if err = group_model.UpdateTeamGroup(ctx, g.OwnerID, t.ID, g.ID, gt.AccessMode, gt.CanCreateIn, isNew); err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err = group_model.UpdateTeamGroup(ctx, g.OwnerID, t.ID, g.ID, t.AccessMode, t.IsOwnerTeam() || t.AccessMode >= perm.AccessModeAdmin || t.CanCreateOrgRepo, isNew); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = t.LoadUnits(ctx); err != nil {
|
||||
return
|
||||
}
|
||||
for _, u := range t.Units {
|
||||
|
||||
newAccessMode := u.AccessMode
|
||||
if g.ParentGroup == nil {
|
||||
gu, err := group_model.GetGroupUnit(ctx, g.ID, t.ID, u.Type)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newAccessMode = min(newAccessMode, gu.AccessMode)
|
||||
}
|
||||
if isNew {
|
||||
if _, err = sess.Table("group_unit").Insert(&group_model.GroupUnit{
|
||||
Type: u.Type,
|
||||
TeamID: t.ID,
|
||||
GroupID: g.ID,
|
||||
AccessMode: newAccessMode,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if _, err = sess.Table("group_unit").Where(builder.Eq{
|
||||
"type": u.Type,
|
||||
"team_id": t.ID,
|
||||
"group_id": g.ID,
|
||||
}).Update(&group_model.GroupUnit{
|
||||
AccessMode: newAccessMode,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
31
services/group/update.go
Normal file
31
services/group/update.go
Normal file
@ -0,0 +1,31 @@
|
||||
package group
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
group_model "code.gitea.io/gitea/models/group"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"context"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type UpdateOptions struct {
|
||||
Name optional.Option[string]
|
||||
Description optional.Option[string]
|
||||
Visibility optional.Option[structs.VisibleType]
|
||||
}
|
||||
|
||||
func UpdateGroup(ctx context.Context, g *group_model.Group, opts *UpdateOptions) error {
|
||||
if opts.Name.Has() {
|
||||
g.Name = opts.Name.Value()
|
||||
g.LowerName = strings.ToLower(g.Name)
|
||||
}
|
||||
if opts.Description.Has() {
|
||||
g.Description = opts.Description.Value()
|
||||
}
|
||||
if opts.Visibility.Has() {
|
||||
g.Visibility = opts.Visibility.Value()
|
||||
}
|
||||
_, err := db.GetEngine(ctx).ID(g.ID).Update(g)
|
||||
return err
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user