aboutsummaryrefslogtreecommitdiffstats
path: root/services
diff options
context:
space:
mode:
authorLunny Xiao <xiaolunwen@gmail.com>2024-11-27 13:12:26 -0800
committerGitHub <noreply@github.com>2024-11-27 21:12:26 +0000
commitf62f68cbdda07ae6933b4804bc757f35dc92d134 (patch)
tree6709319ec99caa6b22ed7d409f77ad17323f7808 /services
parent5a50b271e71b5e8bb7bdef149e07dcd2b0db3e3a (diff)
downloadgitea-f62f68cbdda07ae6933b4804bc757f35dc92d134.tar.gz
gitea-f62f68cbdda07ae6933b4804bc757f35dc92d134.zip
Move team related functions to service layer (#32537)
There are still some functions under `models` after last big refactor about `models`. This change will move all team related functions to service layer with no code change.
Diffstat (limited to 'services')
-rw-r--r--services/auth/source/source_group_sync.go6
-rw-r--r--services/doctor/fix8312.go4
-rw-r--r--services/org/repo.go27
-rw-r--r--services/org/team.go401
-rw-r--r--services/org/team_test.go314
-rw-r--r--services/org/user.go102
-rw-r--r--services/org/user_test.go74
-rw-r--r--services/repository/collaboration.go32
-rw-r--r--services/repository/create.go3
-rw-r--r--services/repository/create_test.go148
-rw-r--r--services/repository/delete.go81
-rw-r--r--services/repository/repo_team.go226
-rw-r--r--services/repository/repo_team_test.go (renamed from services/org/repo_test.go)2
-rw-r--r--services/repository/transfer.go4
-rw-r--r--services/user/user.go2
-rw-r--r--services/user/user_test.go3
16 files changed, 1158 insertions, 271 deletions
diff --git a/services/auth/source/source_group_sync.go b/services/auth/source/source_group_sync.go
index 05293f202f..9cb7d4165c 100644
--- a/services/auth/source/source_group_sync.go
+++ b/services/auth/source/source_group_sync.go
@@ -7,11 +7,11 @@ import (
"context"
"fmt"
- "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/organization"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/log"
+ org_service "code.gitea.io/gitea/services/org"
)
type syncType int
@@ -100,12 +100,12 @@ func syncGroupsToTeamsCached(ctx context.Context, user *user_model.User, orgTeam
}
if action == syncAdd && !isMember {
- if err := models.AddTeamMember(ctx, team, user); err != nil {
+ if err := org_service.AddTeamMember(ctx, team, user); err != nil {
log.Error("group sync: Could not add user to team: %v", err)
return err
}
} else if action == syncRemove && isMember {
- if err := models.RemoveTeamMember(ctx, team, user); err != nil {
+ if err := org_service.RemoveTeamMember(ctx, team, user); err != nil {
log.Error("group sync: Could not remove user from team: %v", err)
return err
}
diff --git a/services/doctor/fix8312.go b/services/doctor/fix8312.go
index 4fc049873a..3e2ca68eb4 100644
--- a/services/doctor/fix8312.go
+++ b/services/doctor/fix8312.go
@@ -6,11 +6,11 @@ package doctor
import (
"context"
- "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
org_model "code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
"code.gitea.io/gitea/modules/log"
+ org_service "code.gitea.io/gitea/services/org"
"xorm.io/builder"
)
@@ -29,7 +29,7 @@ func fixOwnerTeamCreateOrgRepo(ctx context.Context, logger log.Logger, autofix b
return nil
}
- return models.UpdateTeam(ctx, team, false, false)
+ return org_service.UpdateTeam(ctx, team, false, false)
},
)
if err != nil {
diff --git a/services/org/repo.go b/services/org/repo.go
deleted file mode 100644
index 78a829ef25..0000000000
--- a/services/org/repo.go
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright 2022 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package org
-
-import (
- "context"
- "errors"
-
- "code.gitea.io/gitea/models"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/organization"
- repo_model "code.gitea.io/gitea/models/repo"
-)
-
-// TeamAddRepository adds new repository to team of organization.
-func TeamAddRepository(ctx context.Context, t *organization.Team, repo *repo_model.Repository) (err error) {
- if repo.OwnerID != t.OrgID {
- return errors.New("repository does not belong to organization")
- } else if organization.HasTeamRepo(ctx, t.OrgID, t.ID, repo.ID) {
- return nil
- }
-
- return db.WithTx(ctx, func(ctx context.Context) error {
- return models.AddRepository(ctx, t, repo)
- })
-}
diff --git a/services/org/team.go b/services/org/team.go
new file mode 100644
index 0000000000..3688e68433
--- /dev/null
+++ b/services/org/team.go
@@ -0,0 +1,401 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package org
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "code.gitea.io/gitea/models/db"
+ git_model "code.gitea.io/gitea/models/git"
+ issues_model "code.gitea.io/gitea/models/issues"
+ "code.gitea.io/gitea/models/organization"
+ access_model "code.gitea.io/gitea/models/perm/access"
+ 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/setting"
+ "code.gitea.io/gitea/modules/util"
+ repo_service "code.gitea.io/gitea/services/repository"
+
+ "xorm.io/builder"
+)
+
+// NewTeam creates a record of new team.
+// It's caller's responsibility to assign organization ID.
+func NewTeam(ctx context.Context, t *organization.Team) (err error) {
+ if len(t.Name) == 0 {
+ return util.NewInvalidArgumentErrorf("empty team name")
+ }
+
+ if err = organization.IsUsableTeamName(t.Name); err != nil {
+ return err
+ }
+
+ has, err := db.ExistByID[user_model.User](ctx, t.OrgID)
+ if err != nil {
+ return err
+ }
+ if !has {
+ return organization.ErrOrgNotExist{ID: t.OrgID}
+ }
+
+ t.LowerName = strings.ToLower(t.Name)
+ has, err = db.Exist[organization.Team](ctx, builder.Eq{
+ "org_id": t.OrgID,
+ "lower_name": t.LowerName,
+ })
+ if err != nil {
+ return err
+ }
+ if has {
+ return organization.ErrTeamAlreadyExist{OrgID: t.OrgID, Name: t.LowerName}
+ }
+
+ ctx, committer, err := db.TxContext(ctx)
+ if err != nil {
+ return err
+ }
+ defer committer.Close()
+
+ if err = db.Insert(ctx, t); err != nil {
+ return err
+ }
+
+ // insert units for team
+ if len(t.Units) > 0 {
+ for _, unit := range t.Units {
+ unit.TeamID = t.ID
+ }
+ if err = db.Insert(ctx, &t.Units); err != nil {
+ return err
+ }
+ }
+
+ // Add all repositories to the team if it has access to all of them.
+ if t.IncludesAllRepositories {
+ err = repo_service.AddAllRepositoriesToTeam(ctx, t)
+ if err != nil {
+ return fmt.Errorf("addAllRepositories: %w", err)
+ }
+ }
+
+ // Update organization number of teams.
+ if _, err = db.Exec(ctx, "UPDATE `user` SET num_teams=num_teams+1 WHERE id = ?", t.OrgID); err != nil {
+ return err
+ }
+ return committer.Commit()
+}
+
+// UpdateTeam updates information of team.
+func UpdateTeam(ctx context.Context, t *organization.Team, authChanged, includeAllChanged bool) (err error) {
+ if len(t.Name) == 0 {
+ return util.NewInvalidArgumentErrorf("empty team name")
+ }
+
+ if len(t.Description) > 255 {
+ t.Description = t.Description[:255]
+ }
+
+ ctx, committer, err := db.TxContext(ctx)
+ if err != nil {
+ return err
+ }
+ defer committer.Close()
+
+ t.LowerName = strings.ToLower(t.Name)
+ has, err := db.Exist[organization.Team](ctx, builder.Eq{
+ "org_id": t.OrgID,
+ "lower_name": t.LowerName,
+ }.And(builder.Neq{"id": t.ID}),
+ )
+ if err != nil {
+ return err
+ } else if has {
+ return organization.ErrTeamAlreadyExist{OrgID: t.OrgID, Name: t.LowerName}
+ }
+
+ sess := db.GetEngine(ctx)
+ if _, err = sess.ID(t.ID).Cols("name", "lower_name", "description",
+ "can_create_org_repo", "authorize", "includes_all_repositories").Update(t); err != nil {
+ return fmt.Errorf("update: %w", err)
+ }
+
+ // update units for team
+ if len(t.Units) > 0 {
+ for _, unit := range t.Units {
+ unit.TeamID = t.ID
+ }
+ // Delete team-unit.
+ if _, err := sess.
+ Where("team_id=?", t.ID).
+ Delete(new(organization.TeamUnit)); err != nil {
+ return err
+ }
+ if _, err = sess.Cols("org_id", "team_id", "type", "access_mode").Insert(&t.Units); err != nil {
+ return err
+ }
+ }
+
+ // Update access for team members if needed.
+ if authChanged {
+ if err = t.LoadRepositories(ctx); err != nil {
+ return fmt.Errorf("LoadRepositories: %w", err)
+ }
+
+ for _, repo := range t.Repos {
+ if err = access_model.RecalculateTeamAccesses(ctx, repo, 0); err != nil {
+ return fmt.Errorf("recalculateTeamAccesses: %w", err)
+ }
+ }
+ }
+
+ // Add all repositories to the team if it has access to all of them.
+ if includeAllChanged && t.IncludesAllRepositories {
+ err = repo_service.AddAllRepositoriesToTeam(ctx, t)
+ if err != nil {
+ return fmt.Errorf("addAllRepositories: %w", err)
+ }
+ }
+
+ return committer.Commit()
+}
+
+// DeleteTeam deletes given team.
+// It's caller's responsibility to assign organization ID.
+func DeleteTeam(ctx context.Context, t *organization.Team) error {
+ ctx, committer, err := db.TxContext(ctx)
+ if err != nil {
+ return err
+ }
+ defer committer.Close()
+
+ if err := t.LoadRepositories(ctx); err != nil {
+ return err
+ }
+
+ if err := t.LoadMembers(ctx); err != nil {
+ return err
+ }
+
+ // update branch protections
+ {
+ protections := make([]*git_model.ProtectedBranch, 0, 10)
+ err := db.GetEngine(ctx).In("repo_id",
+ builder.Select("id").From("repository").Where(builder.Eq{"owner_id": t.OrgID})).
+ Find(&protections)
+ if err != nil {
+ return fmt.Errorf("findProtectedBranches: %w", err)
+ }
+ for _, p := range protections {
+ if err := git_model.RemoveTeamIDFromProtectedBranch(ctx, p, t.ID); err != nil {
+ return err
+ }
+ }
+ }
+
+ if err := repo_service.RemoveAllRepositoriesFromTeam(ctx, t); err != nil {
+ return err
+ }
+
+ if err := db.DeleteBeans(ctx,
+ &organization.Team{ID: t.ID},
+ &organization.TeamUser{OrgID: t.OrgID, TeamID: t.ID},
+ &organization.TeamUnit{TeamID: t.ID},
+ &organization.TeamInvite{TeamID: t.ID},
+ &issues_model.Review{Type: issues_model.ReviewTypeRequest, ReviewerTeamID: t.ID}, // batch delete the binding relationship between team and PR (request review from team)
+ ); err != nil {
+ return err
+ }
+
+ for _, tm := range t.Members {
+ if err := removeInvalidOrgUser(ctx, t.OrgID, tm); err != nil {
+ return err
+ }
+ }
+
+ // Update organization number of teams.
+ if _, err := db.Exec(ctx, "UPDATE `user` SET num_teams=num_teams-1 WHERE id=?", t.OrgID); err != nil {
+ return err
+ }
+
+ return committer.Commit()
+}
+
+// AddTeamMember adds new membership of given team to given organization,
+// the user will have membership to given organization automatically when needed.
+func AddTeamMember(ctx context.Context, team *organization.Team, user *user_model.User) error {
+ if user_model.IsUserBlockedBy(ctx, user, team.OrgID) {
+ return user_model.ErrBlockedUser
+ }
+
+ isAlreadyMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, user.ID)
+ if err != nil || isAlreadyMember {
+ return err
+ }
+
+ if err := organization.AddOrgUser(ctx, team.OrgID, user.ID); err != nil {
+ return err
+ }
+
+ err = db.WithTx(ctx, func(ctx context.Context) error {
+ // check in transaction
+ isAlreadyMember, err = organization.IsTeamMember(ctx, team.OrgID, team.ID, user.ID)
+ if err != nil || isAlreadyMember {
+ return err
+ }
+
+ sess := db.GetEngine(ctx)
+
+ if err := db.Insert(ctx, &organization.TeamUser{
+ UID: user.ID,
+ OrgID: team.OrgID,
+ TeamID: team.ID,
+ }); err != nil {
+ return err
+ } else if _, err := sess.Incr("num_members").ID(team.ID).Update(new(organization.Team)); err != nil {
+ return err
+ }
+
+ team.NumMembers++
+
+ // Give access to team repositories.
+ // update exist access if mode become bigger
+ subQuery := builder.Select("repo_id").From("team_repo").
+ Where(builder.Eq{"team_id": team.ID})
+
+ if _, err := sess.Where("user_id=?", user.ID).
+ In("repo_id", subQuery).
+ And("mode < ?", team.AccessMode).
+ SetExpr("mode", team.AccessMode).
+ Update(new(access_model.Access)); err != nil {
+ return fmt.Errorf("update user accesses: %w", err)
+ }
+
+ // for not exist access
+ var repoIDs []int64
+ accessSubQuery := builder.Select("repo_id").From("access").Where(builder.Eq{"user_id": user.ID})
+ if err := sess.SQL(subQuery.And(builder.NotIn("repo_id", accessSubQuery))).Find(&repoIDs); err != nil {
+ return fmt.Errorf("select id accesses: %w", err)
+ }
+
+ accesses := make([]*access_model.Access, 0, 100)
+ for i, repoID := range repoIDs {
+ accesses = append(accesses, &access_model.Access{RepoID: repoID, UserID: user.ID, Mode: team.AccessMode})
+ if (i%100 == 0 || i == len(repoIDs)-1) && len(accesses) > 0 {
+ if err = db.Insert(ctx, accesses); err != nil {
+ return fmt.Errorf("insert new user accesses: %w", err)
+ }
+ accesses = accesses[:0]
+ }
+ }
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+
+ // this behaviour may spend much time so run it in a goroutine
+ // FIXME: Update watch repos batchly
+ if setting.Service.AutoWatchNewRepos {
+ // Get team and its repositories.
+ if err := team.LoadRepositories(ctx); err != nil {
+ log.Error("team.LoadRepositories failed: %v", err)
+ }
+
+ // FIXME: in the goroutine, it can't access the "ctx", it could only use db.DefaultContext at the moment
+ go func(repos []*repo_model.Repository) {
+ for _, repo := range repos {
+ if err = repo_model.WatchRepo(db.DefaultContext, user, repo, true); err != nil {
+ log.Error("watch repo failed: %v", err)
+ }
+ }
+ }(team.Repos)
+ }
+
+ return nil
+}
+
+func removeTeamMember(ctx context.Context, team *organization.Team, user *user_model.User) error {
+ e := db.GetEngine(ctx)
+ isMember, err := organization.IsTeamMember(ctx, team.OrgID, team.ID, user.ID)
+ if err != nil || !isMember {
+ return err
+ }
+
+ // Check if the user to delete is the last member in owner team.
+ if team.IsOwnerTeam() && team.NumMembers == 1 {
+ return organization.ErrLastOrgOwner{UID: user.ID}
+ }
+
+ team.NumMembers--
+
+ if err := team.LoadRepositories(ctx); err != nil {
+ return err
+ }
+
+ if _, err := e.Delete(&organization.TeamUser{
+ UID: user.ID,
+ OrgID: team.OrgID,
+ TeamID: team.ID,
+ }); err != nil {
+ return err
+ } else if _, err = e.
+ ID(team.ID).
+ Cols("num_members").
+ Update(team); err != nil {
+ return err
+ }
+
+ // Delete access to team repositories.
+ for _, repo := range team.Repos {
+ if err := access_model.RecalculateUserAccess(ctx, repo, user.ID); err != nil {
+ return err
+ }
+
+ // Remove watches from now unaccessible
+ if err := repo_service.ReconsiderWatches(ctx, repo, user); err != nil {
+ return err
+ }
+
+ // Remove issue assignments from now unaccessible
+ if err := repo_service.ReconsiderRepoIssuesAssignee(ctx, repo, user); err != nil {
+ return err
+ }
+ }
+
+ return removeInvalidOrgUser(ctx, team.OrgID, user)
+}
+
+func removeInvalidOrgUser(ctx context.Context, orgID int64, user *user_model.User) error {
+ // Check if the user is a member of any team in the organization.
+ if count, err := db.GetEngine(ctx).Count(&organization.TeamUser{
+ UID: user.ID,
+ OrgID: orgID,
+ }); err != nil {
+ return err
+ } else if count == 0 {
+ org, err := organization.GetOrgByID(ctx, orgID)
+ if err != nil {
+ return err
+ }
+
+ return RemoveOrgUser(ctx, org, user)
+ }
+ return nil
+}
+
+// RemoveTeamMember removes member from given team of given organization.
+func RemoveTeamMember(ctx context.Context, team *organization.Team, user *user_model.User) error {
+ ctx, committer, err := db.TxContext(ctx)
+ if err != nil {
+ return err
+ }
+ defer committer.Close()
+ if err := removeTeamMember(ctx, team, user); err != nil {
+ return err
+ }
+ return committer.Commit()
+}
diff --git a/services/org/team_test.go b/services/org/team_test.go
new file mode 100644
index 0000000000..58b8e0803c
--- /dev/null
+++ b/services/org/team_test.go
@@ -0,0 +1,314 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package org
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+
+ "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"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/structs"
+ repo_service "code.gitea.io/gitea/services/repository"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestTeam_AddMember(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ test := func(team *organization.Team, user *user_model.User) {
+ assert.NoError(t, AddTeamMember(db.DefaultContext, team, user))
+ unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID})
+ unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID}, &user_model.User{ID: team.OrgID})
+ }
+
+ team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
+ team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3})
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+
+ test(team1, user2)
+ test(team1, user4)
+ test(team3, user2)
+}
+
+func TestTeam_RemoveMember(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ testSuccess := func(team *organization.Team, user *user_model.User) {
+ assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, user))
+ unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID})
+ unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID})
+ }
+
+ team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
+ team2 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
+ team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3})
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+
+ testSuccess(team1, user4)
+ testSuccess(team2, user2)
+ testSuccess(team3, user2)
+
+ err := RemoveTeamMember(db.DefaultContext, team1, user2)
+ assert.True(t, organization.IsErrLastOrgOwner(err))
+}
+
+func TestNewTeam(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ const teamName = "newTeamName"
+ team := &organization.Team{Name: teamName, OrgID: 3}
+ assert.NoError(t, NewTeam(db.DefaultContext, team))
+ unittest.AssertExistsAndLoadBean(t, &organization.Team{Name: teamName})
+ unittest.CheckConsistencyFor(t, &organization.Team{}, &user_model.User{ID: team.OrgID})
+}
+
+func TestUpdateTeam(t *testing.T) {
+ // successful update
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
+ team.LowerName = "newname"
+ team.Name = "newName"
+ team.Description = strings.Repeat("A long description!", 100)
+ team.AccessMode = perm.AccessModeAdmin
+ assert.NoError(t, UpdateTeam(db.DefaultContext, team, true, false))
+
+ team = unittest.AssertExistsAndLoadBean(t, &organization.Team{Name: "newName"})
+ assert.True(t, strings.HasPrefix(team.Description, "A long description!"))
+
+ access := unittest.AssertExistsAndLoadBean(t, &access_model.Access{UserID: 4, RepoID: 3})
+ assert.EqualValues(t, perm.AccessModeAdmin, access.Mode)
+
+ unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID})
+}
+
+func TestUpdateTeam2(t *testing.T) {
+ // update to already-existing team
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
+ team.LowerName = "owners"
+ team.Name = "Owners"
+ team.Description = strings.Repeat("A long description!", 100)
+ err := UpdateTeam(db.DefaultContext, team, true, false)
+ assert.True(t, organization.IsErrTeamAlreadyExist(err))
+
+ unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID})
+}
+
+func TestDeleteTeam(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
+ assert.NoError(t, DeleteTeam(db.DefaultContext, team))
+ unittest.AssertNotExistsBean(t, &organization.Team{ID: team.ID})
+ unittest.AssertNotExistsBean(t, &organization.TeamRepo{TeamID: team.ID})
+ unittest.AssertNotExistsBean(t, &organization.TeamUser{TeamID: team.ID})
+
+ // check that team members don't have "leftover" access to repos
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+ repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
+ accessMode, err := access_model.AccessLevel(db.DefaultContext, user, repo)
+ assert.NoError(t, err)
+ assert.True(t, accessMode < perm.AccessModeWrite)
+}
+
+func TestAddTeamMember(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ test := func(team *organization.Team, user *user_model.User) {
+ assert.NoError(t, AddTeamMember(db.DefaultContext, team, user))
+ unittest.AssertExistsAndLoadBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID})
+ unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID}, &user_model.User{ID: team.OrgID})
+ }
+
+ team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
+ team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3})
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+
+ test(team1, user2)
+ test(team1, user4)
+ test(team3, user2)
+}
+
+func TestRemoveTeamMember(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ testSuccess := func(team *organization.Team, user *user_model.User) {
+ assert.NoError(t, RemoveTeamMember(db.DefaultContext, team, user))
+ unittest.AssertNotExistsBean(t, &organization.TeamUser{UID: user.ID, TeamID: team.ID})
+ unittest.CheckConsistencyFor(t, &organization.Team{ID: team.ID})
+ }
+
+ team1 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 1})
+ team2 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
+ team3 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 3})
+ user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+
+ testSuccess(team1, user4)
+ testSuccess(team2, user2)
+ testSuccess(team3, user2)
+
+ err := RemoveTeamMember(db.DefaultContext, team1, user2)
+ assert.True(t, organization.IsErrLastOrgOwner(err))
+}
+
+func TestRepository_RecalculateAccesses3(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+ team5 := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 5})
+ user29 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 29})
+
+ has, err := db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: user29.ID, RepoID: 23})
+ assert.NoError(t, err)
+ assert.False(t, has)
+
+ // adding user29 to team5 should add an explicit access row for repo 23
+ // even though repo 23 is public
+ assert.NoError(t, AddTeamMember(db.DefaultContext, team5, user29))
+
+ has, err = db.GetEngine(db.DefaultContext).Get(&access_model.Access{UserID: user29.ID, RepoID: 23})
+ assert.NoError(t, err)
+ assert.True(t, has)
+}
+
+func TestIncludesAllRepositoriesTeams(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ testTeamRepositories := func(teamID int64, repoIDs []int64) {
+ team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
+ assert.NoError(t, team.LoadRepositories(db.DefaultContext), "%s: GetRepositories", team.Name)
+ assert.Len(t, team.Repos, team.NumRepos, "%s: len repo", team.Name)
+ assert.Len(t, team.Repos, len(repoIDs), "%s: repo count", team.Name)
+ for i, rid := range repoIDs {
+ if rid > 0 {
+ assert.True(t, repo_service.HasRepository(db.DefaultContext, team, rid), "%s: HasRepository(%d) %d", rid, i)
+ }
+ }
+ }
+
+ // Get an admin user.
+ user, err := user_model.GetUserByID(db.DefaultContext, 1)
+ assert.NoError(t, err, "GetUserByID")
+
+ // Create org.
+ org := &organization.Organization{
+ Name: "All_repo",
+ IsActive: true,
+ Type: user_model.UserTypeOrganization,
+ Visibility: structs.VisibleTypePublic,
+ }
+ assert.NoError(t, organization.CreateOrganization(db.DefaultContext, org, user), "CreateOrganization")
+
+ // Check Owner team.
+ ownerTeam, err := org.GetOwnerTeam(db.DefaultContext)
+ assert.NoError(t, err, "GetOwnerTeam")
+ assert.True(t, ownerTeam.IncludesAllRepositories, "Owner team includes all repositories")
+
+ // Create repos.
+ repoIDs := make([]int64, 0)
+ for i := 0; i < 3; i++ {
+ r, err := repo_service.CreateRepositoryDirectly(db.DefaultContext, user, org.AsUser(), repo_service.CreateRepoOptions{Name: fmt.Sprintf("repo-%d", i)})
+ assert.NoError(t, err, "CreateRepository %d", i)
+ if r != nil {
+ repoIDs = append(repoIDs, r.ID)
+ }
+ }
+ // Get fresh copy of Owner team after creating repos.
+ ownerTeam, err = org.GetOwnerTeam(db.DefaultContext)
+ assert.NoError(t, err, "GetOwnerTeam")
+
+ // Create teams and check repositories.
+ teams := []*organization.Team{
+ ownerTeam,
+ {
+ OrgID: org.ID,
+ Name: "team one",
+ AccessMode: perm.AccessModeRead,
+ IncludesAllRepositories: true,
+ },
+ {
+ OrgID: org.ID,
+ Name: "team 2",
+ AccessMode: perm.AccessModeRead,
+ IncludesAllRepositories: false,
+ },
+ {
+ OrgID: org.ID,
+ Name: "team three",
+ AccessMode: perm.AccessModeWrite,
+ IncludesAllRepositories: true,
+ },
+ {
+ OrgID: org.ID,
+ Name: "team 4",
+ AccessMode: perm.AccessModeWrite,
+ IncludesAllRepositories: false,
+ },
+ }
+ teamRepos := [][]int64{
+ repoIDs,
+ repoIDs,
+ {},
+ repoIDs,
+ {},
+ }
+ for i, team := range teams {
+ if i > 0 { // first team is Owner.
+ assert.NoError(t, NewTeam(db.DefaultContext, team), "%s: NewTeam", team.Name)
+ }
+ testTeamRepositories(team.ID, teamRepos[i])
+ }
+
+ // Update teams and check repositories.
+ teams[3].IncludesAllRepositories = false
+ teams[4].IncludesAllRepositories = true
+ teamRepos[4] = repoIDs
+ for i, team := range teams {
+ assert.NoError(t, UpdateTeam(db.DefaultContext, team, false, true), "%s: UpdateTeam", team.Name)
+ testTeamRepositories(team.ID, teamRepos[i])
+ }
+
+ // Create repo and check teams repositories.
+ r, err := repo_service.CreateRepositoryDirectly(db.DefaultContext, user, org.AsUser(), repo_service.CreateRepoOptions{Name: "repo-last"})
+ assert.NoError(t, err, "CreateRepository last")
+ if r != nil {
+ repoIDs = append(repoIDs, r.ID)
+ }
+ teamRepos[0] = repoIDs
+ teamRepos[1] = repoIDs
+ teamRepos[4] = repoIDs
+ for i, team := range teams {
+ testTeamRepositories(team.ID, teamRepos[i])
+ }
+
+ // Remove repo and check teams repositories.
+ assert.NoError(t, repo_service.DeleteRepositoryDirectly(db.DefaultContext, user, repoIDs[0]), "DeleteRepository")
+ teamRepos[0] = repoIDs[1:]
+ teamRepos[1] = repoIDs[1:]
+ teamRepos[3] = repoIDs[1:3]
+ teamRepos[4] = repoIDs[1:]
+ for i, team := range teams {
+ testTeamRepositories(team.ID, teamRepos[i])
+ }
+
+ // Wipe created items.
+ for i, rid := range repoIDs {
+ if i > 0 { // first repo already deleted.
+ assert.NoError(t, repo_service.DeleteRepositoryDirectly(db.DefaultContext, user, rid), "DeleteRepository %d", i)
+ }
+ }
+ assert.NoError(t, organization.DeleteOrganization(db.DefaultContext, org), "DeleteOrganization")
+}
diff --git a/services/org/user.go b/services/org/user.go
new file mode 100644
index 0000000000..0627860fe7
--- /dev/null
+++ b/services/org/user.go
@@ -0,0 +1,102 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package org
+
+import (
+ "context"
+ "fmt"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/organization"
+ access_model "code.gitea.io/gitea/models/perm/access"
+ repo_model "code.gitea.io/gitea/models/repo"
+ user_model "code.gitea.io/gitea/models/user"
+)
+
+// RemoveOrgUser removes user from given organization.
+func RemoveOrgUser(ctx context.Context, org *organization.Organization, user *user_model.User) error {
+ ou := new(organization.OrgUser)
+
+ has, err := db.GetEngine(ctx).
+ Where("uid=?", user.ID).
+ And("org_id=?", org.ID).
+ Get(ou)
+ if err != nil {
+ return fmt.Errorf("get org-user: %w", err)
+ } else if !has {
+ return nil
+ }
+
+ // Check if the user to delete is the last member in owner team.
+ if isOwner, err := organization.IsOrganizationOwner(ctx, org.ID, user.ID); err != nil {
+ return err
+ } else if isOwner {
+ t, err := organization.GetOwnerTeam(ctx, org.ID)
+ if err != nil {
+ return err
+ }
+ if t.NumMembers == 1 {
+ if err := t.LoadMembers(ctx); err != nil {
+ return err
+ }
+ if t.Members[0].ID == user.ID {
+ return organization.ErrLastOrgOwner{UID: user.ID}
+ }
+ }
+ }
+
+ ctx, committer, err := db.TxContext(ctx)
+ if err != nil {
+ return err
+ }
+ defer committer.Close()
+
+ if _, err := db.DeleteByID[organization.OrgUser](ctx, ou.ID); err != nil {
+ return err
+ } else if _, err = db.Exec(ctx, "UPDATE `user` SET num_members=num_members-1 WHERE id=?", org.ID); err != nil {
+ return err
+ }
+
+ // Delete all repository accesses and unwatch them.
+ env, err := organization.AccessibleReposEnv(ctx, org, user.ID)
+ if err != nil {
+ return fmt.Errorf("AccessibleReposEnv: %w", err)
+ }
+ repoIDs, err := env.RepoIDs(1, org.NumRepos)
+ if err != nil {
+ return fmt.Errorf("GetUserRepositories [%d]: %w", user.ID, err)
+ }
+ for _, repoID := range repoIDs {
+ repo, err := repo_model.GetRepositoryByID(ctx, repoID)
+ if err != nil {
+ return err
+ }
+ if err = repo_model.WatchRepo(ctx, user, repo, false); err != nil {
+ return err
+ }
+ }
+
+ if len(repoIDs) > 0 {
+ if _, err = db.GetEngine(ctx).
+ Where("user_id = ?", user.ID).
+ In("repo_id", repoIDs).
+ Delete(new(access_model.Access)); err != nil {
+ return err
+ }
+ }
+
+ // Delete member in their teams.
+ teams, err := organization.GetUserOrgTeams(ctx, org.ID, user.ID)
+ if err != nil {
+ return err
+ }
+ for _, t := range teams {
+ if err = removeTeamMember(ctx, t, user); err != nil {
+ return err
+ }
+ }
+
+ return committer.Commit()
+}
diff --git a/services/org/user_test.go b/services/org/user_test.go
new file mode 100644
index 0000000000..56d01a3b63
--- /dev/null
+++ b/services/org/user_test.go
@@ -0,0 +1,74 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package org
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/organization"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestUser_RemoveMember(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+ user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
+
+ // remove a user that is a member
+ unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{UID: user4.ID, OrgID: org.ID})
+ prevNumMembers := org.NumMembers
+ assert.NoError(t, RemoveOrgUser(db.DefaultContext, org, user4))
+ unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: user4.ID, OrgID: org.ID})
+
+ org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: org.ID})
+ assert.Equal(t, prevNumMembers-1, org.NumMembers)
+
+ // remove a user that is not a member
+ unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: user5.ID, OrgID: org.ID})
+ prevNumMembers = org.NumMembers
+ assert.NoError(t, RemoveOrgUser(db.DefaultContext, org, user5))
+ unittest.AssertNotExistsBean(t, &organization.OrgUser{UID: user5.ID, OrgID: org.ID})
+
+ org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: org.ID})
+ assert.Equal(t, prevNumMembers, org.NumMembers)
+
+ unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{})
+}
+
+func TestRemoveOrgUser(t *testing.T) {
+ assert.NoError(t, unittest.PrepareTestDatabase())
+
+ testSuccess := func(org *organization.Organization, user *user_model.User) {
+ expectedNumMembers := org.NumMembers
+ if unittest.BeanExists(t, &organization.OrgUser{OrgID: org.ID, UID: user.ID}) {
+ expectedNumMembers--
+ }
+ assert.NoError(t, RemoveOrgUser(db.DefaultContext, org, user))
+ unittest.AssertNotExistsBean(t, &organization.OrgUser{OrgID: org.ID, UID: user.ID})
+ org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: org.ID})
+ assert.EqualValues(t, expectedNumMembers, org.NumMembers)
+ }
+
+ org3 := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
+ org7 := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 7})
+ user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4})
+ user5 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
+
+ testSuccess(org3, user4)
+
+ org3 = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
+ testSuccess(org3, user4)
+
+ err := RemoveOrgUser(db.DefaultContext, org7, user5)
+ assert.Error(t, err)
+ assert.True(t, organization.IsErrLastOrgOwner(err))
+ unittest.AssertExistsAndLoadBean(t, &organization.OrgUser{OrgID: org7.ID, UID: user5.ID})
+ unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{})
+}
diff --git a/services/repository/collaboration.go b/services/repository/collaboration.go
index abe0489fc5..b5fc523623 100644
--- a/services/repository/collaboration.go
+++ b/services/repository/collaboration.go
@@ -6,9 +6,10 @@ package repository
import (
"context"
+ "fmt"
- "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
@@ -94,14 +95,39 @@ func DeleteCollaboration(ctx context.Context, repo *repo_model.Repository, colla
return err
}
- if err = models.ReconsiderWatches(ctx, repo, collaborator); err != nil {
+ if err = ReconsiderWatches(ctx, repo, collaborator); err != nil {
return err
}
// Unassign a user from any issue (s)he has been assigned to in the repository
- if err := models.ReconsiderRepoIssuesAssignee(ctx, repo, collaborator); err != nil {
+ if err := ReconsiderRepoIssuesAssignee(ctx, repo, collaborator); err != nil {
return err
}
return committer.Commit()
}
+
+func ReconsiderRepoIssuesAssignee(ctx context.Context, repo *repo_model.Repository, user *user_model.User) error {
+ if canAssigned, err := access_model.CanBeAssigned(ctx, user, repo, true); err != nil || canAssigned {
+ return err
+ }
+
+ if _, err := db.GetEngine(ctx).Where(builder.Eq{"assignee_id": user.ID}).
+ In("issue_id", builder.Select("id").From("issue").Where(builder.Eq{"repo_id": repo.ID})).
+ Delete(&issues_model.IssueAssignees{}); err != nil {
+ return fmt.Errorf("Could not delete assignee[%d] %w", user.ID, err)
+ }
+ return nil
+}
+
+func ReconsiderWatches(ctx context.Context, repo *repo_model.Repository, user *user_model.User) error {
+ if has, err := access_model.HasAnyUnitAccess(ctx, user.ID, repo); err != nil || has {
+ return err
+ }
+ if err := repo_model.WatchRepo(ctx, user, repo, false); err != nil {
+ return err
+ }
+
+ // Remove all IssueWatches a user has subscribed to in the repository
+ return issues_model.RemoveIssueWatchersByRepoID(ctx, user.ID, repo.ID)
+}
diff --git a/services/repository/create.go b/services/repository/create.go
index 0207f12a33..14e625d962 100644
--- a/services/repository/create.go
+++ b/services/repository/create.go
@@ -12,7 +12,6 @@ import (
"strings"
"time"
- "code.gitea.io/gitea/models"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/organization"
"code.gitea.io/gitea/models/perm"
@@ -448,7 +447,7 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re
}
for _, t := range teams {
if t.IncludesAllRepositories {
- if err := models.AddRepository(ctx, t, repo); err != nil {
+ if err := addRepositoryToTeam(ctx, t, repo); err != nil {
return fmt.Errorf("AddRepository: %w", err)
}
}
diff --git a/services/repository/create_test.go b/services/repository/create_test.go
deleted file mode 100644
index 41e6b615db..0000000000
--- a/services/repository/create_test.go
+++ /dev/null
@@ -1,148 +0,0 @@
-// Copyright 2023 The Gitea Authors. All rights reserved.
-// SPDX-License-Identifier: MIT
-
-package repository
-
-import (
- "fmt"
- "testing"
-
- "code.gitea.io/gitea/models"
- "code.gitea.io/gitea/models/db"
- "code.gitea.io/gitea/models/organization"
- "code.gitea.io/gitea/models/perm"
- "code.gitea.io/gitea/models/unittest"
- user_model "code.gitea.io/gitea/models/user"
- "code.gitea.io/gitea/modules/structs"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestIncludesAllRepositoriesTeams(t *testing.T) {
- assert.NoError(t, unittest.PrepareTestDatabase())
-
- testTeamRepositories := func(teamID int64, repoIDs []int64) {
- team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
- assert.NoError(t, team.LoadRepositories(db.DefaultContext), "%s: GetRepositories", team.Name)
- assert.Len(t, team.Repos, team.NumRepos, "%s: len repo", team.Name)
- assert.Len(t, team.Repos, len(repoIDs), "%s: repo count", team.Name)
- for i, rid := range repoIDs {
- if rid > 0 {
- assert.True(t, HasRepository(db.DefaultContext, team, rid), "%s: HasRepository(%d) %d", rid, i)
- }
- }
- }
-
- // Get an admin user.
- user, err := user_model.GetUserByID(db.DefaultContext, 1)
- assert.NoError(t, err, "GetUserByID")
-
- // Create org.
- org := &organization.Organization{
- Name: "All_repo",
- IsActive: true,
- Type: user_model.UserTypeOrganization,
- Visibility: structs.VisibleTypePublic,
- }
- assert.NoError(t, organization.CreateOrganization(db.DefaultContext, org, user), "CreateOrganization")
-
- // Check Owner team.
- ownerTeam, err := org.GetOwnerTeam(db.DefaultContext)
- assert.NoError(t, err, "GetOwnerTeam")
- assert.True(t, ownerTeam.IncludesAllRepositories, "Owner team includes all repositories")
-
- // Create repos.
- repoIDs := make([]int64, 0)
- for i := 0; i < 3; i++ {
- r, err := CreateRepositoryDirectly(db.DefaultContext, user, org.AsUser(), CreateRepoOptions{Name: fmt.Sprintf("repo-%d", i)})
- assert.NoError(t, err, "CreateRepository %d", i)
- if r != nil {
- repoIDs = append(repoIDs, r.ID)
- }
- }
- // Get fresh copy of Owner team after creating repos.
- ownerTeam, err = org.GetOwnerTeam(db.DefaultContext)
- assert.NoError(t, err, "GetOwnerTeam")
-
- // Create teams and check repositories.
- teams := []*organization.Team{
- ownerTeam,
- {
- OrgID: org.ID,
- Name: "team one",
- AccessMode: perm.AccessModeRead,
- IncludesAllRepositories: true,
- },
- {
- OrgID: org.ID,
- Name: "team 2",
- AccessMode: perm.AccessModeRead,
- IncludesAllRepositories: false,
- },
- {
- OrgID: org.ID,
- Name: "team three",
- AccessMode: perm.AccessModeWrite,
- IncludesAllRepositories: true,
- },
- {
- OrgID: org.ID,
- Name: "team 4",
- AccessMode: perm.AccessModeWrite,
- IncludesAllRepositories: false,
- },
- }
- teamRepos := [][]int64{
- repoIDs,
- repoIDs,
- {},
- repoIDs,
- {},
- }
- for i, team := range teams {
- if i > 0 { // first team is Owner.
- assert.NoError(t, models.NewTeam(db.DefaultContext, team), "%s: NewTeam", team.Name)
- }
- testTeamRepositories(team.ID, teamRepos[i])
- }
-
- // Update teams and check repositories.
- teams[3].IncludesAllRepositories = false
- teams[4].IncludesAllRepositories = true
- teamRepos[4] = repoIDs
- for i, team := range teams {
- assert.NoError(t, models.UpdateTeam(db.DefaultContext, team, false, true), "%s: UpdateTeam", team.Name)
- testTeamRepositories(team.ID, teamRepos[i])
- }
-
- // Create repo and check teams repositories.
- r, err := CreateRepositoryDirectly(db.DefaultContext, user, org.AsUser(), CreateRepoOptions{Name: "repo-last"})
- assert.NoError(t, err, "CreateRepository last")
- if r != nil {
- repoIDs = append(repoIDs, r.ID)
- }
- teamRepos[0] = repoIDs
- teamRepos[1] = repoIDs
- teamRepos[4] = repoIDs
- for i, team := range teams {
- testTeamRepositories(team.ID, teamRepos[i])
- }
-
- // Remove repo and check teams repositories.
- assert.NoError(t, DeleteRepositoryDirectly(db.DefaultContext, user, repoIDs[0]), "DeleteRepository")
- teamRepos[0] = repoIDs[1:]
- teamRepos[1] = repoIDs[1:]
- teamRepos[3] = repoIDs[1:3]
- teamRepos[4] = repoIDs[1:]
- for i, team := range teams {
- testTeamRepositories(team.ID, teamRepos[i])
- }
-
- // Wipe created items.
- for i, rid := range repoIDs {
- if i > 0 { // first repo already deleted.
- assert.NoError(t, DeleteRepositoryDirectly(db.DefaultContext, user, rid), "DeleteRepository %d", i)
- }
- }
- assert.NoError(t, organization.DeleteOrganization(db.DefaultContext, org), "DeleteOrganization")
-}
diff --git a/services/repository/delete.go b/services/repository/delete.go
index e580833140..f33bae7790 100644
--- a/services/repository/delete.go
+++ b/services/repository/delete.go
@@ -348,87 +348,6 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID
return nil
}
-// removeRepositoryFromTeam removes a repository from a team and recalculates access
-// Note: Repository shall not be removed from team if it includes all repositories (unless the repository is deleted)
-func removeRepositoryFromTeam(ctx context.Context, t *organization.Team, repo *repo_model.Repository, recalculate bool) (err error) {
- e := db.GetEngine(ctx)
- if err = organization.RemoveTeamRepo(ctx, t.ID, repo.ID); err != nil {
- return err
- }
-
- t.NumRepos--
- if _, err = e.ID(t.ID).Cols("num_repos").Update(t); err != nil {
- return err
- }
-
- // Don't need to recalculate when delete a repository from organization.
- if recalculate {
- if err = access_model.RecalculateTeamAccesses(ctx, repo, t.ID); err != nil {
- return err
- }
- }
-
- teamMembers, err := organization.GetTeamMembers(ctx, &organization.SearchMembersOptions{
- TeamID: t.ID,
- })
- if err != nil {
- return fmt.Errorf("GetTeamMembers: %w", err)
- }
- for _, member := range teamMembers {
- has, err := access_model.HasAnyUnitAccess(ctx, member.ID, repo)
- if err != nil {
- return err
- } else if has {
- continue
- }
-
- if err = repo_model.WatchRepo(ctx, member, repo, false); err != nil {
- return err
- }
-
- // Remove all IssueWatches a user has subscribed to in the repositories
- if err := issues_model.RemoveIssueWatchersByRepoID(ctx, member.ID, repo.ID); err != nil {
- return err
- }
- }
-
- return nil
-}
-
-// HasRepository returns true if given repository belong to team.
-func HasRepository(ctx context.Context, t *organization.Team, repoID int64) bool {
- return organization.HasTeamRepo(ctx, t.OrgID, t.ID, repoID)
-}
-
-// RemoveRepositoryFromTeam removes repository from team of organization.
-// If the team shall include all repositories the request is ignored.
-func RemoveRepositoryFromTeam(ctx context.Context, t *organization.Team, repoID int64) error {
- if !HasRepository(ctx, t, repoID) {
- return nil
- }
-
- if t.IncludesAllRepositories {
- return nil
- }
-
- repo, err := repo_model.GetRepositoryByID(ctx, repoID)
- if err != nil {
- return err
- }
-
- ctx, committer, err := db.TxContext(ctx)
- if err != nil {
- return err
- }
- defer committer.Close()
-
- if err = removeRepositoryFromTeam(ctx, t, repo, true); err != nil {
- return err
- }
-
- return committer.Commit()
-}
-
// DeleteOwnerRepositoriesDirectly calls DeleteRepositoryDirectly for all repos of the given owner
func DeleteOwnerRepositoriesDirectly(ctx context.Context, owner *user_model.User) error {
for {
diff --git a/services/repository/repo_team.go b/services/repository/repo_team.go
new file mode 100644
index 0000000000..29c67893b2
--- /dev/null
+++ b/services/repository/repo_team.go
@@ -0,0 +1,226 @@
+// Copyright 2024 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package repository
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "code.gitea.io/gitea/models/db"
+ issues_model "code.gitea.io/gitea/models/issues"
+ "code.gitea.io/gitea/models/organization"
+ access_model "code.gitea.io/gitea/models/perm/access"
+ repo_model "code.gitea.io/gitea/models/repo"
+ "code.gitea.io/gitea/modules/setting"
+)
+
+// TeamAddRepository adds new repository to team of organization.
+func TeamAddRepository(ctx context.Context, t *organization.Team, repo *repo_model.Repository) (err error) {
+ if repo.OwnerID != t.OrgID {
+ return errors.New("repository does not belong to organization")
+ } else if organization.HasTeamRepo(ctx, t.OrgID, t.ID, repo.ID) {
+ return nil
+ }
+
+ return db.WithTx(ctx, func(ctx context.Context) error {
+ return addRepositoryToTeam(ctx, t, repo)
+ })
+}
+
+func addRepositoryToTeam(ctx context.Context, t *organization.Team, repo *repo_model.Repository) (err error) {
+ if err = organization.AddTeamRepo(ctx, t.OrgID, t.ID, repo.ID); err != nil {
+ return err
+ }
+
+ if err = organization.IncrTeamRepoNum(ctx, t.ID); err != nil {
+ return fmt.Errorf("update team: %w", err)
+ }
+
+ t.NumRepos++
+
+ if err = access_model.RecalculateTeamAccesses(ctx, repo, 0); err != nil {
+ return fmt.Errorf("recalculateAccesses: %w", err)
+ }
+
+ // Make all team members watch this repo if enabled in global settings
+ if setting.Service.AutoWatchNewRepos {
+ if err = t.LoadMembers(ctx); err != nil {
+ return fmt.Errorf("getMembers: %w", err)
+ }
+ for _, u := range t.Members {
+ if err = repo_model.WatchRepo(ctx, u, repo, true); err != nil {
+ return fmt.Errorf("watchRepo: %w", err)
+ }
+ }
+ }
+
+ return nil
+}
+
+// AddAllRepositoriesToTeam adds all repositories to the team.
+// If the team already has some repositories they will be left unchanged.
+func AddAllRepositoriesToTeam(ctx context.Context, t *organization.Team) error {
+ return db.WithTx(ctx, func(ctx context.Context) error {
+ orgRepos, err := organization.GetOrgRepositories(ctx, t.OrgID)
+ if err != nil {
+ return fmt.Errorf("get org repos: %w", err)
+ }
+
+ for _, repo := range orgRepos {
+ if !organization.HasTeamRepo(ctx, t.OrgID, t.ID, repo.ID) {
+ if err := addRepositoryToTeam(ctx, t, repo); err != nil {
+ return fmt.Errorf("AddRepository: %w", err)
+ }
+ }
+ }
+
+ return nil
+ })
+}
+
+// RemoveAllRepositoriesFromTeam removes all repositories from team and recalculates access
+func RemoveAllRepositoriesFromTeam(ctx context.Context, t *organization.Team) (err error) {
+ if t.IncludesAllRepositories {
+ return nil
+ }
+
+ ctx, committer, err := db.TxContext(ctx)
+ if err != nil {
+ return err
+ }
+ defer committer.Close()
+
+ if err = removeAllRepositoriesFromTeam(ctx, t); err != nil {
+ return err
+ }
+
+ return committer.Commit()
+}
+
+// removeAllRepositoriesFromTeam removes all repositories from team and recalculates access
+// Note: Shall not be called if team includes all repositories
+func removeAllRepositoriesFromTeam(ctx context.Context, t *organization.Team) (err error) {
+ e := db.GetEngine(ctx)
+ // Delete all accesses.
+ for _, repo := range t.Repos {
+ if err := access_model.RecalculateTeamAccesses(ctx, repo, t.ID); err != nil {
+ return err
+ }
+
+ // Remove watches from all users and now unaccessible repos
+ for _, user := range t.Members {
+ has, err := access_model.HasAnyUnitAccess(ctx, user.ID, repo)
+ if err != nil {
+ return err
+ } else if has {
+ continue
+ }
+
+ if err = repo_model.WatchRepo(ctx, user, repo, false); err != nil {
+ return err
+ }
+
+ // Remove all IssueWatches a user has subscribed to in the repositories
+ if err = issues_model.RemoveIssueWatchersByRepoID(ctx, user.ID, repo.ID); err != nil {
+ return err
+ }
+ }
+ }
+
+ // Delete team-repo
+ if _, err := e.
+ Where("team_id=?", t.ID).
+ Delete(new(organization.TeamRepo)); err != nil {
+ return err
+ }
+
+ t.NumRepos = 0
+ if _, err = e.ID(t.ID).Cols("num_repos").Update(t); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// RemoveRepositoryFromTeam removes repository from team of organization.
+// If the team shall include all repositories the request is ignored.
+func RemoveRepositoryFromTeam(ctx context.Context, t *organization.Team, repoID int64) error {
+ if !HasRepository(ctx, t, repoID) {
+ return nil
+ }
+
+ if t.IncludesAllRepositories {
+ return nil
+ }
+
+ repo, err := repo_model.GetRepositoryByID(ctx, repoID)
+ if err != nil {
+ return err
+ }
+
+ ctx, committer, err := db.TxContext(ctx)
+ if err != nil {
+ return err
+ }
+ defer committer.Close()
+
+ if err = removeRepositoryFromTeam(ctx, t, repo, true); err != nil {
+ return err
+ }
+
+ return committer.Commit()
+}
+
+// removeRepositoryFromTeam removes a repository from a team and recalculates access
+// Note: Repository shall not be removed from team if it includes all repositories (unless the repository is deleted)
+func removeRepositoryFromTeam(ctx context.Context, t *organization.Team, repo *repo_model.Repository, recalculate bool) (err error) {
+ e := db.GetEngine(ctx)
+ if err = organization.RemoveTeamRepo(ctx, t.ID, repo.ID); err != nil {
+ return err
+ }
+
+ t.NumRepos--
+ if _, err = e.ID(t.ID).Cols("num_repos").Update(t); err != nil {
+ return err
+ }
+
+ // Don't need to recalculate when delete a repository from organization.
+ if recalculate {
+ if err = access_model.RecalculateTeamAccesses(ctx, repo, t.ID); err != nil {
+ return err
+ }
+ }
+
+ teamMembers, err := organization.GetTeamMembers(ctx, &organization.SearchMembersOptions{
+ TeamID: t.ID,
+ })
+ if err != nil {
+ return fmt.Errorf("GetTeamMembers: %w", err)
+ }
+ for _, member := range teamMembers {
+ has, err := access_model.HasAnyUnitAccess(ctx, member.ID, repo)
+ if err != nil {
+ return err
+ } else if has {
+ continue
+ }
+
+ if err = repo_model.WatchRepo(ctx, member, repo, false); err != nil {
+ return err
+ }
+
+ // Remove all IssueWatches a user has subscribed to in the repositories
+ if err := issues_model.RemoveIssueWatchersByRepoID(ctx, member.ID, repo.ID); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// HasRepository returns true if given repository belong to team.
+func HasRepository(ctx context.Context, t *organization.Team, repoID int64) bool {
+ return organization.HasTeamRepo(ctx, t.OrgID, t.ID, repoID)
+}
diff --git a/services/org/repo_test.go b/services/repository/repo_team_test.go
index 68c64a01ab..70b1b47d0a 100644
--- a/services/org/repo_test.go
+++ b/services/repository/repo_team_test.go
@@ -1,7 +1,7 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
-package org
+package repository
import (
"testing"
diff --git a/services/repository/transfer.go b/services/repository/transfer.go
index 301d895337..9a643469d9 100644
--- a/services/repository/transfer.go
+++ b/services/repository/transfer.go
@@ -59,7 +59,7 @@ func TransferOwnership(ctx context.Context, doer, newOwner *user_model.User, rep
}
for _, team := range teams {
- if err := models.AddRepository(ctx, team, newRepo); err != nil {
+ if err := addRepositoryToTeam(ctx, team, newRepo); err != nil {
return err
}
}
@@ -205,7 +205,7 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName
}
for _, t := range teams {
if t.IncludesAllRepositories {
- if err := models.AddRepository(ctx, t, repo); err != nil {
+ if err := addRepositoryToTeam(ctx, t, repo); err != nil {
return fmt.Errorf("AddRepository: %w", err)
}
}
diff --git a/services/user/user.go b/services/user/user.go
index 7855dbb78b..7bde642412 100644
--- a/services/user/user.go
+++ b/services/user/user.go
@@ -188,7 +188,7 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
break
}
for _, org := range orgs {
- if err := models.RemoveOrgUser(ctx, org, u); err != nil {
+ if err := org_service.RemoveOrgUser(ctx, org, u); err != nil {
if organization.IsErrLastOrgOwner(err) {
err = org_service.DeleteOrganization(ctx, org, true)
if err != nil {
diff --git a/services/user/user_test.go b/services/user/user_test.go
index efcbc669c8..c668b005c5 100644
--- a/services/user/user_test.go
+++ b/services/user/user_test.go
@@ -18,6 +18,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
+ org_service "code.gitea.io/gitea/services/org"
"github.com/stretchr/testify/assert"
)
@@ -44,7 +45,7 @@ func TestDeleteUser(t *testing.T) {
assert.NoError(t, db.GetEngine(db.DefaultContext).Find(&orgUsers, &organization.OrgUser{UID: userID}))
for _, orgUser := range orgUsers {
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: orgUser.OrgID})
- if err := models.RemoveOrgUser(db.DefaultContext, org, user); err != nil {
+ if err := org_service.RemoveOrgUser(db.DefaultContext, org, user); err != nil {
assert.True(t, organization.IsErrLastOrgOwner(err))
return
}