mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 08:30:25 +08:00 
			
		
		
		
	Delete repos of org when purge delete user (#27273)
Fixes https://codeberg.org/forgejo/forgejo/issues/1514 I had to remove `RenameOrganization` to avoid circular import. We should really add some foreign keys to the database.
This commit is contained in:
		@@ -385,7 +385,7 @@ func Delete(ctx *context.APIContext) {
 | 
				
			|||||||
	//   "404":
 | 
						//   "404":
 | 
				
			||||||
	//     "$ref": "#/responses/notFound"
 | 
						//     "$ref": "#/responses/notFound"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if err := org.DeleteOrganization(ctx, ctx.Org.Organization); err != nil {
 | 
						if err := org.DeleteOrganization(ctx, ctx.Org.Organization, false); err != nil {
 | 
				
			||||||
		ctx.Error(http.StatusInternalServerError, "DeleteOrganization", err)
 | 
							ctx.Error(http.StatusInternalServerError, "DeleteOrganization", err)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -75,7 +75,7 @@ func SettingsPost(ctx *context.Context) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// Check if organization name has been changed.
 | 
						// Check if organization name has been changed.
 | 
				
			||||||
	if nameChanged {
 | 
						if nameChanged {
 | 
				
			||||||
		err := org_service.RenameOrganization(ctx, org, form.Name)
 | 
							err := user_service.RenameUser(ctx, org.AsUser(), form.Name)
 | 
				
			||||||
		switch {
 | 
							switch {
 | 
				
			||||||
		case user_model.IsErrUserAlreadyExist(err):
 | 
							case user_model.IsErrUserAlreadyExist(err):
 | 
				
			||||||
			ctx.Data["OrgName"] = true
 | 
								ctx.Data["OrgName"] = true
 | 
				
			||||||
@@ -180,7 +180,7 @@ func SettingsDelete(ctx *context.Context) {
 | 
				
			|||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if err := org_service.DeleteOrganization(ctx, ctx.Org.Organization); err != nil {
 | 
							if err := org_service.DeleteOrganization(ctx, ctx.Org.Organization, false); err != nil {
 | 
				
			||||||
			if models.IsErrUserOwnRepos(err) {
 | 
								if models.IsErrUserOwnRepos(err) {
 | 
				
			||||||
				ctx.Flash.Error(ctx.Tr("form.org_still_own_repo"))
 | 
									ctx.Flash.Error(ctx.Tr("form.org_still_own_repo"))
 | 
				
			||||||
				ctx.Redirect(ctx.Org.OrgLink + "/settings/delete")
 | 
									ctx.Redirect(ctx.Org.OrgLink + "/settings/delete")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,17 +15,24 @@ import (
 | 
				
			|||||||
	user_model "code.gitea.io/gitea/models/user"
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/storage"
 | 
						"code.gitea.io/gitea/modules/storage"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/util"
 | 
						"code.gitea.io/gitea/modules/util"
 | 
				
			||||||
	user_service "code.gitea.io/gitea/services/user"
 | 
						repo_service "code.gitea.io/gitea/services/repository"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// DeleteOrganization completely and permanently deletes everything of organization.
 | 
					// DeleteOrganization completely and permanently deletes everything of organization.
 | 
				
			||||||
func DeleteOrganization(ctx context.Context, org *org_model.Organization) error {
 | 
					func DeleteOrganization(ctx context.Context, org *org_model.Organization, purge bool) error {
 | 
				
			||||||
	ctx, commiter, err := db.TxContext(ctx)
 | 
						ctx, commiter, err := db.TxContext(ctx)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	defer commiter.Close()
 | 
						defer commiter.Close()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if purge {
 | 
				
			||||||
 | 
							err := repo_service.DeleteOwnerRepositoriesDirectly(ctx, org.AsUser())
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return err
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Check ownership of repository.
 | 
						// Check ownership of repository.
 | 
				
			||||||
	count, err := repo_model.CountRepositories(ctx, repo_model.CountRepositoryOptions{OwnerID: org.ID})
 | 
						count, err := repo_model.CountRepositories(ctx, repo_model.CountRepositoryOptions{OwnerID: org.ID})
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@@ -67,8 +74,3 @@ func DeleteOrganization(ctx context.Context, org *org_model.Organization) error
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	return nil
 | 
						return nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
// RenameOrganization renames an organization.
 | 
					 | 
				
			||||||
func RenameOrganization(ctx context.Context, org *org_model.Organization, newName string) error {
 | 
					 | 
				
			||||||
	return user_service.RenameUser(ctx, org.AsUser(), newName)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,17 +22,17 @@ func TestMain(m *testing.M) {
 | 
				
			|||||||
func TestDeleteOrganization(t *testing.T) {
 | 
					func TestDeleteOrganization(t *testing.T) {
 | 
				
			||||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
						assert.NoError(t, unittest.PrepareTestDatabase())
 | 
				
			||||||
	org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 6})
 | 
						org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 6})
 | 
				
			||||||
	assert.NoError(t, DeleteOrganization(db.DefaultContext, org))
 | 
						assert.NoError(t, DeleteOrganization(db.DefaultContext, org, false))
 | 
				
			||||||
	unittest.AssertNotExistsBean(t, &organization.Organization{ID: 6})
 | 
						unittest.AssertNotExistsBean(t, &organization.Organization{ID: 6})
 | 
				
			||||||
	unittest.AssertNotExistsBean(t, &organization.OrgUser{OrgID: 6})
 | 
						unittest.AssertNotExistsBean(t, &organization.OrgUser{OrgID: 6})
 | 
				
			||||||
	unittest.AssertNotExistsBean(t, &organization.Team{OrgID: 6})
 | 
						unittest.AssertNotExistsBean(t, &organization.Team{OrgID: 6})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
 | 
						org = unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
 | 
				
			||||||
	err := DeleteOrganization(db.DefaultContext, org)
 | 
						err := DeleteOrganization(db.DefaultContext, org, false)
 | 
				
			||||||
	assert.Error(t, err)
 | 
						assert.Error(t, err)
 | 
				
			||||||
	assert.True(t, models.IsErrUserOwnRepos(err))
 | 
						assert.True(t, models.IsErrUserOwnRepos(err))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	user := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 5})
 | 
						user := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 5})
 | 
				
			||||||
	assert.Error(t, DeleteOrganization(db.DefaultContext, user))
 | 
						assert.Error(t, DeleteOrganization(db.DefaultContext, user, false))
 | 
				
			||||||
	unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{})
 | 
						unittest.CheckConsistencyFor(t, &user_model.User{}, &organization.Team{})
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -420,3 +420,30 @@ func RemoveRepositoryFromTeam(ctx context.Context, t *organization.Team, repoID
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	return committer.Commit()
 | 
						return committer.Commit()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DeleteOwnerRepositoriesDirectly calls DeleteRepositoryDirectly for all repos of the given owner
 | 
				
			||||||
 | 
					func DeleteOwnerRepositoriesDirectly(ctx context.Context, owner *user_model.User) error {
 | 
				
			||||||
 | 
						for {
 | 
				
			||||||
 | 
							repos, _, err := repo_model.GetUserRepositories(ctx, &repo_model.SearchRepoOptions{
 | 
				
			||||||
 | 
								ListOptions: db.ListOptions{
 | 
				
			||||||
 | 
									PageSize: repo_model.RepositoryListDefaultPageSize,
 | 
				
			||||||
 | 
									Page:     1,
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								Private: true,
 | 
				
			||||||
 | 
								OwnerID: owner.ID,
 | 
				
			||||||
 | 
								Actor:   owner,
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("GetUserRepositories: %w", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if len(repos) == 0 {
 | 
				
			||||||
 | 
								break
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							for _, repo := range repos {
 | 
				
			||||||
 | 
								if err := DeleteRepositoryDirectly(ctx, owner, repo.ID); err != nil {
 | 
				
			||||||
 | 
									return fmt.Errorf("unable to delete repository %s for %s[%d]. Error: %w", repo.Name, owner.Name, owner.ID, err)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
// Copyright 2017 The Gitea Authors. All rights reserved.
 | 
					// Copyright 2017 The Gitea Authors. All rights reserved.
 | 
				
			||||||
// SPDX-License-Identifier: MIT
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
package repository
 | 
					package repository_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"testing"
 | 
						"testing"
 | 
				
			||||||
@@ -10,6 +10,8 @@ import (
 | 
				
			|||||||
	"code.gitea.io/gitea/models/organization"
 | 
						"code.gitea.io/gitea/models/organization"
 | 
				
			||||||
	repo_model "code.gitea.io/gitea/models/repo"
 | 
						repo_model "code.gitea.io/gitea/models/repo"
 | 
				
			||||||
	"code.gitea.io/gitea/models/unittest"
 | 
						"code.gitea.io/gitea/models/unittest"
 | 
				
			||||||
 | 
						user_model "code.gitea.io/gitea/models/user"
 | 
				
			||||||
 | 
						repo_service "code.gitea.io/gitea/services/repository"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/stretchr/testify/assert"
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@@ -19,7 +21,7 @@ func TestTeam_HasRepository(t *testing.T) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	test := func(teamID, repoID int64, expected bool) {
 | 
						test := func(teamID, repoID int64, expected bool) {
 | 
				
			||||||
		team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
 | 
							team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
 | 
				
			||||||
		assert.Equal(t, expected, HasRepository(db.DefaultContext, team, repoID))
 | 
							assert.Equal(t, expected, repo_service.HasRepository(db.DefaultContext, team, repoID))
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	test(1, 1, false)
 | 
						test(1, 1, false)
 | 
				
			||||||
	test(1, 3, true)
 | 
						test(1, 3, true)
 | 
				
			||||||
@@ -35,7 +37,7 @@ func TestTeam_RemoveRepository(t *testing.T) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	testSuccess := func(teamID, repoID int64) {
 | 
						testSuccess := func(teamID, repoID int64) {
 | 
				
			||||||
		team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
 | 
							team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: teamID})
 | 
				
			||||||
		assert.NoError(t, RemoveRepositoryFromTeam(db.DefaultContext, team, repoID))
 | 
							assert.NoError(t, repo_service.RemoveRepositoryFromTeam(db.DefaultContext, team, repoID))
 | 
				
			||||||
		unittest.AssertNotExistsBean(t, &organization.TeamRepo{TeamID: teamID, RepoID: repoID})
 | 
							unittest.AssertNotExistsBean(t, &organization.TeamRepo{TeamID: teamID, RepoID: repoID})
 | 
				
			||||||
		unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID}, &repo_model.Repository{ID: repoID})
 | 
							unittest.CheckConsistencyFor(t, &organization.Team{ID: teamID}, &repo_model.Repository{ID: repoID})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -43,3 +45,11 @@ func TestTeam_RemoveRepository(t *testing.T) {
 | 
				
			|||||||
	testSuccess(2, 5)
 | 
						testSuccess(2, 5)
 | 
				
			||||||
	testSuccess(1, unittest.NonexistentID)
 | 
						testSuccess(1, unittest.NonexistentID)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestDeleteOwnerRepositoriesDirectly(t *testing.T) {
 | 
				
			||||||
 | 
						unittest.PrepareTestEnv(t)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						assert.NoError(t, repo_service.DeleteOwnerRepositoriesDirectly(db.DefaultContext, user))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
// Copyright 2023 The Gitea Authors. All rights reserved.
 | 
					// Copyright 2023 The Gitea Authors. All rights reserved.
 | 
				
			||||||
// SPDX-License-Identifier: MIT
 | 
					// SPDX-License-Identifier: MIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
package repository
 | 
					package repository_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"bytes"
 | 
						"bytes"
 | 
				
			||||||
@@ -16,12 +16,13 @@ import (
 | 
				
			|||||||
	"code.gitea.io/gitea/modules/lfs"
 | 
						"code.gitea.io/gitea/modules/lfs"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/storage"
 | 
						"code.gitea.io/gitea/modules/storage"
 | 
				
			||||||
 | 
						repo_service "code.gitea.io/gitea/services/repository"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/stretchr/testify/assert"
 | 
						"github.com/stretchr/testify/assert"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestGarbageCollectLFSMetaObjects(t *testing.T) {
 | 
					func TestGarbageCollectLFSMetaObjects(t *testing.T) {
 | 
				
			||||||
	assert.NoError(t, unittest.PrepareTestDatabase())
 | 
						unittest.PrepareTestEnv(t)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	setting.LFS.StartServer = true
 | 
						setting.LFS.StartServer = true
 | 
				
			||||||
	err := storage.Init()
 | 
						err := storage.Init()
 | 
				
			||||||
@@ -35,7 +36,7 @@ func TestGarbageCollectLFSMetaObjects(t *testing.T) {
 | 
				
			|||||||
	lfsOid := storeObjectInRepo(t, repo.ID, &lfsContent)
 | 
						lfsOid := storeObjectInRepo(t, repo.ID, &lfsContent)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// gc
 | 
						// gc
 | 
				
			||||||
	err = GarbageCollectLFSMetaObjects(context.Background(), GarbageCollectLFSMetaObjectsOptions{
 | 
						err = repo_service.GarbageCollectLFSMetaObjects(context.Background(), repo_service.GarbageCollectLFSMetaObjectsOptions{
 | 
				
			||||||
		AutoFix:                 true,
 | 
							AutoFix:                 true,
 | 
				
			||||||
		OlderThan:               time.Now().Add(7 * 24 * time.Hour).Add(5 * 24 * time.Hour),
 | 
							OlderThan:               time.Now().Add(7 * 24 * time.Hour).Add(5 * 24 * time.Hour),
 | 
				
			||||||
		UpdatedLessRecentlyThan: time.Now().Add(7 * 24 * time.Hour).Add(3 * 24 * time.Hour),
 | 
							UpdatedLessRecentlyThan: time.Now().Add(7 * 24 * time.Hour).Add(3 * 24 * time.Hour),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,6 +24,7 @@ import (
 | 
				
			|||||||
	"code.gitea.io/gitea/modules/storage"
 | 
						"code.gitea.io/gitea/modules/storage"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/util"
 | 
						"code.gitea.io/gitea/modules/util"
 | 
				
			||||||
	"code.gitea.io/gitea/services/agit"
 | 
						"code.gitea.io/gitea/services/agit"
 | 
				
			||||||
 | 
						org_service "code.gitea.io/gitea/services/org"
 | 
				
			||||||
	"code.gitea.io/gitea/services/packages"
 | 
						"code.gitea.io/gitea/services/packages"
 | 
				
			||||||
	container_service "code.gitea.io/gitea/services/packages/container"
 | 
						container_service "code.gitea.io/gitea/services/packages/container"
 | 
				
			||||||
	repo_service "code.gitea.io/gitea/services/repository"
 | 
						repo_service "code.gitea.io/gitea/services/repository"
 | 
				
			||||||
@@ -158,27 +159,9 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
 | 
				
			|||||||
		//
 | 
							//
 | 
				
			||||||
		// An alternative option here would be write a DeleteAllRepositoriesForUserID function which would delete all of the repos
 | 
							// An alternative option here would be write a DeleteAllRepositoriesForUserID function which would delete all of the repos
 | 
				
			||||||
		// but such a function would likely get out of date
 | 
							// but such a function would likely get out of date
 | 
				
			||||||
		for {
 | 
							err := repo_service.DeleteOwnerRepositoriesDirectly(ctx, u)
 | 
				
			||||||
			repos, _, err := repo_model.GetUserRepositories(ctx, &repo_model.SearchRepoOptions{
 | 
							if err != nil {
 | 
				
			||||||
				ListOptions: db.ListOptions{
 | 
								return err
 | 
				
			||||||
					PageSize: repo_model.RepositoryListDefaultPageSize,
 | 
					 | 
				
			||||||
					Page:     1,
 | 
					 | 
				
			||||||
				},
 | 
					 | 
				
			||||||
				Private: true,
 | 
					 | 
				
			||||||
				OwnerID: u.ID,
 | 
					 | 
				
			||||||
				Actor:   u,
 | 
					 | 
				
			||||||
			})
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				return fmt.Errorf("GetUserRepositories: %w", err)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			if len(repos) == 0 {
 | 
					 | 
				
			||||||
				break
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			for _, repo := range repos {
 | 
					 | 
				
			||||||
				if err := repo_service.DeleteRepositoryDirectly(ctx, u, repo.ID); err != nil {
 | 
					 | 
				
			||||||
					return fmt.Errorf("unable to delete repository %s for %s[%d]. Error: %w", repo.Name, u.Name, u.ID, err)
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Remove from Organizations and delete last owner organizations
 | 
							// Remove from Organizations and delete last owner organizations
 | 
				
			||||||
@@ -206,7 +189,10 @@ func DeleteUser(ctx context.Context, u *user_model.User, purge bool) error {
 | 
				
			|||||||
			for _, org := range orgs {
 | 
								for _, org := range orgs {
 | 
				
			||||||
				if err := models.RemoveOrgUser(ctx, org.ID, u.ID); err != nil {
 | 
									if err := models.RemoveOrgUser(ctx, org.ID, u.ID); err != nil {
 | 
				
			||||||
					if organization.IsErrLastOrgOwner(err) {
 | 
										if organization.IsErrLastOrgOwner(err) {
 | 
				
			||||||
						err = organization.DeleteOrganization(ctx, org)
 | 
											err = org_service.DeleteOrganization(ctx, org, true)
 | 
				
			||||||
 | 
											if err != nil {
 | 
				
			||||||
 | 
												return fmt.Errorf("unable to delete organization %d: %w", org.ID, err)
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
					if err != nil {
 | 
										if err != nil {
 | 
				
			||||||
						return fmt.Errorf("unable to remove user %s[%d] from org %s[%d]. Error: %w", u.Name, u.ID, org.Name, org.ID, err)
 | 
											return fmt.Errorf("unable to remove user %s[%d] from org %s[%d]. Error: %w", u.Name, u.ID, org.Name, org.ID, err)
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user