1
1
mirror of https://github.com/go-gitea/gitea synced 2025-07-15 23:17:19 +00:00

Rework create/fork/adopt/generate repository to make sure resources will be cleanup once failed (#31035)

Fix #28144 

To make the resources will be cleanup once failed. All repository
operations now follow a consistent pattern:

- 1. Create a database record for the repository with the status
being_migrated.
- 2. Register a deferred cleanup function to delete the repository and
its related data if the operation fails.
- 3.	Perform the actual Git and database operations step by step.
- 4. Upon successful completion, update the repository’s status to
ready.

The adopt operation is a special case — if it fails, the repository on
disk should not be deleted.
This commit is contained in:
Lunny Xiao
2025-04-07 22:12:54 -07:00
committed by GitHub
parent 90b509aafb
commit a100ac3306
14 changed files with 562 additions and 390 deletions

View File

@@ -17,6 +17,7 @@ import (
"code.gitea.io/gitea/models/perm"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
system_model "code.gitea.io/gitea/models/system"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/models/webhook"
@@ -140,8 +141,11 @@ func prepareRepoCommit(ctx context.Context, repo *repo_model.Repository, tmpDir
// InitRepository initializes README and .gitignore if needed.
func initRepository(ctx context.Context, u *user_model.User, repo *repo_model.Repository, opts CreateRepoOptions) (err error) {
if err = repo_module.CheckInitRepository(ctx, repo); err != nil {
return err
// Init git bare new repository.
if err = git.InitRepository(ctx, repo.RepoPath(), true, repo.ObjectFormatName); err != nil {
return fmt.Errorf("git.InitRepository: %w", err)
} else if err = gitrepo.CreateDelegateHooks(ctx, repo); err != nil {
return fmt.Errorf("createDelegateHooks: %w", err)
}
// Initialize repository according to user's choice.
@@ -244,100 +248,93 @@ func CreateRepositoryDirectly(ctx context.Context, doer, u *user_model.User, opt
ObjectFormatName: opts.ObjectFormatName,
}
var rollbackRepo *repo_model.Repository
if err := db.WithTx(ctx, func(ctx context.Context) error {
if err := CreateRepositoryByExample(ctx, doer, u, repo, false, false); err != nil {
return err
}
// No need for init mirror.
if opts.IsMirror {
return nil
}
isExist, err := gitrepo.IsRepositoryExist(ctx, repo)
if err != nil {
log.Error("Unable to check if %s exists. Error: %v", repo.FullName(), err)
return err
}
if isExist {
// repo already exists - We have two or three options.
// 1. We fail stating that the directory exists
// 2. We create the db repository to go with this data and adopt the git repo
// 3. We delete it and start afresh
//
// Previously Gitea would just delete and start afresh - this was naughty.
// So we will now fail and delegate to other functionality to adopt or delete
log.Error("Files already exist in %s and we are not going to adopt or delete.", repo.FullName())
return repo_model.ErrRepoFilesAlreadyExist{
Uname: u.Name,
Name: repo.Name,
}
}
if err = initRepository(ctx, doer, repo, opts); err != 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)
}
return fmt.Errorf("initRepository: %w", err)
}
// Initialize Issue Labels if selected
if len(opts.IssueLabels) > 0 {
if err = repo_module.InitializeLabels(ctx, repo.ID, opts.IssueLabels, false); err != nil {
rollbackRepo = repo
rollbackRepo.OwnerID = u.ID
return fmt.Errorf("InitializeLabels: %w", err)
}
}
if err := repo_module.CheckDaemonExportOK(ctx, repo); err != nil {
return fmt.Errorf("checkDaemonExportOK: %w", err)
}
if stdout, _, err := git.NewCommand("update-server-info").
RunStdString(ctx, &git.RunOpts{Dir: repo.RepoPath()}); err != nil {
log.Error("CreateRepository(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err)
rollbackRepo = repo
rollbackRepo.OwnerID = u.ID
return fmt.Errorf("CreateRepository(git update-server-info): %w", err)
}
// update licenses
var licenses []string
if len(opts.License) > 0 {
licenses = append(licenses, opts.License)
stdout, _, err := git.NewCommand("rev-parse", "HEAD").RunStdString(ctx, &git.RunOpts{Dir: repo.RepoPath()})
if err != nil {
log.Error("CreateRepository(git rev-parse HEAD) in %v: Stdout: %s\nError: %v", repo, stdout, err)
rollbackRepo = repo
rollbackRepo.OwnerID = u.ID
return fmt.Errorf("CreateRepository(git rev-parse HEAD): %w", err)
}
if err := repo_model.UpdateRepoLicenses(ctx, repo, stdout, licenses); err != nil {
return err
}
}
return nil
}); err != nil {
if rollbackRepo != nil {
if errDelete := DeleteRepositoryDirectly(ctx, doer, rollbackRepo.ID); errDelete != nil {
log.Error("Rollback deleteRepository: %v", errDelete)
}
}
needsUpdateStatus := opts.Status != repo_model.RepositoryReady
// 1 - create the repository database operations first
err := db.WithTx(ctx, func(ctx context.Context) error {
return createRepositoryInDB(ctx, doer, u, repo, false)
})
if err != nil {
return nil, err
}
// last - clean up if something goes wrong
// WARNING: Don't override all later err with local variables
defer func() {
if err != nil {
// we can not use the ctx because it maybe canceled or timeout
cleanupRepository(doer, repo.ID)
}
}()
// No need for init mirror.
if opts.IsMirror {
return repo, nil
}
// 2 - check whether the repository with the same storage exists
var isExist bool
isExist, err = gitrepo.IsRepositoryExist(ctx, repo)
if err != nil {
log.Error("Unable to check if %s exists. Error: %v", repo.FullName(), err)
return nil, err
}
if isExist {
log.Error("Files already exist in %s and we are not going to adopt or delete.", repo.FullName())
// Don't return directly, we need err in defer to cleanupRepository
err = repo_model.ErrRepoFilesAlreadyExist{
Uname: repo.OwnerName,
Name: repo.Name,
}
return nil, err
}
// 3 - init git repository in storage
if err = initRepository(ctx, doer, repo, opts); err != nil {
return nil, fmt.Errorf("initRepository: %w", err)
}
// 4 - Initialize Issue Labels if selected
if len(opts.IssueLabels) > 0 {
if err = repo_module.InitializeLabels(ctx, repo.ID, opts.IssueLabels, false); err != nil {
return nil, fmt.Errorf("InitializeLabels: %w", err)
}
}
// 5 - Update the git repository
if err = updateGitRepoAfterCreate(ctx, repo); err != nil {
return nil, fmt.Errorf("updateGitRepoAfterCreate: %w", err)
}
// 6 - update licenses
var licenses []string
if len(opts.License) > 0 {
licenses = append(licenses, opts.License)
var stdout string
stdout, _, err = git.NewCommand("rev-parse", "HEAD").RunStdString(ctx, &git.RunOpts{Dir: repo.RepoPath()})
if err != nil {
log.Error("CreateRepository(git rev-parse HEAD) in %v: Stdout: %s\nError: %v", repo, stdout, err)
return nil, fmt.Errorf("CreateRepository(git rev-parse HEAD): %w", err)
}
if err = repo_model.UpdateRepoLicenses(ctx, repo, stdout, licenses); err != nil {
return nil, err
}
}
// 7 - update repository status to be ready
if needsUpdateStatus {
repo.Status = repo_model.RepositoryReady
if err = repo_model.UpdateRepositoryCols(ctx, repo, "status"); err != nil {
return nil, fmt.Errorf("UpdateRepositoryCols: %w", err)
}
}
return repo, nil
}
// CreateRepositoryByExample creates a repository for the user/organization.
func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository, overwriteOrAdopt, isFork bool) (err error) {
// createRepositoryInDB creates a repository for the user/organization.
func createRepositoryInDB(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository, isFork bool) (err error) {
if err = repo_model.IsUsableRepoName(repo.Name); err != nil {
return err
}
@@ -352,19 +349,6 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re
}
}
isExist, err := gitrepo.IsRepositoryExist(ctx, repo)
if err != nil {
log.Error("Unable to check if %s exists. Error: %v", repo.FullName(), err)
return err
}
if !overwriteOrAdopt && isExist {
log.Error("Files already exist in %s and we are not going to adopt or delete.", repo.FullName())
return repo_model.ErrRepoFilesAlreadyExist{
Uname: u.Name,
Name: repo.Name,
}
}
if err = db.Insert(ctx, repo); err != nil {
return err
}
@@ -473,3 +457,26 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re
return nil
}
func cleanupRepository(doer *user_model.User, repoID int64) {
if errDelete := DeleteRepositoryDirectly(db.DefaultContext, doer, repoID); errDelete != nil {
log.Error("cleanupRepository failed: %v", errDelete)
// add system notice
if err := system_model.CreateRepositoryNotice("DeleteRepositoryDirectly failed when cleanup repository: %v", errDelete); err != nil {
log.Error("CreateRepositoryNotice: %v", err)
}
}
}
func updateGitRepoAfterCreate(ctx context.Context, repo *repo_model.Repository) error {
if err := repo_module.CheckDaemonExportOK(ctx, repo); err != nil {
return fmt.Errorf("checkDaemonExportOK: %w", err)
}
if stdout, _, err := git.NewCommand("update-server-info").
RunStdString(ctx, &git.RunOpts{Dir: repo.RepoPath()}); err != nil {
log.Error("CreateRepository(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err)
return fmt.Errorf("CreateRepository(git update-server-info): %w", err)
}
return nil
}