diff --git a/models/group/avatar.go b/models/group/avatar.go index 3b6bf66bf7..d07e8341da 100644 --- a/models/group/avatar.go +++ b/models/group/avatar.go @@ -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) } diff --git a/models/group/errors.go b/models/group/errors.go new file mode 100644 index 0000000000..a578c92933 --- /dev/null +++ b/models/group/errors.go @@ -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) +} diff --git a/models/group/group.go b/models/group/group.go index 323f963348..c8525af0f5 100644 --- a/models/group/group.go +++ b/models/group/group.go @@ -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 } diff --git a/modules/container/filter.go b/modules/container/filter.go index 37ec7c3d56..9f1237e626 100644 --- a/modules/container/filter.go +++ b/modules/container/filter.go @@ -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) +} diff --git a/modules/util/slice.go b/modules/util/slice.go index aaa729c1c9..97857e0f47 100644 --- a/modules/util/slice.go +++ b/modules/util/slice.go @@ -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 +} diff --git a/services/group/avatar.go b/services/group/avatar.go new file mode 100644 index 0000000000..f38096c6c6 --- /dev/null +++ b/services/group/avatar.go @@ -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 + }) +} diff --git a/services/group/delete.go b/services/group/delete.go new file mode 100644 index 0000000000..0dc19c2560 --- /dev/null +++ b/services/group/delete.go @@ -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() +} diff --git a/services/group/group.go b/services/group/group.go new file mode 100644 index 0000000000..fb2414bb00 --- /dev/null +++ b/services/group/group.go @@ -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() +} diff --git a/services/group/search.go b/services/group/search.go new file mode 100644 index 0000000000..afe30576be --- /dev/null +++ b/services/group/search.go @@ -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 +} +*/ diff --git a/services/group/team.go b/services/group/team.go new file mode 100644 index 0000000000..3cf690e25e --- /dev/null +++ b/services/group/team.go @@ -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 +} diff --git a/services/group/update.go b/services/group/update.go new file mode 100644 index 0000000000..63e131243f --- /dev/null +++ b/services/group/update.go @@ -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 +}