From 9bb1adf8ea5db928e0b21d6ee89876d15d2ec6a1 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Mon, 5 Feb 2024 14:17:23 +0800 Subject: [PATCH] Move some repository transfer functions to service layer (#28855) --- models/repo/update.go | 50 ----- models/repo_transfer.go | 247 +--------------------- models/repo_transfer_test.go | 57 ----- routers/api/v1/repo/transfer.go | 2 +- routers/web/repo/repo.go | 2 +- routers/web/repo/setting/setting.go | 2 +- services/repository/transfer.go | 302 ++++++++++++++++++++++++++- services/repository/transfer_test.go | 43 ++++ 8 files changed, 347 insertions(+), 358 deletions(-) delete mode 100644 models/repo_transfer_test.go diff --git a/models/repo/update.go b/models/repo/update.go index 6ddf1a8905..e7ca224028 100644 --- a/models/repo/update.go +++ b/models/repo/update.go @@ -6,7 +6,6 @@ package repo import ( "context" "fmt" - "strings" "time" "code.gitea.io/gitea/models/db" @@ -135,55 +134,6 @@ func CheckCreateRepository(ctx context.Context, doer, u *user_model.User, name s return nil } -// ChangeRepositoryName changes all corresponding setting from old repository name to new one. -func ChangeRepositoryName(ctx context.Context, doer *user_model.User, repo *Repository, newRepoName string) (err error) { - oldRepoName := repo.Name - newRepoName = strings.ToLower(newRepoName) - if err = IsUsableRepoName(newRepoName); err != nil { - return err - } - - if err := repo.LoadOwner(ctx); err != nil { - return err - } - - has, err := IsRepositoryModelOrDirExist(ctx, repo.Owner, newRepoName) - if err != nil { - return fmt.Errorf("IsRepositoryExist: %w", err) - } else if has { - return ErrRepoAlreadyExist{repo.Owner.Name, newRepoName} - } - - newRepoPath := RepoPath(repo.Owner.Name, newRepoName) - if err = util.Rename(repo.RepoPath(), newRepoPath); err != nil { - return fmt.Errorf("rename repository directory: %w", err) - } - - wikiPath := repo.WikiPath() - isExist, err := util.IsExist(wikiPath) - if err != nil { - log.Error("Unable to check if %s exists. Error: %v", wikiPath, err) - return err - } - if isExist { - if err = util.Rename(wikiPath, WikiPath(repo.Owner.Name, newRepoName)); err != nil { - return fmt.Errorf("rename repository wiki: %w", err) - } - } - - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - - if err := NewRedirect(ctx, repo.Owner.ID, repo.ID, oldRepoName, newRepoName); err != nil { - return err - } - - return committer.Commit() -} - // UpdateRepoSize updates the repository size, calculating it using getDirectorySize func UpdateRepoSize(ctx context.Context, repoID, gitSize, lfsSize int64) error { _, err := db.GetEngine(ctx).ID(repoID).Cols("size", "git_size", "lfs_size").NoAutoTime().Update(&Repository{ diff --git a/models/repo_transfer.go b/models/repo_transfer.go index 630c243c8e..676e2dbb63 100644 --- a/models/repo_transfer.go +++ b/models/repo_transfer.go @@ -6,17 +6,13 @@ package models import ( "context" "fmt" - "os" "code.gitea.io/gitea/models/db" - issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" - access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/timeutil" - "code.gitea.io/gitea/modules/util" ) // RepoTransfer is used to manage repository transfers @@ -115,32 +111,11 @@ func GetPendingRepositoryTransfer(ctx context.Context, repo *repo_model.Reposito return transfer, nil } -func deleteRepositoryTransfer(ctx context.Context, repoID int64) error { +func DeleteRepositoryTransfer(ctx context.Context, repoID int64) error { _, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Delete(&RepoTransfer{}) return err } -// CancelRepositoryTransfer marks the repository as ready and remove pending transfer entry, -// thus cancel the transfer process. -func CancelRepositoryTransfer(ctx context.Context, repo *repo_model.Repository) error { - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - - repo.Status = repo_model.RepositoryReady - if err := repo_model.UpdateRepositoryCols(ctx, repo, "status"); err != nil { - return err - } - - if err := deleteRepositoryTransfer(ctx, repo.ID); err != nil { - return err - } - - return committer.Commit() -} - // TestRepositoryReadyForTransfer make sure repo is ready to transfer func TestRepositoryReadyForTransfer(status repo_model.RepositoryStatus) error { switch status { @@ -197,223 +172,3 @@ func CreatePendingRepositoryTransfer(ctx context.Context, doer, newOwner *user_m return db.Insert(ctx, transfer) }) } - -// TransferOwnership transfers all corresponding repository items from old user to new one. -func TransferOwnership(ctx context.Context, doer *user_model.User, newOwnerName string, repo *repo_model.Repository) (err error) { - repoRenamed := false - wikiRenamed := false - oldOwnerName := doer.Name - - defer func() { - if !repoRenamed && !wikiRenamed { - return - } - - recoverErr := recover() - if err == nil && recoverErr == nil { - return - } - - if repoRenamed { - if err := util.Rename(repo_model.RepoPath(newOwnerName, repo.Name), repo_model.RepoPath(oldOwnerName, repo.Name)); err != nil { - log.Critical("Unable to move repository %s/%s directory from %s back to correct place %s: %v", oldOwnerName, repo.Name, - repo_model.RepoPath(newOwnerName, repo.Name), repo_model.RepoPath(oldOwnerName, repo.Name), err) - } - } - - if wikiRenamed { - if err := util.Rename(repo_model.WikiPath(newOwnerName, repo.Name), repo_model.WikiPath(oldOwnerName, repo.Name)); err != nil { - log.Critical("Unable to move wiki for repository %s/%s directory from %s back to correct place %s: %v", oldOwnerName, repo.Name, - repo_model.WikiPath(newOwnerName, repo.Name), repo_model.WikiPath(oldOwnerName, repo.Name), err) - } - } - - if recoverErr != nil { - log.Error("Panic within TransferOwnership: %v\n%s", recoverErr, log.Stack(2)) - panic(recoverErr) - } - }() - - ctx, committer, err := db.TxContext(ctx) - if err != nil { - return err - } - defer committer.Close() - - sess := db.GetEngine(ctx) - - newOwner, err := user_model.GetUserByName(ctx, newOwnerName) - if err != nil { - return fmt.Errorf("get new owner '%s': %w", newOwnerName, err) - } - newOwnerName = newOwner.Name // ensure capitalisation matches - - // Check if new owner has repository with same name. - if has, err := repo_model.IsRepositoryModelOrDirExist(ctx, newOwner, repo.Name); err != nil { - return fmt.Errorf("IsRepositoryExist: %w", err) - } else if has { - return repo_model.ErrRepoAlreadyExist{ - Uname: newOwnerName, - Name: repo.Name, - } - } - - oldOwner := repo.Owner - oldOwnerName = oldOwner.Name - - // Note: we have to set value here to make sure recalculate accesses is based on - // new owner. - repo.OwnerID = newOwner.ID - repo.Owner = newOwner - repo.OwnerName = newOwner.Name - - // Update repository. - if _, err := sess.ID(repo.ID).Update(repo); err != nil { - return fmt.Errorf("update owner: %w", err) - } - - // Remove redundant collaborators. - collaborators, err := repo_model.GetCollaborators(ctx, repo.ID, db.ListOptions{}) - if err != nil { - return fmt.Errorf("getCollaborators: %w", err) - } - - // Dummy object. - collaboration := &repo_model.Collaboration{RepoID: repo.ID} - for _, c := range collaborators { - if c.IsGhost() { - collaboration.ID = c.Collaboration.ID - if _, err := sess.Delete(collaboration); err != nil { - return fmt.Errorf("remove collaborator '%d': %w", c.ID, err) - } - collaboration.ID = 0 - } - - if c.ID != newOwner.ID { - isMember, err := organization.IsOrganizationMember(ctx, newOwner.ID, c.ID) - if err != nil { - return fmt.Errorf("IsOrgMember: %w", err) - } else if !isMember { - continue - } - } - collaboration.UserID = c.ID - if _, err := sess.Delete(collaboration); err != nil { - return fmt.Errorf("remove collaborator '%d': %w", c.ID, err) - } - collaboration.UserID = 0 - } - - // Remove old team-repository relations. - if oldOwner.IsOrganization() { - if err := organization.RemoveOrgRepo(ctx, oldOwner.ID, repo.ID); err != nil { - return fmt.Errorf("removeOrgRepo: %w", err) - } - } - - if newOwner.IsOrganization() { - teams, err := organization.FindOrgTeams(ctx, newOwner.ID) - if err != nil { - return fmt.Errorf("LoadTeams: %w", err) - } - for _, t := range teams { - if t.IncludesAllRepositories { - if err := AddRepository(ctx, t, repo); err != nil { - return fmt.Errorf("AddRepository: %w", err) - } - } - } - } else if err := access_model.RecalculateAccesses(ctx, repo); err != nil { - // Organization called this in addRepository method. - return fmt.Errorf("recalculateAccesses: %w", err) - } - - // Update repository count. - if _, err := sess.Exec("UPDATE `user` SET num_repos=num_repos+1 WHERE id=?", newOwner.ID); err != nil { - return fmt.Errorf("increase new owner repository count: %w", err) - } else if _, err := sess.Exec("UPDATE `user` SET num_repos=num_repos-1 WHERE id=?", oldOwner.ID); err != nil { - return fmt.Errorf("decrease old owner repository count: %w", err) - } - - if err := repo_model.WatchRepo(ctx, doer.ID, repo.ID, true); err != nil { - return fmt.Errorf("watchRepo: %w", err) - } - - // Remove watch for organization. - if oldOwner.IsOrganization() { - if err := repo_model.WatchRepo(ctx, oldOwner.ID, repo.ID, false); err != nil { - return fmt.Errorf("watchRepo [false]: %w", err) - } - } - - // Delete labels that belong to the old organization and comments that added these labels - if oldOwner.IsOrganization() { - if _, err := sess.Exec(`DELETE FROM issue_label WHERE issue_label.id IN ( - SELECT il_too.id FROM ( - SELECT il_too_too.id - FROM issue_label AS il_too_too - INNER JOIN label ON il_too_too.label_id = label.id - INNER JOIN issue on issue.id = il_too_too.issue_id - WHERE - issue.repo_id = ? AND ((label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != ?)) - ) AS il_too )`, repo.ID, newOwner.ID); err != nil { - return fmt.Errorf("Unable to remove old org labels: %w", err) - } - - if _, err := sess.Exec(`DELETE FROM comment WHERE comment.id IN ( - SELECT il_too.id FROM ( - SELECT com.id - FROM comment AS com - INNER JOIN label ON com.label_id = label.id - INNER JOIN issue ON issue.id = com.issue_id - WHERE - com.type = ? AND issue.repo_id = ? AND ((label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != ?)) - ) AS il_too)`, issues_model.CommentTypeLabel, repo.ID, newOwner.ID); err != nil { - return fmt.Errorf("Unable to remove old org label comments: %w", err) - } - } - - // Rename remote repository to new path and delete local copy. - dir := user_model.UserPath(newOwner.Name) - - if err := os.MkdirAll(dir, os.ModePerm); err != nil { - return fmt.Errorf("Failed to create dir %s: %w", dir, err) - } - - if err := util.Rename(repo_model.RepoPath(oldOwner.Name, repo.Name), repo_model.RepoPath(newOwner.Name, repo.Name)); err != nil { - return fmt.Errorf("rename repository directory: %w", err) - } - repoRenamed = true - - // Rename remote wiki repository to new path and delete local copy. - wikiPath := repo_model.WikiPath(oldOwner.Name, repo.Name) - - if isExist, err := util.IsExist(wikiPath); err != nil { - log.Error("Unable to check if %s exists. Error: %v", wikiPath, err) - return err - } else if isExist { - if err := util.Rename(wikiPath, repo_model.WikiPath(newOwner.Name, repo.Name)); err != nil { - return fmt.Errorf("rename repository wiki: %w", err) - } - wikiRenamed = true - } - - if err := deleteRepositoryTransfer(ctx, repo.ID); err != nil { - return fmt.Errorf("deleteRepositoryTransfer: %w", err) - } - repo.Status = repo_model.RepositoryReady - if err := repo_model.UpdateRepositoryCols(ctx, repo, "status"); err != nil { - return err - } - - // If there was previously a redirect at this location, remove it. - if err := repo_model.DeleteRedirect(ctx, newOwner.ID, repo.Name); err != nil { - return fmt.Errorf("delete repo redirect: %w", err) - } - - if err := repo_model.NewRedirect(ctx, oldOwner.ID, repo.ID, repo.Name, repo.Name); err != nil { - return fmt.Errorf("repo_model.NewRedirect: %w", err) - } - - return committer.Commit() -} diff --git a/models/repo_transfer_test.go b/models/repo_transfer_test.go deleted file mode 100644 index b55cef9473..0000000000 --- a/models/repo_transfer_test.go +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2021 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package models - -import ( - "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" - - "github.com/stretchr/testify/assert" -) - -func TestRepositoryTransfer(t *testing.T) { - assert.NoError(t, unittest.PrepareTestDatabase()) - - doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) - repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) - - transfer, err := GetPendingRepositoryTransfer(db.DefaultContext, repo) - assert.NoError(t, err) - assert.NotNil(t, transfer) - - // Cancel transfer - assert.NoError(t, CancelRepositoryTransfer(db.DefaultContext, repo)) - - transfer, err = GetPendingRepositoryTransfer(db.DefaultContext, repo) - assert.Error(t, err) - assert.Nil(t, transfer) - assert.True(t, IsErrNoPendingTransfer(err)) - - user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - - assert.NoError(t, CreatePendingRepositoryTransfer(db.DefaultContext, doer, user2, repo.ID, nil)) - - transfer, err = GetPendingRepositoryTransfer(db.DefaultContext, repo) - assert.Nil(t, err) - assert.NoError(t, transfer.LoadAttributes(db.DefaultContext)) - assert.Equal(t, "user2", transfer.Recipient.Name) - - org6 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - - // Only transfer can be started at any given time - err = CreatePendingRepositoryTransfer(db.DefaultContext, doer, org6, repo.ID, nil) - assert.Error(t, err) - assert.True(t, IsErrRepoTransferInProgress(err)) - - // Unknown user - err = CreatePendingRepositoryTransfer(db.DefaultContext, doer, &user_model.User{ID: 1000, LowerName: "user1000"}, repo.ID, nil) - assert.Error(t, err) - - // Cancel transfer - assert.NoError(t, CancelRepositoryTransfer(db.DefaultContext, repo)) -} diff --git a/routers/api/v1/repo/transfer.go b/routers/api/v1/repo/transfer.go index b3120f4be0..c0a40ce062 100644 --- a/routers/api/v1/repo/transfer.go +++ b/routers/api/v1/repo/transfer.go @@ -230,5 +230,5 @@ func acceptOrRejectRepoTransfer(ctx *context.APIContext, accept bool) error { return repo_service.TransferOwnership(ctx, repoTransfer.Doer, repoTransfer.Recipient, ctx.Repo.Repository, repoTransfer.Teams) } - return models.CancelRepositoryTransfer(ctx, ctx.Repo.Repository) + return repo_service.CancelRepositoryTransfer(ctx, ctx.Repo.Repository) } diff --git a/routers/web/repo/repo.go b/routers/web/repo/repo.go index b64db91406..6880d64614 100644 --- a/routers/web/repo/repo.go +++ b/routers/web/repo/repo.go @@ -362,7 +362,7 @@ func acceptOrRejectRepoTransfer(ctx *context.Context, accept bool) error { } ctx.Flash.Success(ctx.Tr("repo.settings.transfer.success")) } else { - if err := models.CancelRepositoryTransfer(ctx, ctx.Repo.Repository); err != nil { + if err := repo_service.CancelRepositoryTransfer(ctx, ctx.Repo.Repository); err != nil { return err } ctx.Flash.Success(ctx.Tr("repo.settings.transfer.rejected")) diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index fc1403b3cc..8c1daf52bc 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -813,7 +813,7 @@ func SettingsPost(ctx *context.Context) { return } - if err := models.CancelRepositoryTransfer(ctx, ctx.Repo.Repository); err != nil { + if err := repo_service.CancelRepositoryTransfer(ctx, ctx.Repo.Repository); err != nil { ctx.ServerError("CancelRepositoryTransfer", err) return } diff --git a/services/repository/transfer.go b/services/repository/transfer.go index 574b6c6a56..59a4eb260e 100644 --- a/services/repository/transfer.go +++ b/services/repository/transfer.go @@ -6,8 +6,12 @@ package repository import ( "context" "fmt" + "os" + "strings" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/models/db" + issues_model "code.gitea.io/gitea/models/issues" "code.gitea.io/gitea/models/organization" "code.gitea.io/gitea/models/perm" access_model "code.gitea.io/gitea/models/perm/access" @@ -16,6 +20,7 @@ import ( "code.gitea.io/gitea/modules/log" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/sync" + "code.gitea.io/gitea/modules/util" notify_service "code.gitea.io/gitea/services/notify" ) @@ -37,7 +42,7 @@ func TransferOwnership(ctx context.Context, doer, newOwner *user_model.User, rep oldOwner := repo.Owner repoWorkingPool.CheckIn(fmt.Sprint(repo.ID)) - if err := models.TransferOwnership(ctx, doer, newOwner.Name, repo); err != nil { + if err := transferOwnership(ctx, doer, newOwner.Name, repo); err != nil { repoWorkingPool.CheckOut(fmt.Sprint(repo.ID)) return err } @@ -59,6 +64,278 @@ func TransferOwnership(ctx context.Context, doer, newOwner *user_model.User, rep return nil } +// transferOwnership transfers all corresponding repository items from old user to new one. +func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName string, repo *repo_model.Repository) (err error) { + repoRenamed := false + wikiRenamed := false + oldOwnerName := doer.Name + + defer func() { + if !repoRenamed && !wikiRenamed { + return + } + + recoverErr := recover() + if err == nil && recoverErr == nil { + return + } + + if repoRenamed { + if err := util.Rename(repo_model.RepoPath(newOwnerName, repo.Name), repo_model.RepoPath(oldOwnerName, repo.Name)); err != nil { + log.Critical("Unable to move repository %s/%s directory from %s back to correct place %s: %v", oldOwnerName, repo.Name, + repo_model.RepoPath(newOwnerName, repo.Name), repo_model.RepoPath(oldOwnerName, repo.Name), err) + } + } + + if wikiRenamed { + if err := util.Rename(repo_model.WikiPath(newOwnerName, repo.Name), repo_model.WikiPath(oldOwnerName, repo.Name)); err != nil { + log.Critical("Unable to move wiki for repository %s/%s directory from %s back to correct place %s: %v", oldOwnerName, repo.Name, + repo_model.WikiPath(newOwnerName, repo.Name), repo_model.WikiPath(oldOwnerName, repo.Name), err) + } + } + + if recoverErr != nil { + log.Error("Panic within TransferOwnership: %v\n%s", recoverErr, log.Stack(2)) + panic(recoverErr) + } + }() + + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return err + } + defer committer.Close() + + sess := db.GetEngine(ctx) + + newOwner, err := user_model.GetUserByName(ctx, newOwnerName) + if err != nil { + return fmt.Errorf("get new owner '%s': %w", newOwnerName, err) + } + newOwnerName = newOwner.Name // ensure capitalisation matches + + // Check if new owner has repository with same name. + if has, err := repo_model.IsRepositoryModelOrDirExist(ctx, newOwner, repo.Name); err != nil { + return fmt.Errorf("IsRepositoryExist: %w", err) + } else if has { + return repo_model.ErrRepoAlreadyExist{ + Uname: newOwnerName, + Name: repo.Name, + } + } + + oldOwner := repo.Owner + oldOwnerName = oldOwner.Name + + // Note: we have to set value here to make sure recalculate accesses is based on + // new owner. + repo.OwnerID = newOwner.ID + repo.Owner = newOwner + repo.OwnerName = newOwner.Name + + // Update repository. + if _, err := sess.ID(repo.ID).Update(repo); err != nil { + return fmt.Errorf("update owner: %w", err) + } + + // Remove redundant collaborators. + collaborators, err := repo_model.GetCollaborators(ctx, repo.ID, db.ListOptions{}) + if err != nil { + return fmt.Errorf("getCollaborators: %w", err) + } + + // Dummy object. + collaboration := &repo_model.Collaboration{RepoID: repo.ID} + for _, c := range collaborators { + if c.IsGhost() { + collaboration.ID = c.Collaboration.ID + if _, err := sess.Delete(collaboration); err != nil { + return fmt.Errorf("remove collaborator '%d': %w", c.ID, err) + } + collaboration.ID = 0 + } + + if c.ID != newOwner.ID { + isMember, err := organization.IsOrganizationMember(ctx, newOwner.ID, c.ID) + if err != nil { + return fmt.Errorf("IsOrgMember: %w", err) + } else if !isMember { + continue + } + } + collaboration.UserID = c.ID + if _, err := sess.Delete(collaboration); err != nil { + return fmt.Errorf("remove collaborator '%d': %w", c.ID, err) + } + collaboration.UserID = 0 + } + + // Remove old team-repository relations. + if oldOwner.IsOrganization() { + if err := organization.RemoveOrgRepo(ctx, oldOwner.ID, repo.ID); err != nil { + return fmt.Errorf("removeOrgRepo: %w", err) + } + } + + if newOwner.IsOrganization() { + teams, err := organization.FindOrgTeams(ctx, newOwner.ID) + if err != nil { + return fmt.Errorf("LoadTeams: %w", err) + } + for _, t := range teams { + if t.IncludesAllRepositories { + if err := models.AddRepository(ctx, t, repo); err != nil { + return fmt.Errorf("AddRepository: %w", err) + } + } + } + } else if err := access_model.RecalculateAccesses(ctx, repo); err != nil { + // Organization called this in addRepository method. + return fmt.Errorf("recalculateAccesses: %w", err) + } + + // Update repository count. + if _, err := sess.Exec("UPDATE `user` SET num_repos=num_repos+1 WHERE id=?", newOwner.ID); err != nil { + return fmt.Errorf("increase new owner repository count: %w", err) + } else if _, err := sess.Exec("UPDATE `user` SET num_repos=num_repos-1 WHERE id=?", oldOwner.ID); err != nil { + return fmt.Errorf("decrease old owner repository count: %w", err) + } + + if err := repo_model.WatchRepo(ctx, doer.ID, repo.ID, true); err != nil { + return fmt.Errorf("watchRepo: %w", err) + } + + // Remove watch for organization. + if oldOwner.IsOrganization() { + if err := repo_model.WatchRepo(ctx, oldOwner.ID, repo.ID, false); err != nil { + return fmt.Errorf("watchRepo [false]: %w", err) + } + } + + // Delete labels that belong to the old organization and comments that added these labels + if oldOwner.IsOrganization() { + if _, err := sess.Exec(`DELETE FROM issue_label WHERE issue_label.id IN ( + SELECT il_too.id FROM ( + SELECT il_too_too.id + FROM issue_label AS il_too_too + INNER JOIN label ON il_too_too.label_id = label.id + INNER JOIN issue on issue.id = il_too_too.issue_id + WHERE + issue.repo_id = ? AND ((label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != ?)) + ) AS il_too )`, repo.ID, newOwner.ID); err != nil { + return fmt.Errorf("Unable to remove old org labels: %w", err) + } + + if _, err := sess.Exec(`DELETE FROM comment WHERE comment.id IN ( + SELECT il_too.id FROM ( + SELECT com.id + FROM comment AS com + INNER JOIN label ON com.label_id = label.id + INNER JOIN issue ON issue.id = com.issue_id + WHERE + com.type = ? AND issue.repo_id = ? AND ((label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != ?)) + ) AS il_too)`, issues_model.CommentTypeLabel, repo.ID, newOwner.ID); err != nil { + return fmt.Errorf("Unable to remove old org label comments: %w", err) + } + } + + // Rename remote repository to new path and delete local copy. + dir := user_model.UserPath(newOwner.Name) + + if err := os.MkdirAll(dir, os.ModePerm); err != nil { + return fmt.Errorf("Failed to create dir %s: %w", dir, err) + } + + if err := util.Rename(repo_model.RepoPath(oldOwner.Name, repo.Name), repo_model.RepoPath(newOwner.Name, repo.Name)); err != nil { + return fmt.Errorf("rename repository directory: %w", err) + } + repoRenamed = true + + // Rename remote wiki repository to new path and delete local copy. + wikiPath := repo_model.WikiPath(oldOwner.Name, repo.Name) + + if isExist, err := util.IsExist(wikiPath); err != nil { + log.Error("Unable to check if %s exists. Error: %v", wikiPath, err) + return err + } else if isExist { + if err := util.Rename(wikiPath, repo_model.WikiPath(newOwner.Name, repo.Name)); err != nil { + return fmt.Errorf("rename repository wiki: %w", err) + } + wikiRenamed = true + } + + if err := models.DeleteRepositoryTransfer(ctx, repo.ID); err != nil { + return fmt.Errorf("deleteRepositoryTransfer: %w", err) + } + repo.Status = repo_model.RepositoryReady + if err := repo_model.UpdateRepositoryCols(ctx, repo, "status"); err != nil { + return err + } + + // If there was previously a redirect at this location, remove it. + if err := repo_model.DeleteRedirect(ctx, newOwner.ID, repo.Name); err != nil { + return fmt.Errorf("delete repo redirect: %w", err) + } + + if err := repo_model.NewRedirect(ctx, oldOwner.ID, repo.ID, repo.Name, repo.Name); err != nil { + return fmt.Errorf("repo_model.NewRedirect: %w", err) + } + + return committer.Commit() +} + +// changeRepositoryName changes all corresponding setting from old repository name to new one. +func changeRepositoryName(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, newRepoName string) (err error) { + oldRepoName := repo.Name + newRepoName = strings.ToLower(newRepoName) + if err = repo_model.IsUsableRepoName(newRepoName); err != nil { + return err + } + + if err := repo.LoadOwner(ctx); err != nil { + return err + } + + has, err := repo_model.IsRepositoryModelOrDirExist(ctx, repo.Owner, newRepoName) + if err != nil { + return fmt.Errorf("IsRepositoryExist: %w", err) + } else if has { + return repo_model.ErrRepoAlreadyExist{ + Uname: repo.Owner.Name, + Name: newRepoName, + } + } + + newRepoPath := repo_model.RepoPath(repo.Owner.Name, newRepoName) + if err = util.Rename(repo.RepoPath(), newRepoPath); err != nil { + return fmt.Errorf("rename repository directory: %w", err) + } + + wikiPath := repo.WikiPath() + isExist, err := util.IsExist(wikiPath) + if err != nil { + log.Error("Unable to check if %s exists. Error: %v", wikiPath, err) + return err + } + if isExist { + if err = util.Rename(wikiPath, repo_model.WikiPath(repo.Owner.Name, newRepoName)); err != nil { + return fmt.Errorf("rename repository wiki: %w", err) + } + } + + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return err + } + defer committer.Close() + + if err := repo_model.NewRedirect(ctx, repo.Owner.ID, repo.ID, oldRepoName, newRepoName); err != nil { + return err + } + + return committer.Commit() +} + // ChangeRepositoryName changes all corresponding setting from old repository name to new one. func ChangeRepositoryName(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, newRepoName string) error { log.Trace("ChangeRepositoryName: %s/%s -> %s", doer.Name, repo.Name, newRepoName) @@ -70,7 +347,7 @@ func ChangeRepositoryName(ctx context.Context, doer *user_model.User, repo *repo // local copy's origin accordingly. repoWorkingPool.CheckIn(fmt.Sprint(repo.ID)) - if err := repo_model.ChangeRepositoryName(ctx, doer, repo, newRepoName); err != nil { + if err := changeRepositoryName(ctx, doer, repo, newRepoName); err != nil { repoWorkingPool.CheckOut(fmt.Sprint(repo.ID)) return err } @@ -130,3 +407,24 @@ func StartRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.Use return nil } + +// CancelRepositoryTransfer marks the repository as ready and remove pending transfer entry, +// thus cancel the transfer process. +func CancelRepositoryTransfer(ctx context.Context, repo *repo_model.Repository) error { + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return err + } + defer committer.Close() + + repo.Status = repo_model.RepositoryReady + if err := repo_model.UpdateRepositoryCols(ctx, repo, "status"); err != nil { + return err + } + + if err := models.DeleteRepositoryTransfer(ctx, repo.ID); err != nil { + return err + } + + return committer.Commit() +} diff --git a/services/repository/transfer_test.go b/services/repository/transfer_test.go index d55c76ea47..c3f03d6638 100644 --- a/services/repository/transfer_test.go +++ b/services/repository/transfer_test.go @@ -7,6 +7,7 @@ import ( "sync" "testing" + "code.gitea.io/gitea/models" activities_model "code.gitea.io/gitea/models/activities" "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/organization" @@ -78,3 +79,45 @@ func TestStartRepositoryTransferSetPermission(t *testing.T) { unittest.CheckConsistencyFor(t, &repo_model.Repository{}, &user_model.User{}, &organization.Team{}) } + +func TestRepositoryTransfer(t *testing.T) { + assert.NoError(t, unittest.PrepareTestDatabase()) + + doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) + + transfer, err := models.GetPendingRepositoryTransfer(db.DefaultContext, repo) + assert.NoError(t, err) + assert.NotNil(t, transfer) + + // Cancel transfer + assert.NoError(t, CancelRepositoryTransfer(db.DefaultContext, repo)) + + transfer, err = models.GetPendingRepositoryTransfer(db.DefaultContext, repo) + assert.Error(t, err) + assert.Nil(t, transfer) + assert.True(t, models.IsErrNoPendingTransfer(err)) + + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + assert.NoError(t, models.CreatePendingRepositoryTransfer(db.DefaultContext, doer, user2, repo.ID, nil)) + + transfer, err = models.GetPendingRepositoryTransfer(db.DefaultContext, repo) + assert.Nil(t, err) + assert.NoError(t, transfer.LoadAttributes(db.DefaultContext)) + assert.Equal(t, "user2", transfer.Recipient.Name) + + org6 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + + // Only transfer can be started at any given time + err = models.CreatePendingRepositoryTransfer(db.DefaultContext, doer, org6, repo.ID, nil) + assert.Error(t, err) + assert.True(t, models.IsErrRepoTransferInProgress(err)) + + // Unknown user + err = models.CreatePendingRepositoryTransfer(db.DefaultContext, doer, &user_model.User{ID: 1000, LowerName: "user1000"}, repo.ID, nil) + assert.Error(t, err) + + // Cancel transfer + assert.NoError(t, CancelRepositoryTransfer(db.DefaultContext, repo)) +}