mirror of
https://github.com/go-gitea/gitea.git
synced 2025-04-20 00:19:08 +03: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:
@ -235,6 +235,11 @@ func GetDeletedBranchByID(ctx context.Context, repoID, branchID int64) (*Branch,
|
|||||||
return &branch, nil
|
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 {
|
func DeleteBranches(ctx context.Context, repoID, doerID int64, branchIDs []int64) error {
|
||||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
branches := make([]*Branch, 0, len(branchIDs))
|
branches := make([]*Branch, 0, len(branchIDs))
|
||||||
|
@ -558,3 +558,8 @@ func FindTagsByCommitIDs(ctx context.Context, repoID int64, commitIDs ...string)
|
|||||||
}
|
}
|
||||||
return res, nil
|
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"
|
"strings"
|
||||||
|
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
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/label"
|
||||||
"code.gitea.io/gitea/modules/log"
|
|
||||||
"code.gitea.io/gitea/modules/options"
|
"code.gitea.io/gitea/modules/options"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
@ -121,29 +117,6 @@ func LoadRepoConfig() error {
|
|||||||
return nil
|
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
|
// InitializeLabels adds a label set to a repository using a template
|
||||||
func InitializeLabels(ctx context.Context, id int64, labelTemplate string, isOrg bool) error {
|
func InitializeLabels(ctx context.Context, id int64, labelTemplate string, isOrg bool) error {
|
||||||
list, err := LoadTemplateLabelsByDisplayName(labelTemplate)
|
list, err := LoadTemplateLabelsByDisplayName(labelTemplate)
|
||||||
|
@ -16,7 +16,6 @@ import (
|
|||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/container"
|
"code.gitea.io/gitea/modules/container"
|
||||||
"code.gitea.io/gitea/modules/git"
|
|
||||||
"code.gitea.io/gitea/modules/gitrepo"
|
"code.gitea.io/gitea/modules/gitrepo"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/optional"
|
"code.gitea.io/gitea/modules/optional"
|
||||||
@ -28,6 +27,18 @@ import (
|
|||||||
"github.com/gobwas/glob"
|
"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.
|
// 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) {
|
func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts CreateRepoOptions) (*repo_model.Repository, error) {
|
||||||
if !doer.IsAdmin && !u.CanCreateRepo() {
|
if !doer.IsAdmin && !u.CanCreateRepo() {
|
||||||
@ -48,58 +59,51 @@ func AdoptRepository(ctx context.Context, doer, u *user_model.User, opts CreateR
|
|||||||
IsPrivate: opts.IsPrivate,
|
IsPrivate: opts.IsPrivate,
|
||||||
IsFsckEnabled: !opts.IsMirror,
|
IsFsckEnabled: !opts.IsMirror,
|
||||||
CloseIssuesViaCommitInAnyBranch: setting.Repository.DefaultCloseIssuesViaCommitsInAnyBranch,
|
CloseIssuesViaCommitInAnyBranch: setting.Repository.DefaultCloseIssuesViaCommitsInAnyBranch,
|
||||||
Status: opts.Status,
|
Status: repo_model.RepositoryBeingMigrated,
|
||||||
IsEmpty: !opts.AutoInit,
|
IsEmpty: !opts.AutoInit,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
// 1 - create the repository database operations first
|
||||||
isExist, err := gitrepo.IsRepositoryExist(ctx, repo)
|
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 {
|
if err != nil {
|
||||||
log.Error("Unable to check if %s exists. Error: %v", repo.FullName(), err)
|
// we can not use the ctx because it maybe canceled or timeout
|
||||||
return err
|
if errDel := deleteFailedAdoptRepository(repo.ID); errDel != nil {
|
||||||
}
|
log.Error("Failed to delete repository %s that could not be adopted: %v", repo.FullName(), errDel)
|
||||||
if !isExist {
|
|
||||||
return repo_model.ErrRepoNotExist{
|
|
||||||
OwnerName: u.Name,
|
|
||||||
Name: repo.Name,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
if err := CreateRepositoryByExample(ctx, doer, u, repo, true, false); err != nil {
|
// Re-fetch the repository from database before updating it (else it would
|
||||||
return err
|
// 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)
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := func() error {
|
// 2 - adopt the repository from disk
|
||||||
if err := adoptRepository(ctx, repo, opts.DefaultBranch); err != nil {
|
if err = adoptRepository(ctx, repo, opts.DefaultBranch); err != nil {
|
||||||
return fmt.Errorf("adoptRepository: %w", err)
|
return nil, 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
notify_service.AdoptRepository(ctx, doer, u, repo)
|
||||||
|
|
||||||
return repo, nil
|
return repo, nil
|
||||||
|
@ -14,6 +14,7 @@ import (
|
|||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@ -89,10 +90,36 @@ func TestListUnadoptedRepositories_ListOptions(t *testing.T) {
|
|||||||
|
|
||||||
func TestAdoptRepository(t *testing.T) {
|
func TestAdoptRepository(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
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})
|
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)
|
assert.NoError(t, err)
|
||||||
repoTestAdopt := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: "test-adopt"})
|
repoTestAdopt := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{Name: "test-adopt"})
|
||||||
assert.Equal(t, "sha1", repoTestAdopt.ObjectFormatName)
|
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"
|
"code.gitea.io/gitea/models/perm"
|
||||||
access_model "code.gitea.io/gitea/models/perm/access"
|
access_model "code.gitea.io/gitea/models/perm/access"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
|
system_model "code.gitea.io/gitea/models/system"
|
||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/models/webhook"
|
"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.
|
// InitRepository initializes README and .gitignore if needed.
|
||||||
func initRepository(ctx context.Context, u *user_model.User, repo *repo_model.Repository, opts CreateRepoOptions) (err error) {
|
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 {
|
// Init git bare new repository.
|
||||||
return err
|
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.
|
// 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,
|
ObjectFormatName: opts.ObjectFormatName,
|
||||||
}
|
}
|
||||||
|
|
||||||
var rollbackRepo *repo_model.Repository
|
needsUpdateStatus := opts.Status != repo_model.RepositoryReady
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 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
|
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
|
return repo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateRepositoryByExample creates a repository for the user/organization.
|
// createRepositoryInDB 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) {
|
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 {
|
if err = repo_model.IsUsableRepoName(repo.Name); err != nil {
|
||||||
return err
|
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 {
|
if err = db.Insert(ctx, repo); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -473,3 +457,26 @@ func CreateRepositoryByExample(ctx context.Context, doer, u *user_model.User, re
|
|||||||
|
|
||||||
return nil
|
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"
|
"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.
|
// 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)
|
// 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 {
|
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
|
needRewriteKeysFile := deleted > 0
|
||||||
|
|
||||||
if cnt, err := sess.ID(repoID).Delete(&repo_model.Repository{}); err != nil {
|
if err := deleteDBRepository(ctx, repoID); err != nil {
|
||||||
return err
|
return err
|
||||||
} else if cnt != 1 {
|
|
||||||
return repo_model.ErrRepoNotExist{
|
|
||||||
ID: repoID,
|
|
||||||
OwnerName: "",
|
|
||||||
Name: "",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if org != nil && org.IsOrganization() {
|
if org != nil && org.IsOrganization() {
|
||||||
|
@ -100,114 +100,106 @@ func ForkRepository(ctx context.Context, doer, owner *user_model.User, opts Fork
|
|||||||
IsFork: true,
|
IsFork: true,
|
||||||
ForkID: opts.BaseRepo.ID,
|
ForkID: opts.BaseRepo.ID,
|
||||||
ObjectFormatName: opts.BaseRepo.ObjectFormatName,
|
ObjectFormatName: opts.BaseRepo.ObjectFormatName,
|
||||||
|
Status: repo_model.RepositoryBeingMigrated,
|
||||||
}
|
}
|
||||||
|
|
||||||
oldRepoPath := opts.BaseRepo.RepoPath()
|
// 1 - Create the repository in the database
|
||||||
|
err = db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
needsRollback := false
|
if err = createRepositoryInDB(ctx, doer, owner, repo, true); err != nil {
|
||||||
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 {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err = repo_model.IncrementRepoForkNum(ctx, opts.BaseRepo.ID); err != nil {
|
||||||
if err = repo_model.IncrementRepoForkNum(txCtx, opts.BaseRepo.ID); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// copy lfs files failure should not be ignored
|
// copy lfs files failure should not be ignored
|
||||||
if err = git_model.CopyLFS(txCtx, repo, opts.BaseRepo); err != nil {
|
return git_model.CopyLFS(ctx, repo, opts.BaseRepo)
|
||||||
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
|
|
||||||
})
|
})
|
||||||
needsRollbackInPanic = false
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rollbackFn()
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// even if below operations failed, it could be ignored. And they will be retried
|
// last - clean up if something goes wrong
|
||||||
if err := repo_module.UpdateRepoSize(ctx, repo); err != nil {
|
// WARNING: Don't override all later err with local variables
|
||||||
log.Error("Failed to update size for repository: %v", err)
|
defer func() {
|
||||||
}
|
if err != nil {
|
||||||
if err := repo_model.CopyLanguageStat(ctx, opts.BaseRepo, repo); err != nil {
|
// we can not use the ctx because it maybe canceled or timeout
|
||||||
log.Error("Copy language stat from oldRepo failed: %v", err)
|
cleanupRepository(doer, repo.ID)
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 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)
|
notify_service.ForkRepository(ctx, doer, opts.BaseRepo, repo)
|
||||||
|
@ -4,13 +4,16 @@
|
|||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
"code.gitea.io/gitea/models/unittest"
|
"code.gitea.io/gitea/models/unittest"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
@ -46,3 +49,43 @@ func TestForkRepository(t *testing.T) {
|
|||||||
assert.Nil(t, fork2)
|
assert.Nil(t, fork2)
|
||||||
assert.True(t, repo_model.IsErrReachLimitOfRepo(err))
|
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"
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
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/git"
|
||||||
"code.gitea.io/gitea/modules/gitrepo"
|
"code.gitea.io/gitea/modules/gitrepo"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"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
|
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)$`)
|
var fileNameSanitizeRegexp = regexp.MustCompile(`(?i)\.\.|[<>:\"/\\|?*\x{0000}-\x{001F}]|^(con|prn|aux|nul|com\d|lpt\d)$`)
|
||||||
|
|
||||||
// Sanitize user input to valid OS filenames
|
// Sanitize user input to valid OS filenames
|
||||||
|
@ -118,14 +118,8 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
|
|||||||
repo.Owner = u
|
repo.Owner = u
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := repo_module.CheckDaemonExportOK(ctx, repo); err != nil {
|
if err := updateGitRepoAfterCreate(ctx, repo); err != nil {
|
||||||
return repo, fmt.Errorf("checkDaemonExportOK: %w", err)
|
return nil, fmt.Errorf("updateGitRepoAfterCreate: %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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
gitRepo, err := git.OpenRepository(ctx, repoPath)
|
gitRepo, err := git.OpenRepository(ctx, repoPath)
|
||||||
|
@ -5,12 +5,17 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
git_model "code.gitea.io/gitea/models/git"
|
git_model "code.gitea.io/gitea/models/git"
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
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"
|
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
|
generateRepo := &repo_model.Repository{
|
||||||
if err = db.WithTx(ctx, func(ctx context.Context) error {
|
OwnerID: owner.ID,
|
||||||
generateRepo, err = generateRepository(ctx, doer, owner, templateRepo, opts)
|
Owner: owner,
|
||||||
if err != nil {
|
OwnerName: owner.Name,
|
||||||
return err
|
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
|
// 1 - Create the repository in the database
|
||||||
if opts.GitContent && !templateRepo.IsEmpty {
|
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
if err = GenerateGitContent(ctx, templateRepo, generateRepo); err != nil {
|
return createRepositoryInDB(ctx, doer, owner, generateRepo, false)
|
||||||
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
|
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, err
|
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)
|
notify_service.CreateRepository(ctx, doer, owner, generateRepo)
|
||||||
|
|
||||||
return generateRepo, nil
|
return generateRepo, nil
|
||||||
|
@ -6,15 +6,23 @@ package integration
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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/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/setting"
|
||||||
"code.gitea.io/gitea/modules/test"
|
"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"
|
"code.gitea.io/gitea/tests"
|
||||||
|
|
||||||
"github.com/PuerkitoBio/goquery"
|
"github.com/PuerkitoBio/goquery"
|
||||||
@ -493,3 +501,46 @@ func testViewCommit(t *testing.T) {
|
|||||||
resp := MakeRequest(t, req, http.StatusNotFound)
|
resp := MakeRequest(t, req, http.StatusNotFound)
|
||||||
assert.True(t, test.IsNormalPageCompleted(resp.Body.String()), "non-existing commit should render 404 page")
|
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)
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user