From 55cc649d3d293a13eee7a56ca1744577c08b4e6c Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Fri, 14 Mar 2025 11:38:55 -0700
Subject: [PATCH] Add abstraction layer to delete repository from disk (#33879)

Extract from #28966
Follow #33874
---
 modules/gitrepo/gitrepo.go      | 21 ++++++++++++++++++++-
 routers/web/repo/blame.go       |  9 +++++----
 services/repository/create.go   |  2 +-
 services/repository/delete.go   | 10 ++++++++--
 services/repository/fork.go     |  2 +-
 services/repository/transfer.go |  4 ++--
 6 files changed, 37 insertions(+), 11 deletions(-)

diff --git a/modules/gitrepo/gitrepo.go b/modules/gitrepo/gitrepo.go
index c3b412fd83..5e2ec9ed1e 100644
--- a/modules/gitrepo/gitrepo.go
+++ b/modules/gitrepo/gitrepo.go
@@ -5,6 +5,7 @@ package gitrepo
 
 import (
 	"context"
+	"fmt"
 	"io"
 	"path/filepath"
 	"strings"
@@ -20,8 +21,12 @@ type Repository interface {
 	GetOwnerName() string
 }
 
+func absPath(owner, name string) string {
+	return filepath.Join(setting.RepoRootPath, strings.ToLower(owner), strings.ToLower(name)+".git")
+}
+
 func repoPath(repo Repository) string {
-	return filepath.Join(setting.RepoRootPath, strings.ToLower(repo.GetOwnerName()), strings.ToLower(repo.GetName())+".git")
+	return absPath(repo.GetOwnerName(), repo.GetName())
 }
 
 func wikiPath(repo Repository) string {
@@ -74,3 +79,17 @@ func RepositoryFromRequestContextOrOpen(ctx reqctx.RequestContext, repo Reposito
 func IsRepositoryExist(ctx context.Context, repo Repository) (bool, error) {
 	return util.IsExist(repoPath(repo))
 }
+
+// DeleteRepository deletes the repository directory from the disk
+func DeleteRepository(ctx context.Context, repo Repository) error {
+	return util.RemoveAll(repoPath(repo))
+}
+
+// RenameRepository renames a repository's name on disk
+func RenameRepository(ctx context.Context, repo Repository, newName string) error {
+	newRepoPath := absPath(repo.GetOwnerName(), newName)
+	if err := util.Rename(repoPath(repo), newRepoPath); err != nil {
+		return fmt.Errorf("rename repository directory: %w", err)
+	}
+	return nil
+}
diff --git a/routers/web/repo/blame.go b/routers/web/repo/blame.go
index e79029a55e..2da5acd299 100644
--- a/routers/web/repo/blame.go
+++ b/routers/web/repo/blame.go
@@ -11,6 +11,7 @@ import (
 	"strconv"
 	"strings"
 
+	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/charset"
 	"code.gitea.io/gitea/modules/git"
@@ -105,7 +106,7 @@ func RefBlame(ctx *context.Context) {
 
 	bypassBlameIgnore, _ := strconv.ParseBool(ctx.FormString("bypass-blame-ignore"))
 
-	result, err := performBlame(ctx, ctx.Repo.Repository.RepoPath(), ctx.Repo.Commit, fileName, bypassBlameIgnore)
+	result, err := performBlame(ctx, ctx.Repo.Repository, ctx.Repo.Commit, fileName, bypassBlameIgnore)
 	if err != nil {
 		ctx.NotFound(err)
 		return
@@ -130,10 +131,10 @@ type blameResult struct {
 	FaultyIgnoreRevsFile bool
 }
 
-func performBlame(ctx *context.Context, repoPath string, commit *git.Commit, file string, bypassBlameIgnore bool) (*blameResult, error) {
+func performBlame(ctx *context.Context, repo *repo_model.Repository, commit *git.Commit, file string, bypassBlameIgnore bool) (*blameResult, error) {
 	objectFormat := ctx.Repo.GetObjectFormat()
 
-	blameReader, err := git.CreateBlameReader(ctx, objectFormat, repoPath, commit, file, bypassBlameIgnore)
+	blameReader, err := git.CreateBlameReader(ctx, objectFormat, repo.RepoPath(), commit, file, bypassBlameIgnore)
 	if err != nil {
 		return nil, err
 	}
@@ -149,7 +150,7 @@ func performBlame(ctx *context.Context, repoPath string, commit *git.Commit, fil
 		if len(r.Parts) == 0 && r.UsesIgnoreRevs {
 			// try again without ignored revs
 
-			blameReader, err = git.CreateBlameReader(ctx, objectFormat, repoPath, commit, file, true)
+			blameReader, err = git.CreateBlameReader(ctx, objectFormat, repo.RepoPath(), commit, file, true)
 			if err != nil {
 				return nil, err
 			}
diff --git a/services/repository/create.go b/services/repository/create.go
index 62b5931fd9..1a6a68b35a 100644
--- a/services/repository/create.go
+++ b/services/repository/create.go
@@ -277,7 +277,7 @@ func CreateRepositoryDirectly(ctx context.Context, doer, u *user_model.User, opt
 		}
 
 		if err = initRepository(ctx, doer, repo, opts); err != nil {
-			if err2 := util.RemoveAll(repo.RepoPath()); err2 != nil {
+			if err2 := gitrepo.DeleteRepository(ctx, repo); err2 != nil {
 				log.Error("initRepository: %v", err)
 				return fmt.Errorf(
 					"delete repo directory %s/%s failed(2): %v", u.Name, repo.Name, err2)
diff --git a/services/repository/delete.go b/services/repository/delete.go
index 3b953d3ec7..ff74a20817 100644
--- a/services/repository/delete.go
+++ b/services/repository/delete.go
@@ -23,6 +23,7 @@ import (
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/models/webhook"
 	actions_module "code.gitea.io/gitea/modules/actions"
+	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/lfs"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/storage"
@@ -289,8 +290,13 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID
 	// we delete the file but the database rollback, the repository will be broken.
 
 	// Remove repository files.
-	repoPath := repo.RepoPath()
-	system_model.RemoveAllWithNotice(ctx, "Delete repository files", repoPath)
+	if err := gitrepo.DeleteRepository(ctx, repo); err != nil {
+		desc := fmt.Sprintf("Delete repository files [%s]: %v", repo.FullName(), err)
+		// Note we use the db.DefaultContext here rather than passing in a context as the context may be cancelled
+		if err = system_model.CreateNotice(db.DefaultContext, system_model.NoticeRepository, desc); err != nil {
+			log.Error("CreateRepositoryNotice: %v", err)
+		}
+	}
 
 	// Remove wiki files
 	if repo.HasWiki() {
diff --git a/services/repository/fork.go b/services/repository/fork.go
index 1bd4d4a13b..ec6ba56ddf 100644
--- a/services/repository/fork.go
+++ b/services/repository/fork.go
@@ -116,7 +116,7 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork
 
 		// As the transaction will be failed and hence database changes will be destroyed we only need
 		// to delete the related repository on the filesystem
-		if errDelete := util.RemoveAll(repo.RepoPath()); errDelete != nil {
+		if errDelete := gitrepo.DeleteRepository(ctx, repo); errDelete != nil {
 			log.Error("Failed to remove fork repo")
 		}
 	}
diff --git a/services/repository/transfer.go b/services/repository/transfer.go
index bd3bf326b4..3940b2a142 100644
--- a/services/repository/transfer.go
+++ b/services/repository/transfer.go
@@ -17,6 +17,7 @@ import (
 	project_model "code.gitea.io/gitea/models/project"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/gitrepo"
 	"code.gitea.io/gitea/modules/globallock"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/util"
@@ -335,8 +336,7 @@ func changeRepositoryName(ctx context.Context, repo *repo_model.Repository, newR
 		}
 	}
 
-	newRepoPath := repo_model.RepoPath(repo.Owner.Name, newRepoName)
-	if err = util.Rename(repo.RepoPath(), newRepoPath); err != nil {
+	if err = gitrepo.RenameRepository(ctx, repo, newRepoName); err != nil {
 		return fmt.Errorf("rename repository directory: %w", err)
 	}