Skip to content

Refactor repository transfer #33211

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 52 additions & 15 deletions models/repo/transfer.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ type RepoTransfer struct { //nolint
RecipientID int64
Recipient *user_model.User `xorm:"-"`
RepoID int64
Repo *Repository `xorm:"-"`
TeamIDs []int64
Teams []*organization.Team `xorm:"-"`

Expand All @@ -79,48 +80,65 @@ func init() {
db.RegisterModel(new(RepoTransfer))
}

// LoadAttributes fetches the transfer recipient from the database
func (r *RepoTransfer) LoadAttributes(ctx context.Context) error {
func (r *RepoTransfer) LoadRecipient(ctx context.Context) error {
if r.Recipient == nil {
u, err := user_model.GetUserByID(ctx, r.RecipientID)
if err != nil {
return err
}

r.Recipient = u
}

if r.Recipient.IsOrganization() && len(r.TeamIDs) != len(r.Teams) {
for _, v := range r.TeamIDs {
team, err := organization.GetTeamByID(ctx, v)
if err != nil {
return err
}
return nil
}

if team.OrgID != r.Recipient.ID {
return fmt.Errorf("team %d belongs not to org %d", v, r.Recipient.ID)
}
func (r *RepoTransfer) LoadRepo(ctx context.Context) error {
if r.Repo == nil {
repo, err := GetRepositoryByID(ctx, r.RepoID)
if err != nil {
return err
}
r.Repo = repo
}

return nil
}

// LoadAttributes fetches the transfer recipient from the database
func (r *RepoTransfer) LoadAttributes(ctx context.Context) error {
if err := r.LoadRecipient(ctx); err != nil {
return err
}

if r.Recipient.IsOrganization() && r.Teams == nil {
teamsMap, err := organization.GetTeamsByIDs(ctx, r.TeamIDs)
if err != nil {
return err
}
for _, team := range teamsMap {
r.Teams = append(r.Teams, team)
}
}

if err := r.LoadRepo(ctx); err != nil {
return err
}

if r.Doer == nil {
u, err := user_model.GetUserByID(ctx, r.DoerID)
if err != nil {
return err
}

r.Doer = u
}

return nil
}

// CanUserAcceptTransfer checks if the user has the rights to accept/decline a repo transfer.
// CanUserAcceptOrRejectTransfer checks if the user has the rights to accept/decline a repo transfer.
// For user, it checks if it's himself
// For organizations, it checks if the user is able to create repos
func (r *RepoTransfer) CanUserAcceptTransfer(ctx context.Context, u *user_model.User) bool {
func (r *RepoTransfer) CanUserAcceptOrRejectTransfer(ctx context.Context, u *user_model.User) bool {
if err := r.LoadAttributes(ctx); err != nil {
log.Error("LoadAttributes: %v", err)
return false
Expand Down Expand Up @@ -166,6 +184,10 @@ func GetPendingRepositoryTransfers(ctx context.Context, opts *PendingRepositoryT
Find(&transfers)
}

func IsRepositoryTransferExist(ctx context.Context, repoID int64) (bool, error) {
return db.GetEngine(ctx).Where("repo_id = ?", repoID).Exist(new(RepoTransfer))
}

// GetPendingRepositoryTransfer fetches the most recent and ongoing transfer
// process for the repository
func GetPendingRepositoryTransfer(ctx context.Context, repo *Repository) (*RepoTransfer, error) {
Expand Down Expand Up @@ -206,11 +228,26 @@ func CreatePendingRepositoryTransfer(ctx context.Context, doer, newOwner *user_m
return err
}

if _, err := user_model.GetUserByID(ctx, newOwner.ID); err != nil {
return err
}

Comment on lines +231 to +234
Copy link
Contributor

@yp05327 yp05327 Jan 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that this is unnecessary. If you got a User pointer, it should come from a GetUserByxxx function, so the user exist check is already done before calling this function, and even if newOwner is nil (although it is impossible), this will cause panic.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because this test introduced a non-exist user. So we need to check it here.
image

// Make sure repo is ready to transfer
if err := TestRepositoryReadyForTransfer(repo.Status); err != nil {
return err
}

exist, err := IsRepositoryTransferExist(ctx, repo.ID)
if err != nil {
return err
}
if exist {
return ErrRepoTransferInProgress{
Uname: repo.Owner.LowerName,
Name: repo.Name,
}
}

repo.Status = RepositoryPendingTransfer
if err := UpdateRepositoryCols(ctx, repo, "status"); err != nil {
return err
Expand Down
55 changes: 19 additions & 36 deletions routers/api/v1/repo/transfer.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/convert"
Expand Down Expand Up @@ -161,12 +162,16 @@ func AcceptTransfer(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"

err := acceptOrRejectRepoTransfer(ctx, true)
if ctx.Written() {
return
}
err := repo_service.AcceptTransferOwnership(ctx, ctx.Repo.Repository, ctx.Doer)
if err != nil {
ctx.Error(http.StatusInternalServerError, "acceptOrRejectRepoTransfer", err)
switch {
case repo_model.IsErrNoPendingTransfer(err):
ctx.Error(http.StatusNotFound, "AcceptTransferOwnership", err)
case errors.Is(err, util.ErrPermissionDenied):
ctx.Error(http.StatusForbidden, "AcceptTransferOwnership", err)
default:
ctx.Error(http.StatusInternalServerError, "AcceptTransferOwnership", err)
}
return
}

Expand Down Expand Up @@ -199,40 +204,18 @@ func RejectTransfer(ctx *context.APIContext) {
// "404":
// "$ref": "#/responses/notFound"

err := acceptOrRejectRepoTransfer(ctx, false)
if ctx.Written() {
return
}
err := repo_service.RejectRepositoryTransfer(ctx, ctx.Repo.Repository, ctx.Doer)
if err != nil {
ctx.Error(http.StatusInternalServerError, "acceptOrRejectRepoTransfer", err)
switch {
case repo_model.IsErrNoPendingTransfer(err):
ctx.Error(http.StatusNotFound, "RejectRepositoryTransfer", err)
case errors.Is(err, util.ErrPermissionDenied):
ctx.Error(http.StatusForbidden, "RejectRepositoryTransfer", err)
default:
ctx.Error(http.StatusInternalServerError, "RejectRepositoryTransfer", err)
}
return
}

ctx.JSON(http.StatusOK, convert.ToRepo(ctx, ctx.Repo.Repository, ctx.Repo.Permission))
}

func acceptOrRejectRepoTransfer(ctx *context.APIContext, accept bool) error {
repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository)
if err != nil {
if repo_model.IsErrNoPendingTransfer(err) {
ctx.NotFound()
return nil
}
return err
}

if err := repoTransfer.LoadAttributes(ctx); err != nil {
return err
}

if !repoTransfer.CanUserAcceptTransfer(ctx, ctx.Doer) {
ctx.Error(http.StatusForbidden, "CanUserAcceptTransfer", nil)
return fmt.Errorf("user does not have permissions to do this")
}

if accept {
return repo_service.TransferOwnership(ctx, repoTransfer.Doer, repoTransfer.Recipient, ctx.Repo.Repository, repoTransfer.Teams)
}

return repo_service.CancelRepositoryTransfer(ctx, ctx.Repo.Repository)
}
79 changes: 36 additions & 43 deletions routers/web/repo/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,36 @@ const (
tplStarUnstar templates.TplName = "repo/star_unstar"
)

func acceptTransfer(ctx *context.Context) {
err := repo_service.AcceptTransferOwnership(ctx, ctx.Repo.Repository, ctx.Doer)
if err == nil {
ctx.Flash.Success(ctx.Tr("repo.settings.transfer.success"))
ctx.Redirect(ctx.Repo.Repository.Link())
return
}
handleActionError(ctx, err)
}

func rejectTransfer(ctx *context.Context) {
err := repo_service.RejectRepositoryTransfer(ctx, ctx.Repo.Repository, ctx.Doer)
if err == nil {
ctx.Flash.Success(ctx.Tr("repo.settings.transfer.rejected"))
ctx.Redirect(ctx.Repo.Repository.Link())
return
}
handleActionError(ctx, err)
}

func handleActionError(ctx *context.Context, err error) {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.Flash.Error(ctx.Tr("repo.action.blocked_user"))
} else if errors.Is(err, util.ErrPermissionDenied) {
ctx.Error(http.StatusNotFound)
} else {
ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.PathParam("action")), err)
}
}

// Action response for actions to a repository
func Action(ctx *context.Context) {
var err error
Expand All @@ -322,9 +352,11 @@ func Action(ctx *context.Context) {
case "unstar":
err = repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, false)
case "accept_transfer":
err = acceptOrRejectRepoTransfer(ctx, true)
acceptTransfer(ctx)
return
case "reject_transfer":
err = acceptOrRejectRepoTransfer(ctx, false)
rejectTransfer(ctx)
return
case "desc": // FIXME: this is not used
if !ctx.Repo.IsOwner() {
ctx.Error(http.StatusNotFound)
Expand All @@ -337,12 +369,8 @@ func Action(ctx *context.Context) {
}

if err != nil {
if errors.Is(err, user_model.ErrBlockedUser) {
ctx.Flash.Error(ctx.Tr("repo.action.blocked_user"))
} else {
ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.PathParam("action")), err)
return
}
handleActionError(ctx, err)
return
}

switch ctx.PathParam("action") {
Expand Down Expand Up @@ -377,41 +405,6 @@ func Action(ctx *context.Context) {
ctx.RedirectToCurrentSite(ctx.FormString("redirect_to"), ctx.Repo.RepoLink)
}

func acceptOrRejectRepoTransfer(ctx *context.Context, accept bool) error {
repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, ctx.Repo.Repository)
if err != nil {
return err
}

if err := repoTransfer.LoadAttributes(ctx); err != nil {
return err
}

if !repoTransfer.CanUserAcceptTransfer(ctx, ctx.Doer) {
return errors.New("user does not have enough permissions")
}

if accept {
if ctx.Repo.GitRepo != nil {
ctx.Repo.GitRepo.Close()
ctx.Repo.GitRepo = nil
}

if err := repo_service.TransferOwnership(ctx, repoTransfer.Doer, repoTransfer.Recipient, ctx.Repo.Repository, repoTransfer.Teams); err != nil {
return err
}
ctx.Flash.Success(ctx.Tr("repo.settings.transfer.success"))
} else {
if err := repo_service.CancelRepositoryTransfer(ctx, ctx.Repo.Repository); err != nil {
return err
}
ctx.Flash.Success(ctx.Tr("repo.settings.transfer.rejected"))
}

ctx.Redirect(ctx.Repo.Repository.Link())
return nil
}

// RedirectDownload return a file based on the following infos:
func RedirectDownload(ctx *context.Context) {
var (
Expand Down
8 changes: 1 addition & 7 deletions routers/web/repo/setting/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -832,16 +832,10 @@ func SettingsPost(ctx *context.Context) {
} else {
ctx.ServerError("GetPendingRepositoryTransfer", err)
}

return
}

if err := repoTransfer.LoadAttributes(ctx); err != nil {
ctx.ServerError("LoadRecipient", err)
return
}

if err := repo_service.CancelRepositoryTransfer(ctx, ctx.Repo.Repository); err != nil {
if err := repo_service.CancelRepositoryTransfer(ctx, repoTransfer, ctx.Doer); err != nil {
ctx.ServerError("CancelRepositoryTransfer", err)
return
}
Expand Down
2 changes: 1 addition & 1 deletion services/context/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -653,7 +653,7 @@ func RepoAssignment(ctx *Context) {

ctx.Data["RepoTransfer"] = repoTransfer
if ctx.Doer != nil {
ctx.Data["CanUserAcceptTransfer"] = repoTransfer.CanUserAcceptTransfer(ctx, ctx.Doer)
ctx.Data["CanUserAcceptOrRejectTransfer"] = repoTransfer.CanUserAcceptOrRejectTransfer(ctx, ctx.Doer)
}
}

Expand Down
4 changes: 2 additions & 2 deletions services/notify/notify.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,9 +272,9 @@ func MigrateRepository(ctx context.Context, doer, u *user_model.User, repo *repo
}

// TransferRepository notifies create repository to notifiers
func TransferRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, newOwnerName string) {
func TransferRepository(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldOwnerName string) {
for _, notifier := range notifiers {
notifier.TransferRepository(ctx, doer, repo, newOwnerName)
notifier.TransferRepository(ctx, doer, repo, oldOwnerName)
}
}

Expand Down
Loading