From e83f2cbbacd2696f26dde7105d7f07833c0cc33e Mon Sep 17 00:00:00 2001
From: 6543 <6543@obermui.de>
Date: Thu, 19 Oct 2023 15:20:52 +0200
Subject: [PATCH] Add doctor dbconsistency fix to delete repos with no owner
 (#27290)

to address #27273
replace #24873
---
 modules/doctor/repository.go  | 70 +++++++++++++++++++++++++++++++++++
 services/repository/delete.go | 15 +++++---
 2 files changed, 79 insertions(+), 6 deletions(-)
 create mode 100644 modules/doctor/repository.go

diff --git a/modules/doctor/repository.go b/modules/doctor/repository.go
new file mode 100644
index 0000000000..aa3f064ddd
--- /dev/null
+++ b/modules/doctor/repository.go
@@ -0,0 +1,70 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package doctor
+
+import (
+	"context"
+
+	"code.gitea.io/gitea/models/db"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/log"
+	repo_service "code.gitea.io/gitea/services/repository"
+
+	"xorm.io/builder"
+)
+
+func handleDeleteOrphanedRepos(ctx context.Context, logger log.Logger, autofix bool) error {
+	test := &consistencyCheck{
+		Name:         "Repos with no existing owner",
+		Counter:      countOrphanedRepos,
+		Fixer:        deleteOrphanedRepos,
+		FixedMessage: "Deleted all content related to orphaned repos",
+	}
+	return test.Run(ctx, logger, autofix)
+}
+
+// countOrphanedRepos count repository where user of owner_id do not exist
+func countOrphanedRepos(ctx context.Context) (int64, error) {
+	return db.CountOrphanedObjects(ctx, "repository", "user", "repository.owner_id=user.id")
+}
+
+// deleteOrphanedRepos delete repository where user of owner_id do not exist
+func deleteOrphanedRepos(ctx context.Context) (int64, error) {
+	batchSize := db.MaxBatchInsertSize("repository")
+	e := db.GetEngine(ctx)
+	var deleted int64
+	adminUser := &user_model.User{IsAdmin: true}
+
+	for {
+		var ids []int64
+		if err := e.Table("`repository`").
+			Join("LEFT", "`user`", "repository.owner_id=user.id").
+			Where(builder.IsNull{"`user`.id"}).
+			Select("`repository`.id").Limit(batchSize).Find(&ids); err != nil {
+			return deleted, err
+		}
+
+		// if we don't get ids we have deleted them all
+		if len(ids) == 0 {
+			return deleted, nil
+		}
+
+		for _, id := range ids {
+			if err := repo_service.DeleteRepositoryDirectly(ctx, adminUser, id, true); err != nil {
+				return deleted, err
+			}
+			deleted++
+		}
+	}
+}
+
+func init() {
+	Register(&Check{
+		Title:     "Deleted all content related to orphaned repos",
+		Name:      "delete-orphaned-repos",
+		IsDefault: false,
+		Run:       handleDeleteOrphanedRepos,
+		Priority:  4,
+	})
+}
diff --git a/services/repository/delete.go b/services/repository/delete.go
index 9b0a988ea3..861dfa2dcd 100644
--- a/services/repository/delete.go
+++ b/services/repository/delete.go
@@ -33,7 +33,7 @@ import (
 
 // DeleteRepository deletes a repository for a user or organization.
 // make sure if you call this func to close open sessions (sqlite will otherwise get a deadlock)
-func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID int64) error {
+func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID int64, ignoreOrgTeams ...bool) error {
 	ctx, committer, err := db.TxContext(ctx)
 	if err != nil {
 		return err
@@ -65,10 +65,13 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID
 		return fmt.Errorf("list actions artifacts of repo %v: %w", repoID, err)
 	}
 
-	// In case is a organization.
-	org, err := user_model.GetUserByID(ctx, repo.OwnerID)
-	if err != nil {
-		return err
+	// In case owner is a organization, we have to change repo specific teams
+	// if ignoreOrgTeams is not true
+	var org *user_model.User
+	if len(ignoreOrgTeams) == 0 || !ignoreOrgTeams[0] {
+		if org, err = user_model.GetUserByID(ctx, repo.OwnerID); err != nil {
+			return err
+		}
 	}
 
 	// Delete Deploy Keys
@@ -93,7 +96,7 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID
 		}
 	}
 
-	if org.IsOrganization() {
+	if org != nil && org.IsOrganization() {
 		teams, err := organization.FindOrgTeams(ctx, org.ID)
 		if err != nil {
 			return err