mirror of
https://github.com/go-gitea/gitea.git
synced 2025-04-14 21:27:46 +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:
parent
90b509aafb
commit
a100ac3306
@ -235,6 +235,11 @@ func GetDeletedBranchByID(ctx context.Context, repoID, branchID int64) (*Branch,
|
||||
return &branch, nil
|
||||
}
|
||||
|
||||
func DeleteRepoBranches(ctx context.Context, repoID int64) error {
|
||||
_, err := db.GetEngine(ctx).Where("repo_id=?", repoID).Delete(new(Branch))
|
||||
return err
|
||||
}
|
||||
|
||||
func DeleteBranches(ctx context.Context, repoID, doerID int64, branchIDs []int64) error {
|
||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||
branches := make([]*Branch, 0, len(branchIDs))
|
||||
|
@ -558,3 +558,8 @@ func FindTagsByCommitIDs(ctx context.Context, repoID int64, commitIDs ...string)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func DeleteRepoReleases(ctx context.Context, repoID int64) error {
|
||||
_, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Delete(new(Release))
|
||||
return err
|
||||
}
|
||||
|
@ -11,11 +11,7 @@ import (
|
||||
"strings"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/label"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/options"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
@ -121,29 +117,6 @@ func LoadRepoConfig() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func CheckInitRepository(ctx context.Context, repo *repo_model.Repository) (err error) {
|
||||
// Somehow the directory could exist.
|
||||
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 {
|
||||
return repo_model.ErrRepoFilesAlreadyExist{
|
||||
Uname: repo.OwnerName,
|
||||
Name: repo.Name,
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// InitializeLabels adds a label set to a repository using a template
|
||||
func InitializeLabels(ctx context.Context, id int64, labelTemplate string, isOrg bool) error {
|
||||
list, err := LoadTemplateLabelsByDisplayName(labelTemplate)
|
||||
|
@ -16,7 +16,6 @@ import (
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
@ -28,6 +27,18 @@ import (
|
||||
"github.com/gobwas/glob"
|
||||
)
|
||||
|
||||
func deleteFailedAdoptRepository(repoID int64) error {
|
||||
return db.WithTx(db.DefaultContext, func(ctx context.Context) error {
|
||||
if err := deleteDBRepository(ctx, repoID); err != nil {
|
||||
return fmt.Errorf("deleteDBRepository: %w", err)
|
||||
}
|
||||
if err := git_model.DeleteRepoBranches(ctx, repoID); err != nil {
|
||||
return fmt.Errorf("deleteRepoBranches: %w", err)
|
||||
}
|
||||
return repo_model.DeleteRepoReleases(ctx, repoID)
|
||||
})
|
||||
}
|
||||
|
||||
// AdoptRepository adopts pre-existing repository files for the user/organization.
|
||||
func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts CreateRepoOptions) (*repo_model.Repository, error) {
|
||||
if !doer.IsAdmin && !u.CanCreateRepo() {
|
||||
@ -48,58 +59,51 @@ func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts CreateR
|
||||
IsPrivate: opts.IsPrivate,
|
||||
IsFsckEnabled: !opts.IsMirror,
|
||||
CloseIssuesViaCommitInAnyBranch: setting.Repository.DefaultCloseIssuesViaCommitsInAnyBranch,
|
||||
Status: opts.Status,
|
||||
Status: repo_model.RepositoryBeingMigrated,
|
||||
IsEmpty: !opts.AutoInit,
|
||||
}
|
||||
|
||||
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
isExist, err := gitrepo.IsRepositoryExist(ctx, repo)
|
||||
// 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 {
|
||||
log.Error("Unable to check if %s exists. Error: %v", repo.FullName(), err)
|
||||
return err
|
||||
}
|
||||
if !isExist {
|
||||
return repo_model.ErrRepoNotExist{
|
||||
OwnerName: u.Name,
|
||||
Name: repo.Name,
|
||||
// we can not use the ctx because it maybe canceled or timeout
|
||||
if errDel := deleteFailedAdoptRepository(repo.ID); errDel != nil {
|
||||
log.Error("Failed to delete repository %s that could not be adopted: %v", repo.FullName(), errDel)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err := CreateRepositoryByExample(ctx, doer, u, repo, true, false); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Re-fetch the repository from database before updating it (else it would
|
||||
// override changes that were done earlier with sql)
|
||||
if repo, err = repo_model.GetRepositoryByID(ctx, repo.ID); err != nil {
|
||||
return fmt.Errorf("getRepositoryByID: %w", err)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
// Re-fetch the repository from database before updating it (else it would
|
||||
// override changes that were done earlier with sql)
|
||||
if repo, err = repo_model.GetRepositoryByID(ctx, repo.ID); err != nil {
|
||||
return nil, fmt.Errorf("getRepositoryByID: %w", err)
|
||||
}
|
||||
|
||||
if err := func() error {
|
||||
if err := adoptRepository(ctx, repo, opts.DefaultBranch); err != nil {
|
||||
return fmt.Errorf("adoptRepository: %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)
|
||||
return fmt.Errorf("CreateRepository(git update-server-info): %w", err)
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
if errDel := DeleteRepository(ctx, doer, repo, false /* no notify */); errDel != nil {
|
||||
log.Error("Failed to delete repository %s that could not be adopted: %v", repo.FullName(), errDel)
|
||||
}
|
||||
return nil, err
|
||||
// 2 - adopt the repository from disk
|
||||
if err = adoptRepository(ctx, repo, opts.DefaultBranch); err != nil {
|
||||
return nil, fmt.Errorf("adoptRepository: %w", err)
|
||||
}
|
||||
|
||||
// 3 - Update the git repository
|
||||
if err = updateGitRepoAfterCreate(ctx, repo); err != nil {
|
||||
return nil, fmt.Errorf("updateGitRepoAfterCreate: %w", err)
|
||||
}
|
||||
|
||||
// 4 - update repository status
|
||||
repo.Status = repo_model.RepositoryReady
|
||||
if err = repo_model.UpdateRepositoryCols(ctx, repo, "status"); err != nil {
|
||||
return nil, fmt.Errorf("UpdateRepositoryCols: %w", err)
|
||||
}
|
||||
|
||||
notify_service.AdoptRepository(ctx, doer, u, repo)
|
||||
|
||||
return repo, nil
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@ -89,10 +90,36 @@ func TestListUnadoptedRepositories_ListOptions(t *testing.T) {
|
||||
|
||||
func TestAdoptRepository(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
assert.NoError(t, unittest.SyncDirs(filepath.Join(setting.RepoRootPath, "user2", "repo1.git"), filepath.Join(setting.RepoRootPath, "user2", "test-adopt.git")))
|
||||
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
_, err := AdoptRepository(db.DefaultContext, user2, user2, CreateRepoOptions{Name: "test-adopt"})
|
||||
|
||||
// a successful adopt
|
||||
destDir := filepath.Join(setting.RepoRootPath, user2.Name, "test-adopt.git")
|
||||
assert.NoError(t, unittest.SyncDirs(filepath.Join(setting.RepoRootPath, user2.Name, "repo1.git"), destDir))
|
||||
|
||||
adoptedRepo, err := AdoptRepository(db.DefaultContext, user2, user2, CreateRepoOptions{Name: "test-adopt"})
|
||||
assert.NoError(t, err)
|
||||
repoTestAdopt := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: "test-adopt"})
|
||||
assert.Equal(t, "sha1", repoTestAdopt.ObjectFormatName)
|
||||
|
||||
// just delete the adopted repo's db records
|
||||
err = deleteFailedAdoptRepository(adoptedRepo.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
unittest.AssertNotExistsBean(t, &repo_model.Repository{OwnerName: user2.Name, Name: "test-adopt"})
|
||||
|
||||
// a failed adopt because some mock data
|
||||
// remove the hooks directory and create a file so that we cannot create the hooks successfully
|
||||
_ = os.RemoveAll(filepath.Join(destDir, "hooks", "update.d"))
|
||||
assert.NoError(t, os.WriteFile(filepath.Join(destDir, "hooks", "update.d"), []byte("tests"), os.ModePerm))
|
||||
|
||||
adoptedRepo, err = AdoptRepository(db.DefaultContext, user2, user2, CreateRepoOptions{Name: "test-adopt"})
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, adoptedRepo)
|
||||
|
||||
unittest.AssertNotExistsBean(t, &repo_model.Repository{OwnerName: user2.Name, Name: "test-adopt"})
|
||||
|
||||
exist, err := util.IsExist(repo_model.RepoPath(user2.Name, "test-adopt"))
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exist) // the repository should be still in the disk
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
57
services/repository/create_test.go
Normal file
57
services/repository/create_test.go
Normal file
@ -0,0 +1,57 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repository
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCreateRepositoryDirectly(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
// a successful creating repository
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
createdRepo, err := CreateRepositoryDirectly(git.DefaultContext, user2, user2, CreateRepoOptions{
|
||||
Name: "created-repo",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, createdRepo)
|
||||
|
||||
exist, err := util.IsExist(repo_model.RepoPath(user2.Name, createdRepo.Name))
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exist)
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: user2.Name, Name: createdRepo.Name})
|
||||
|
||||
err = DeleteRepositoryDirectly(db.DefaultContext, user2, createdRepo.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// a failed creating because some mock data
|
||||
// create the repository directory so that the creation will fail after database record created.
|
||||
assert.NoError(t, os.MkdirAll(repo_model.RepoPath(user2.Name, createdRepo.Name), os.ModePerm))
|
||||
|
||||
createdRepo2, err := CreateRepositoryDirectly(db.DefaultContext, user2, user2, CreateRepoOptions{
|
||||
Name: "created-repo",
|
||||
})
|
||||
assert.Nil(t, createdRepo2)
|
||||
assert.Error(t, err)
|
||||
|
||||
// assert the cleanup is successful
|
||||
unittest.AssertNotExistsBean(t, &repo_model.Repository{OwnerName: user2.Name, Name: createdRepo.Name})
|
||||
|
||||
exist, err = util.IsExist(repo_model.RepoPath(user2.Name, createdRepo.Name))
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, exist)
|
||||
}
|
@ -32,6 +32,19 @@ import (
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
func deleteDBRepository(ctx context.Context, repoID int64) error {
|
||||
if cnt, err := db.GetEngine(ctx).ID(repoID).Delete(&repo_model.Repository{}); err != nil {
|
||||
return err
|
||||
} else if cnt != 1 {
|
||||
return repo_model.ErrRepoNotExist{
|
||||
ID: repoID,
|
||||
OwnerName: "",
|
||||
Name: "",
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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, ignoreOrgTeams ...bool) error {
|
||||
@ -82,14 +95,8 @@ func DeleteRepositoryDirectly(ctx context.Context, doer *user_model.User, repoID
|
||||
}
|
||||
needRewriteKeysFile := deleted > 0
|
||||
|
||||
if cnt, err := sess.ID(repoID).Delete(&repo_model.Repository{}); err != nil {
|
||||
if err := deleteDBRepository(ctx, repoID); err != nil {
|
||||
return err
|
||||
} else if cnt != 1 {
|
||||
return repo_model.ErrRepoNotExist{
|
||||
ID: repoID,
|
||||
OwnerName: "",
|
||||
Name: "",
|
||||
}
|
||||
}
|
||||
|
||||
if org != nil && org.IsOrganization() {
|
||||
|
@ -100,114 +100,106 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork
|
||||
IsFork: true,
|
||||
ForkID: opts.BaseRepo.ID,
|
||||
ObjectFormatName: opts.BaseRepo.ObjectFormatName,
|
||||
Status: repo_model.RepositoryBeingMigrated,
|
||||
}
|
||||
|
||||
oldRepoPath := opts.BaseRepo.RepoPath()
|
||||
|
||||
needsRollback := false
|
||||
rollbackFn := func() {
|
||||
if !needsRollback {
|
||||
return
|
||||
}
|
||||
|
||||
if exists, _ := gitrepo.IsRepositoryExist(ctx, repo); !exists {
|
||||
return
|
||||
}
|
||||
|
||||
// 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 := gitrepo.DeleteRepository(ctx, repo); errDelete != nil {
|
||||
log.Error("Failed to remove fork repo")
|
||||
}
|
||||
}
|
||||
|
||||
needsRollbackInPanic := true
|
||||
defer func() {
|
||||
panicErr := recover()
|
||||
if panicErr == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if needsRollbackInPanic {
|
||||
rollbackFn()
|
||||
}
|
||||
panic(panicErr)
|
||||
}()
|
||||
|
||||
err = db.WithTx(ctx, func(txCtx context.Context) error {
|
||||
if err = CreateRepositoryByExample(txCtx, doer, owner, repo, false, true); err != nil {
|
||||
// 1 - Create the repository in the database
|
||||
err = db.WithTx(ctx, func(ctx context.Context) error {
|
||||
if err = createRepositoryInDB(ctx, doer, owner, repo, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = repo_model.IncrementRepoForkNum(txCtx, opts.BaseRepo.ID); err != nil {
|
||||
if err = repo_model.IncrementRepoForkNum(ctx, opts.BaseRepo.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// copy lfs files failure should not be ignored
|
||||
if err = git_model.CopyLFS(txCtx, repo, opts.BaseRepo); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
needsRollback = true
|
||||
|
||||
cloneCmd := git.NewCommand("clone", "--bare")
|
||||
if opts.SingleBranch != "" {
|
||||
cloneCmd.AddArguments("--single-branch", "--branch").AddDynamicArguments(opts.SingleBranch)
|
||||
}
|
||||
if stdout, _, err := cloneCmd.AddDynamicArguments(oldRepoPath, repo.RepoPath()).
|
||||
RunStdBytes(txCtx, &git.RunOpts{Timeout: 10 * time.Minute}); err != nil {
|
||||
log.Error("Fork Repository (git clone) Failed for %v (from %v):\nStdout: %s\nError: %v", repo, opts.BaseRepo, stdout, err)
|
||||
return fmt.Errorf("git clone: %w", err)
|
||||
}
|
||||
|
||||
if err := repo_module.CheckDaemonExportOK(txCtx, repo); err != nil {
|
||||
return fmt.Errorf("checkDaemonExportOK: %w", err)
|
||||
}
|
||||
|
||||
if stdout, _, err := git.NewCommand("update-server-info").
|
||||
RunStdString(txCtx, &git.RunOpts{Dir: repo.RepoPath()}); err != nil {
|
||||
log.Error("Fork Repository (git update-server-info) failed for %v:\nStdout: %s\nError: %v", repo, stdout, err)
|
||||
return fmt.Errorf("git update-server-info: %w", err)
|
||||
}
|
||||
|
||||
if err = gitrepo.CreateDelegateHooks(ctx, repo); err != nil {
|
||||
return fmt.Errorf("createDelegateHooks: %w", err)
|
||||
}
|
||||
|
||||
gitRepo, err := gitrepo.OpenRepository(txCtx, repo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("OpenRepository: %w", err)
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
_, err = repo_module.SyncRepoBranchesWithRepo(txCtx, repo, gitRepo, doer.ID)
|
||||
return err
|
||||
return git_model.CopyLFS(ctx, repo, opts.BaseRepo)
|
||||
})
|
||||
needsRollbackInPanic = false
|
||||
if err != nil {
|
||||
rollbackFn()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// even if below operations failed, it could be ignored. And they will be retried
|
||||
if err := repo_module.UpdateRepoSize(ctx, repo); err != nil {
|
||||
log.Error("Failed to update size for repository: %v", err)
|
||||
}
|
||||
if err := repo_model.CopyLanguageStat(ctx, opts.BaseRepo, repo); err != nil {
|
||||
log.Error("Copy language stat from oldRepo failed: %v", err)
|
||||
}
|
||||
if err := repo_model.CopyLicense(ctx, opts.BaseRepo, repo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gitRepo, err := gitrepo.OpenRepository(ctx, repo)
|
||||
if err != nil {
|
||||
log.Error("Open created git repository failed: %v", err)
|
||||
} else {
|
||||
defer gitRepo.Close()
|
||||
if err := repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil {
|
||||
log.Error("Sync releases from git tags failed: %v", 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)
|
||||
}
|
||||
}()
|
||||
|
||||
// 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 - Clone the repository
|
||||
cloneCmd := git.NewCommand("clone", "--bare")
|
||||
if opts.SingleBranch != "" {
|
||||
cloneCmd.AddArguments("--single-branch", "--branch").AddDynamicArguments(opts.SingleBranch)
|
||||
}
|
||||
var stdout []byte
|
||||
if stdout, _, err = cloneCmd.AddDynamicArguments(opts.BaseRepo.RepoPath(), repo.RepoPath()).
|
||||
RunStdBytes(ctx, &git.RunOpts{Timeout: 10 * time.Minute}); err != nil {
|
||||
log.Error("Fork Repository (git clone) Failed for %v (from %v):\nStdout: %s\nError: %v", repo, opts.BaseRepo, stdout, err)
|
||||
return nil, fmt.Errorf("git clone: %w", err)
|
||||
}
|
||||
|
||||
// 4 - Update the git repository
|
||||
if err = updateGitRepoAfterCreate(ctx, repo); err != nil {
|
||||
return nil, fmt.Errorf("updateGitRepoAfterCreate: %w", err)
|
||||
}
|
||||
|
||||
// 5 - Create hooks
|
||||
if err = gitrepo.CreateDelegateHooks(ctx, repo); err != nil {
|
||||
return nil, fmt.Errorf("createDelegateHooks: %w", err)
|
||||
}
|
||||
|
||||
// 6 - Sync the repository branches and tags
|
||||
var gitRepo *git.Repository
|
||||
gitRepo, err = gitrepo.OpenRepository(ctx, repo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("OpenRepository: %w", err)
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
|
||||
if _, err = repo_module.SyncRepoBranchesWithRepo(ctx, repo, gitRepo, doer.ID); err != nil {
|
||||
return nil, fmt.Errorf("SyncRepoBranchesWithRepo: %w", err)
|
||||
}
|
||||
if err = repo_module.SyncReleasesWithTags(ctx, repo, gitRepo); err != nil {
|
||||
return nil, fmt.Errorf("Sync releases from git tags failed: %v", err)
|
||||
}
|
||||
|
||||
// 7 - Update the repository
|
||||
// even if below operations failed, it could be ignored. And they will be retried
|
||||
if err = repo_module.UpdateRepoSize(ctx, repo); err != nil {
|
||||
log.Error("Failed to update size for repository: %v", err)
|
||||
err = nil
|
||||
}
|
||||
if err = repo_model.CopyLanguageStat(ctx, opts.BaseRepo, repo); err != nil {
|
||||
log.Error("Copy language stat from oldRepo failed: %v", err)
|
||||
err = nil
|
||||
}
|
||||
if err = repo_model.CopyLicense(ctx, opts.BaseRepo, repo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 8 - update repository status to be ready
|
||||
repo.Status = repo_model.RepositoryReady
|
||||
if err = repo_model.UpdateRepositoryCols(ctx, repo, "status"); err != nil {
|
||||
return nil, fmt.Errorf("UpdateRepositoryCols: %w", err)
|
||||
}
|
||||
|
||||
notify_service.ForkRepository(ctx, doer, opts.BaseRepo, repo)
|
||||
|
@ -4,13 +4,16 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@ -46,3 +49,43 @@ func TestForkRepository(t *testing.T) {
|
||||
assert.Nil(t, fork2)
|
||||
assert.True(t, repo_model.IsErrReachLimitOfRepo(err))
|
||||
}
|
||||
|
||||
func TestForkRepositoryCleanup(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
// a successful fork
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
repo10 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 10})
|
||||
|
||||
fork, err := ForkRepository(git.DefaultContext, user2, user2, ForkRepoOptions{
|
||||
BaseRepo: repo10,
|
||||
Name: "test",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, fork)
|
||||
|
||||
exist, err := util.IsExist(repo_model.RepoPath(user2.Name, "test"))
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exist)
|
||||
|
||||
err = DeleteRepositoryDirectly(db.DefaultContext, user2, fork.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// a failed creating because some mock data
|
||||
// create the repository directory so that the creation will fail after database record created.
|
||||
assert.NoError(t, os.MkdirAll(repo_model.RepoPath(user2.Name, "test"), os.ModePerm))
|
||||
|
||||
fork2, err := ForkRepository(db.DefaultContext, user2, user2, ForkRepoOptions{
|
||||
BaseRepo: repo10,
|
||||
Name: "test",
|
||||
})
|
||||
assert.Nil(t, fork2)
|
||||
assert.Error(t, err)
|
||||
|
||||
// assert the cleanup is successful
|
||||
unittest.AssertNotExistsBean(t, &repo_model.Repository{OwnerName: user2.Name, Name: "test"})
|
||||
|
||||
exist, err = util.IsExist(repo_model.RepoPath(user2.Name, "test"))
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, exist)
|
||||
}
|
||||
|
@ -17,7 +17,6 @@ import (
|
||||
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@ -328,57 +327,6 @@ func (gro GenerateRepoOptions) IsValid() bool {
|
||||
gro.IssueLabels || gro.ProtectedBranch // or other items as they are added
|
||||
}
|
||||
|
||||
// generateRepository generates a repository from a template
|
||||
func generateRepository(ctx context.Context, doer, owner *user_model.User, templateRepo *repo_model.Repository, opts GenerateRepoOptions) (_ *repo_model.Repository, err error) {
|
||||
generateRepo := &repo_model.Repository{
|
||||
OwnerID: owner.ID,
|
||||
Owner: owner,
|
||||
OwnerName: owner.Name,
|
||||
Name: opts.Name,
|
||||
LowerName: strings.ToLower(opts.Name),
|
||||
Description: opts.Description,
|
||||
DefaultBranch: opts.DefaultBranch,
|
||||
IsPrivate: opts.Private,
|
||||
IsEmpty: !opts.GitContent || templateRepo.IsEmpty,
|
||||
IsFsckEnabled: templateRepo.IsFsckEnabled,
|
||||
TemplateID: templateRepo.ID,
|
||||
TrustModel: templateRepo.TrustModel,
|
||||
ObjectFormatName: templateRepo.ObjectFormatName,
|
||||
}
|
||||
|
||||
if err = CreateRepositoryByExample(ctx, doer, owner, generateRepo, false, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isExist, err := gitrepo.IsRepositoryExist(ctx, generateRepo)
|
||||
if err != nil {
|
||||
log.Error("Unable to check if %s exists. Error: %v", generateRepo.FullName(), err)
|
||||
return nil, err
|
||||
}
|
||||
if isExist {
|
||||
return nil, repo_model.ErrRepoFilesAlreadyExist{
|
||||
Uname: generateRepo.OwnerName,
|
||||
Name: generateRepo.Name,
|
||||
}
|
||||
}
|
||||
|
||||
if err = repo_module.CheckInitRepository(ctx, generateRepo); err != nil {
|
||||
return generateRepo, err
|
||||
}
|
||||
|
||||
if err = repo_module.CheckDaemonExportOK(ctx, generateRepo); err != nil {
|
||||
return generateRepo, fmt.Errorf("checkDaemonExportOK: %w", err)
|
||||
}
|
||||
|
||||
if stdout, _, err := git.NewCommand("update-server-info").
|
||||
RunStdString(ctx, &git.RunOpts{Dir: generateRepo.RepoPath()}); err != nil {
|
||||
log.Error("GenerateRepository(git update-server-info) in %v: Stdout: %s\nError: %v", generateRepo, stdout, err)
|
||||
return generateRepo, fmt.Errorf("error in GenerateRepository(git update-server-info): %w", err)
|
||||
}
|
||||
|
||||
return generateRepo, nil
|
||||
}
|
||||
|
||||
var fileNameSanitizeRegexp = regexp.MustCompile(`(?i)\.\.|[<>:\"/\\|?*\x{0000}-\x{001F}]|^(con|prn|aux|nul|com\d|lpt\d)$`)
|
||||
|
||||
// Sanitize user input to valid OS filenames
|
||||
|
@ -118,14 +118,8 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
|
||||
repo.Owner = u
|
||||
}
|
||||
|
||||
if err := repo_module.CheckDaemonExportOK(ctx, repo); err != nil {
|
||||
return repo, fmt.Errorf("checkDaemonExportOK: %w", err)
|
||||
}
|
||||
|
||||
if stdout, _, err := git.NewCommand("update-server-info").
|
||||
RunStdString(ctx, &git.RunOpts{Dir: repoPath}); err != nil {
|
||||
log.Error("MigrateRepositoryGitData(git update-server-info) in %v: Stdout: %s\nError: %v", repo, stdout, err)
|
||||
return repo, fmt.Errorf("error in MigrateRepositoryGitData(git update-server-info): %w", err)
|
||||
if err := updateGitRepoAfterCreate(ctx, repo); err != nil {
|
||||
return nil, fmt.Errorf("updateGitRepoAfterCreate: %w", err)
|
||||
}
|
||||
|
||||
gitRepo, err := git.OpenRepository(ctx, repoPath)
|
||||
|
@ -5,12 +5,17 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
notify_service "code.gitea.io/gitea/services/notify"
|
||||
)
|
||||
|
||||
@ -69,66 +74,120 @@ func GenerateRepository(ctx context.Context, doer, owner *user_model.User, templ
|
||||
}
|
||||
}
|
||||
|
||||
var generateRepo *repo_model.Repository
|
||||
if err = db.WithTx(ctx, func(ctx context.Context) error {
|
||||
generateRepo, err = generateRepository(ctx, doer, owner, templateRepo, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
generateRepo := &repo_model.Repository{
|
||||
OwnerID: owner.ID,
|
||||
Owner: owner,
|
||||
OwnerName: owner.Name,
|
||||
Name: opts.Name,
|
||||
LowerName: strings.ToLower(opts.Name),
|
||||
Description: opts.Description,
|
||||
DefaultBranch: opts.DefaultBranch,
|
||||
IsPrivate: opts.Private,
|
||||
IsEmpty: !opts.GitContent || templateRepo.IsEmpty,
|
||||
IsFsckEnabled: templateRepo.IsFsckEnabled,
|
||||
TemplateID: templateRepo.ID,
|
||||
TrustModel: templateRepo.TrustModel,
|
||||
ObjectFormatName: templateRepo.ObjectFormatName,
|
||||
Status: repo_model.RepositoryBeingMigrated,
|
||||
}
|
||||
|
||||
// Git Content
|
||||
if opts.GitContent && !templateRepo.IsEmpty {
|
||||
if err = GenerateGitContent(ctx, templateRepo, generateRepo); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Topics
|
||||
if opts.Topics {
|
||||
if err = repo_model.GenerateTopics(ctx, templateRepo, generateRepo); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Git Hooks
|
||||
if opts.GitHooks {
|
||||
if err = GenerateGitHooks(ctx, templateRepo, generateRepo); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Webhooks
|
||||
if opts.Webhooks {
|
||||
if err = GenerateWebhooks(ctx, templateRepo, generateRepo); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Avatar
|
||||
if opts.Avatar && len(templateRepo.Avatar) > 0 {
|
||||
if err = generateAvatar(ctx, templateRepo, generateRepo); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Issue Labels
|
||||
if opts.IssueLabels {
|
||||
if err = GenerateIssueLabels(ctx, templateRepo, generateRepo); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if opts.ProtectedBranch {
|
||||
if err = GenerateProtectedBranch(ctx, templateRepo, generateRepo); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
// 1 - Create the repository in the database
|
||||
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
return createRepositoryInDB(ctx, doer, owner, generateRepo, false)
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// last - clean up the repository if something goes wrong
|
||||
defer func() {
|
||||
if err != nil {
|
||||
// we can not use the ctx because it maybe canceled or timeout
|
||||
cleanupRepository(doer, generateRepo.ID)
|
||||
}
|
||||
}()
|
||||
|
||||
// 2 - check whether the repository with the same storage exists
|
||||
isExist, err := gitrepo.IsRepositoryExist(ctx, generateRepo)
|
||||
if err != nil {
|
||||
log.Error("Unable to check if %s exists. Error: %v", generateRepo.FullName(), err)
|
||||
return nil, err
|
||||
}
|
||||
if isExist {
|
||||
// Don't return directly, we need err in defer to cleanupRepository
|
||||
err = repo_model.ErrRepoFilesAlreadyExist{
|
||||
Uname: generateRepo.OwnerName,
|
||||
Name: generateRepo.Name,
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3 -Init git bare new repository.
|
||||
if err = git.InitRepository(ctx, generateRepo.RepoPath(), true, generateRepo.ObjectFormatName); err != nil {
|
||||
return nil, fmt.Errorf("git.InitRepository: %w", err)
|
||||
} else if err = gitrepo.CreateDelegateHooks(ctx, generateRepo); err != nil {
|
||||
return nil, fmt.Errorf("createDelegateHooks: %w", err)
|
||||
}
|
||||
|
||||
// 4 - Update the git repository
|
||||
if err = updateGitRepoAfterCreate(ctx, generateRepo); err != nil {
|
||||
return nil, fmt.Errorf("updateGitRepoAfterCreate: %w", err)
|
||||
}
|
||||
|
||||
// 5 - generate the repository contents according to the template
|
||||
// Git Content
|
||||
if opts.GitContent && !templateRepo.IsEmpty {
|
||||
if err = GenerateGitContent(ctx, templateRepo, generateRepo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Topics
|
||||
if opts.Topics {
|
||||
if err = repo_model.GenerateTopics(ctx, templateRepo, generateRepo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Git Hooks
|
||||
if opts.GitHooks {
|
||||
if err = GenerateGitHooks(ctx, templateRepo, generateRepo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Webhooks
|
||||
if opts.Webhooks {
|
||||
if err = GenerateWebhooks(ctx, templateRepo, generateRepo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Avatar
|
||||
if opts.Avatar && len(templateRepo.Avatar) > 0 {
|
||||
if err = generateAvatar(ctx, templateRepo, generateRepo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Issue Labels
|
||||
if opts.IssueLabels {
|
||||
if err = GenerateIssueLabels(ctx, templateRepo, generateRepo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if opts.ProtectedBranch {
|
||||
if err = GenerateProtectedBranch(ctx, templateRepo, generateRepo); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 6 - update repository status to be ready
|
||||
generateRepo.Status = repo_model.RepositoryReady
|
||||
if err = repo_model.UpdateRepositoryCols(ctx, generateRepo, "status"); err != nil {
|
||||
return nil, fmt.Errorf("UpdateRepositoryCols: %w", err)
|
||||
}
|
||||
|
||||
notify_service.CreateRepository(ctx, doer, owner, generateRepo)
|
||||
|
||||
return generateRepo, nil
|
||||
|
@ -6,15 +6,23 @@ package integration
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/PuerkitoBio/goquery"
|
||||
@ -493,3 +501,46 @@ func testViewCommit(t *testing.T) {
|
||||
resp := MakeRequest(t, req, http.StatusNotFound)
|
||||
assert.True(t, test.IsNormalPageCompleted(resp.Body.String()), "non-existing commit should render 404 page")
|
||||
}
|
||||
|
||||
// TestGenerateRepository the test cannot succeed when moved as a unit test
|
||||
func TestGenerateRepository(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
// a successful generate from template
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
repo44 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 44})
|
||||
|
||||
generatedRepo, err := repo_service.GenerateRepository(git.DefaultContext, user2, user2, repo44, repo_service.GenerateRepoOptions{
|
||||
Name: "generated-from-template-44",
|
||||
GitContent: true,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, generatedRepo)
|
||||
|
||||
exist, err := util.IsExist(repo_model.RepoPath(user2.Name, generatedRepo.Name))
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exist)
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerName: user2.Name, Name: generatedRepo.Name})
|
||||
|
||||
err = repo_service.DeleteRepositoryDirectly(db.DefaultContext, user2, generatedRepo.ID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// a failed creating because some mock data
|
||||
// create the repository directory so that the creation will fail after database record created.
|
||||
assert.NoError(t, os.MkdirAll(repo_model.RepoPath(user2.Name, "generated-from-template-44"), os.ModePerm))
|
||||
|
||||
generatedRepo2, err := repo_service.GenerateRepository(db.DefaultContext, user2, user2, repo44, repo_service.GenerateRepoOptions{
|
||||
Name: "generated-from-template-44",
|
||||
GitContent: true,
|
||||
})
|
||||
assert.Nil(t, generatedRepo2)
|
||||
assert.Error(t, err)
|
||||
|
||||
// assert the cleanup is successful
|
||||
unittest.AssertNotExistsBean(t, &repo_model.Repository{OwnerName: user2.Name, Name: generatedRepo.Name})
|
||||
|
||||
exist, err = util.IsExist(repo_model.RepoPath(user2.Name, generatedRepo.Name))
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, exist)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user