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:
@@ -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
|
||||
}
|
||||
|
Reference in New Issue
Block a user