mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +09:00 
			
		
		
		
	Refactor repository transfer (#33211)
- Both have `RejectTransfer` and `CancelTransfer` because the permission checks are not the same. `CancelTransfer` can be done by the doer or those who have admin permission to access this repository. `RejectTransfer` can be done by the receiver user if it's an individual or those who can create repositories if it's an organization. - Some tests are wrong, this PR corrects them. --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
		| @@ -68,6 +68,7 @@ type RepoTransfer struct { //nolint | |||||||
| 	RecipientID int64 | 	RecipientID int64 | ||||||
| 	Recipient   *user_model.User `xorm:"-"` | 	Recipient   *user_model.User `xorm:"-"` | ||||||
| 	RepoID      int64 | 	RepoID      int64 | ||||||
|  | 	Repo        *Repository `xorm:"-"` | ||||||
| 	TeamIDs     []int64 | 	TeamIDs     []int64 | ||||||
| 	Teams       []*organization.Team `xorm:"-"` | 	Teams       []*organization.Team `xorm:"-"` | ||||||
|  |  | ||||||
| @@ -79,48 +80,65 @@ func init() { | |||||||
| 	db.RegisterModel(new(RepoTransfer)) | 	db.RegisterModel(new(RepoTransfer)) | ||||||
| } | } | ||||||
|  |  | ||||||
| // LoadAttributes fetches the transfer recipient from the database | func (r *RepoTransfer) LoadRecipient(ctx context.Context) error { | ||||||
| func (r *RepoTransfer) LoadAttributes(ctx context.Context) error { |  | ||||||
| 	if r.Recipient == nil { | 	if r.Recipient == nil { | ||||||
| 		u, err := user_model.GetUserByID(ctx, r.RecipientID) | 		u, err := user_model.GetUserByID(ctx, r.RecipientID) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		r.Recipient = u | 		r.Recipient = u | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if r.Recipient.IsOrganization() && len(r.TeamIDs) != len(r.Teams) { | 	return nil | ||||||
| 		for _, v := range r.TeamIDs { | } | ||||||
| 			team, err := organization.GetTeamByID(ctx, v) |  | ||||||
| 			if err != nil { |  | ||||||
| 				return err |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			if team.OrgID != r.Recipient.ID { | func (r *RepoTransfer) LoadRepo(ctx context.Context) error { | ||||||
| 				return fmt.Errorf("team %d belongs not to org %d", v, r.Recipient.ID) | 	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) | 			r.Teams = append(r.Teams, team) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if err := r.LoadRepo(ctx); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	if r.Doer == nil { | 	if r.Doer == nil { | ||||||
| 		u, err := user_model.GetUserByID(ctx, r.DoerID) | 		u, err := user_model.GetUserByID(ctx, r.DoerID) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		r.Doer = u | 		r.Doer = u | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return nil | 	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 user, it checks if it's himself | ||||||
| // For organizations, it checks if the user is able to create repos | // 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 { | 	if err := r.LoadAttributes(ctx); err != nil { | ||||||
| 		log.Error("LoadAttributes: %v", err) | 		log.Error("LoadAttributes: %v", err) | ||||||
| 		return false | 		return false | ||||||
| @@ -166,6 +184,10 @@ func GetPendingRepositoryTransfers(ctx context.Context, opts *PendingRepositoryT | |||||||
| 		Find(&transfers) | 		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 | // GetPendingRepositoryTransfer fetches the most recent and ongoing transfer | ||||||
| // process for the repository | // process for the repository | ||||||
| func GetPendingRepositoryTransfer(ctx context.Context, repo *Repository) (*RepoTransfer, error) { | func GetPendingRepositoryTransfer(ctx context.Context, repo *Repository) (*RepoTransfer, error) { | ||||||
| @@ -206,11 +228,26 @@ func CreatePendingRepositoryTransfer(ctx context.Context, doer, newOwner *user_m | |||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		if _, err := user_model.GetUserByID(ctx, newOwner.ID); err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		// Make sure repo is ready to transfer | 		// Make sure repo is ready to transfer | ||||||
| 		if err := TestRepositoryReadyForTransfer(repo.Status); err != nil { | 		if err := TestRepositoryReadyForTransfer(repo.Status); err != nil { | ||||||
| 			return err | 			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 | 		repo.Status = RepositoryPendingTransfer | ||||||
| 		if err := UpdateRepositoryCols(ctx, repo, "status"); err != nil { | 		if err := UpdateRepositoryCols(ctx, repo, "status"); err != nil { | ||||||
| 			return err | 			return err | ||||||
|   | |||||||
| @@ -15,6 +15,7 @@ import ( | |||||||
| 	user_model "code.gitea.io/gitea/models/user" | 	user_model "code.gitea.io/gitea/models/user" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| 	api "code.gitea.io/gitea/modules/structs" | 	api "code.gitea.io/gitea/modules/structs" | ||||||
|  | 	"code.gitea.io/gitea/modules/util" | ||||||
| 	"code.gitea.io/gitea/modules/web" | 	"code.gitea.io/gitea/modules/web" | ||||||
| 	"code.gitea.io/gitea/services/context" | 	"code.gitea.io/gitea/services/context" | ||||||
| 	"code.gitea.io/gitea/services/convert" | 	"code.gitea.io/gitea/services/convert" | ||||||
| @@ -161,12 +162,16 @@ func AcceptTransfer(ctx *context.APIContext) { | |||||||
| 	//   "404": | 	//   "404": | ||||||
| 	//     "$ref": "#/responses/notFound" | 	//     "$ref": "#/responses/notFound" | ||||||
|  |  | ||||||
| 	err := acceptOrRejectRepoTransfer(ctx, true) | 	err := repo_service.AcceptTransferOwnership(ctx, ctx.Repo.Repository, ctx.Doer) | ||||||
| 	if ctx.Written() { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	if err != nil { | 	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 | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -199,40 +204,18 @@ func RejectTransfer(ctx *context.APIContext) { | |||||||
| 	//   "404": | 	//   "404": | ||||||
| 	//     "$ref": "#/responses/notFound" | 	//     "$ref": "#/responses/notFound" | ||||||
|  |  | ||||||
| 	err := acceptOrRejectRepoTransfer(ctx, false) | 	err := repo_service.RejectRepositoryTransfer(ctx, ctx.Repo.Repository, ctx.Doer) | ||||||
| 	if ctx.Written() { |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	if err != nil { | 	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 | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	ctx.JSON(http.StatusOK, convert.ToRepo(ctx, ctx.Repo.Repository, ctx.Repo.Permission)) | 	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) |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -309,6 +309,36 @@ const ( | |||||||
| 	tplStarUnstar   templates.TplName = "repo/star_unstar" | 	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 | // Action response for actions to a repository | ||||||
| func Action(ctx *context.Context) { | func Action(ctx *context.Context) { | ||||||
| 	var err error | 	var err error | ||||||
| @@ -322,9 +352,11 @@ func Action(ctx *context.Context) { | |||||||
| 	case "unstar": | 	case "unstar": | ||||||
| 		err = repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, false) | 		err = repo_model.StarRepo(ctx, ctx.Doer, ctx.Repo.Repository, false) | ||||||
| 	case "accept_transfer": | 	case "accept_transfer": | ||||||
| 		err = acceptOrRejectRepoTransfer(ctx, true) | 		acceptTransfer(ctx) | ||||||
|  | 		return | ||||||
| 	case "reject_transfer": | 	case "reject_transfer": | ||||||
| 		err = acceptOrRejectRepoTransfer(ctx, false) | 		rejectTransfer(ctx) | ||||||
|  | 		return | ||||||
| 	case "desc": // FIXME: this is not used | 	case "desc": // FIXME: this is not used | ||||||
| 		if !ctx.Repo.IsOwner() { | 		if !ctx.Repo.IsOwner() { | ||||||
| 			ctx.Error(http.StatusNotFound) | 			ctx.Error(http.StatusNotFound) | ||||||
| @@ -337,12 +369,8 @@ func Action(ctx *context.Context) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		if errors.Is(err, user_model.ErrBlockedUser) { | 		handleActionError(ctx, err) | ||||||
| 			ctx.Flash.Error(ctx.Tr("repo.action.blocked_user")) | 		return | ||||||
| 		} else { |  | ||||||
| 			ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.PathParam("action")), err) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	switch ctx.PathParam("action") { | 	switch ctx.PathParam("action") { | ||||||
| @@ -377,41 +405,6 @@ func Action(ctx *context.Context) { | |||||||
| 	ctx.RedirectToCurrentSite(ctx.FormString("redirect_to"), ctx.Repo.RepoLink) | 	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: | // RedirectDownload return a file based on the following infos: | ||||||
| func RedirectDownload(ctx *context.Context) { | func RedirectDownload(ctx *context.Context) { | ||||||
| 	var ( | 	var ( | ||||||
|   | |||||||
| @@ -832,16 +832,10 @@ func SettingsPost(ctx *context.Context) { | |||||||
| 			} else { | 			} else { | ||||||
| 				ctx.ServerError("GetPendingRepositoryTransfer", err) | 				ctx.ServerError("GetPendingRepositoryTransfer", err) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if err := repoTransfer.LoadAttributes(ctx); err != nil { | 		if err := repo_service.CancelRepositoryTransfer(ctx, repoTransfer, ctx.Doer); err != nil { | ||||||
| 			ctx.ServerError("LoadRecipient", err) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if err := repo_service.CancelRepositoryTransfer(ctx, ctx.Repo.Repository); err != nil { |  | ||||||
| 			ctx.ServerError("CancelRepositoryTransfer", err) | 			ctx.ServerError("CancelRepositoryTransfer", err) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -653,7 +653,7 @@ func RepoAssignment(ctx *Context) { | |||||||
|  |  | ||||||
| 		ctx.Data["RepoTransfer"] = repoTransfer | 		ctx.Data["RepoTransfer"] = repoTransfer | ||||||
| 		if ctx.Doer != nil { | 		if ctx.Doer != nil { | ||||||
| 			ctx.Data["CanUserAcceptTransfer"] = repoTransfer.CanUserAcceptTransfer(ctx, ctx.Doer) | 			ctx.Data["CanUserAcceptOrRejectTransfer"] = repoTransfer.CanUserAcceptOrRejectTransfer(ctx, ctx.Doer) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -272,9 +272,9 @@ func MigrateRepository(ctx context.Context, doer, u *user_model.User, repo *repo | |||||||
| } | } | ||||||
|  |  | ||||||
| // TransferRepository notifies create repository to notifiers | // 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 { | 	for _, notifier := range notifiers { | ||||||
| 		notifier.TransferRepository(ctx, doer, repo, newOwnerName) | 		notifier.TransferRepository(ctx, doer, repo, oldOwnerName) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -27,19 +27,8 @@ func getRepoWorkingLockKey(repoID int64) string { | |||||||
| 	return fmt.Sprintf("repo_working_%d", repoID) | 	return fmt.Sprintf("repo_working_%d", repoID) | ||||||
| } | } | ||||||
|  |  | ||||||
| // TransferOwnership transfers all corresponding setting from old user to new one. | // AcceptTransferOwnership transfers all corresponding setting from old user to new one. | ||||||
| func TransferOwnership(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository, teams []*organization.Team) error { | func AcceptTransferOwnership(ctx context.Context, repo *repo_model.Repository, doer *user_model.User) error { | ||||||
| 	if err := repo.LoadOwner(ctx); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	for _, team := range teams { |  | ||||||
| 		if newOwner.ID != team.OrgID { |  | ||||||
| 			return fmt.Errorf("team %d does not belong to organization", team.ID) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	oldOwner := repo.Owner |  | ||||||
|  |  | ||||||
| 	releaser, err := globallock.Lock(ctx, getRepoWorkingLockKey(repo.ID)) | 	releaser, err := globallock.Lock(ctx, getRepoWorkingLockKey(repo.ID)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Error("lock.Lock(): %v", err) | 		log.Error("lock.Lock(): %v", err) | ||||||
| @@ -47,29 +36,44 @@ func TransferOwnership(ctx context.Context, doer, newOwner *user_model.User, rep | |||||||
| 	} | 	} | ||||||
| 	defer releaser() | 	defer releaser() | ||||||
|  |  | ||||||
| 	if err := transferOwnership(ctx, doer, newOwner.Name, repo); err != nil { | 	repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, repo) | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	releaser() |  | ||||||
|  |  | ||||||
| 	newRepo, err := repo_model.GetRepositoryByID(ctx, repo.ID) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for _, team := range teams { | 	oldOwnerName := repo.OwnerName | ||||||
| 		if err := addRepositoryToTeam(ctx, team, newRepo); err != nil { |  | ||||||
|  | 	if err := db.WithTx(ctx, func(ctx context.Context) error { | ||||||
|  | 		if err := repoTransfer.LoadAttributes(ctx); err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	notify_service.TransferRepository(ctx, doer, repo, oldOwner.Name) | 		if !repoTransfer.CanUserAcceptOrRejectTransfer(ctx, doer) { | ||||||
|  | 			return util.ErrPermissionDenied | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if err := repo.LoadOwner(ctx); err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		for _, team := range repoTransfer.Teams { | ||||||
|  | 			if repoTransfer.Recipient.ID != team.OrgID { | ||||||
|  | 				return fmt.Errorf("team %d does not belong to organization", team.ID) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return transferOwnership(ctx, repoTransfer.Doer, repoTransfer.Recipient.Name, repo, repoTransfer.Teams) | ||||||
|  | 	}); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	releaser() | ||||||
|  |  | ||||||
|  | 	notify_service.TransferRepository(ctx, doer, repo, oldOwnerName) | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // transferOwnership transfers all corresponding repository items from old user to new one. | // 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) { | func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName string, repo *repo_model.Repository, teams []*organization.Team) (err error) { | ||||||
| 	repoRenamed := false | 	repoRenamed := false | ||||||
| 	wikiRenamed := false | 	wikiRenamed := false | ||||||
| 	oldOwnerName := doer.Name | 	oldOwnerName := doer.Name | ||||||
| @@ -138,7 +142,7 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName | |||||||
| 	repo.OwnerName = newOwner.Name | 	repo.OwnerName = newOwner.Name | ||||||
|  |  | ||||||
| 	// Update repository. | 	// Update repository. | ||||||
| 	if _, err := sess.ID(repo.ID).Update(repo); err != nil { | 	if err := repo_model.UpdateRepositoryCols(ctx, repo, "owner_id", "owner_name"); err != nil { | ||||||
| 		return fmt.Errorf("update owner: %w", err) | 		return fmt.Errorf("update owner: %w", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -174,15 +178,13 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName | |||||||
| 		collaboration.UserID = 0 | 		collaboration.UserID = 0 | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Remove old team-repository relations. |  | ||||||
| 	if oldOwner.IsOrganization() { | 	if oldOwner.IsOrganization() { | ||||||
|  | 		// Remove old team-repository relations. | ||||||
| 		if err := organization.RemoveOrgRepo(ctx, oldOwner.ID, repo.ID); err != nil { | 		if err := organization.RemoveOrgRepo(ctx, oldOwner.ID, repo.ID); err != nil { | ||||||
| 			return fmt.Errorf("removeOrgRepo: %w", err) | 			return fmt.Errorf("removeOrgRepo: %w", err) | ||||||
| 		} | 		} | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Remove project's issues that belong to old organization's projects | 		// Remove project's issues that belong to old organization's projects | ||||||
| 	if oldOwner.IsOrganization() { |  | ||||||
| 		projects, err := project_model.GetAllProjectsIDsByOwnerIDAndType(ctx, oldOwner.ID, project_model.TypeOrganization) | 		projects, err := project_model.GetAllProjectsIDsByOwnerIDAndType(ctx, oldOwner.ID, project_model.TypeOrganization) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return fmt.Errorf("Unable to find old org projects: %w", err) | 			return fmt.Errorf("Unable to find old org projects: %w", err) | ||||||
| @@ -225,15 +227,13 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName | |||||||
| 		return fmt.Errorf("watchRepo: %w", err) | 		return fmt.Errorf("watchRepo: %w", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Remove watch for organization. |  | ||||||
| 	if oldOwner.IsOrganization() { | 	if oldOwner.IsOrganization() { | ||||||
|  | 		// Remove watch for organization. | ||||||
| 		if err := repo_model.WatchRepo(ctx, oldOwner, repo, false); err != nil { | 		if err := repo_model.WatchRepo(ctx, oldOwner, repo, false); err != nil { | ||||||
| 			return fmt.Errorf("watchRepo [false]: %w", err) | 			return fmt.Errorf("watchRepo [false]: %w", err) | ||||||
| 		} | 		} | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Delete labels that belong to the old organization and comments that added these labels | 		// 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 ( | 		if _, err := sess.Exec(`DELETE FROM issue_label WHERE issue_label.id IN ( | ||||||
| 			SELECT il_too.id FROM ( | 			SELECT il_too.id FROM ( | ||||||
| 				SELECT il_too_too.id | 				SELECT il_too_too.id | ||||||
| @@ -261,7 +261,6 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName | |||||||
|  |  | ||||||
| 	// Rename remote repository to new path and delete local copy. | 	// Rename remote repository to new path and delete local copy. | ||||||
| 	dir := user_model.UserPath(newOwner.Name) | 	dir := user_model.UserPath(newOwner.Name) | ||||||
|  |  | ||||||
| 	if err := os.MkdirAll(dir, os.ModePerm); err != nil { | 	if err := os.MkdirAll(dir, os.ModePerm); err != nil { | ||||||
| 		return fmt.Errorf("Failed to create dir %s: %w", dir, err) | 		return fmt.Errorf("Failed to create dir %s: %w", dir, err) | ||||||
| 	} | 	} | ||||||
| @@ -273,7 +272,6 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName | |||||||
|  |  | ||||||
| 	// Rename remote wiki repository to new path and delete local copy. | 	// Rename remote wiki repository to new path and delete local copy. | ||||||
| 	wikiPath := repo_model.WikiPath(oldOwner.Name, repo.Name) | 	wikiPath := repo_model.WikiPath(oldOwner.Name, repo.Name) | ||||||
|  |  | ||||||
| 	if isExist, err := util.IsExist(wikiPath); err != nil { | 	if isExist, err := util.IsExist(wikiPath); err != nil { | ||||||
| 		log.Error("Unable to check if %s exists. Error: %v", wikiPath, err) | 		log.Error("Unable to check if %s exists. Error: %v", wikiPath, err) | ||||||
| 		return err | 		return err | ||||||
| @@ -301,6 +299,17 @@ func transferOwnership(ctx context.Context, doer *user_model.User, newOwnerName | |||||||
| 		return fmt.Errorf("repo_model.NewRedirect: %w", err) | 		return fmt.Errorf("repo_model.NewRedirect: %w", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	newRepo, err := repo_model.GetRepositoryByID(ctx, repo.ID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for _, team := range teams { | ||||||
|  | 		if err := addRepositoryToTeam(ctx, team, newRepo); err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	return committer.Commit() | 	return committer.Commit() | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -343,17 +352,9 @@ func changeRepositoryName(ctx context.Context, repo *repo_model.Repository, newR | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	ctx, committer, err := db.TxContext(ctx) | 	return db.WithTx(ctx, func(ctx context.Context) error { | ||||||
| 	if err != nil { | 		return repo_model.NewRedirect(ctx, repo.Owner.ID, repo.ID, oldRepoName, newRepoName) | ||||||
| 		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. | // ChangeRepositoryName changes all corresponding setting from old repository name to new one. | ||||||
| @@ -387,70 +388,142 @@ func ChangeRepositoryName(ctx context.Context, doer *user_model.User, repo *repo | |||||||
| // StartRepositoryTransfer transfer a repo from one owner to a new one. | // StartRepositoryTransfer transfer a repo from one owner to a new one. | ||||||
| // it make repository into pending transfer state, if doer can not create repo for new owner. | // it make repository into pending transfer state, if doer can not create repo for new owner. | ||||||
| func StartRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository, teams []*organization.Team) error { | func StartRepositoryTransfer(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository, teams []*organization.Team) error { | ||||||
|  | 	releaser, err := globallock.Lock(ctx, getRepoWorkingLockKey(repo.ID)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("lock.Lock: %w", err) | ||||||
|  | 	} | ||||||
|  | 	defer releaser() | ||||||
|  |  | ||||||
| 	if err := repo_model.TestRepositoryReadyForTransfer(repo.Status); err != nil { | 	if err := repo_model.TestRepositoryReadyForTransfer(repo.Status); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Admin is always allowed to transfer || user transfer repo back to his account | 	var isDirectTransfer bool | ||||||
| 	if doer.IsAdmin || doer.ID == newOwner.ID { | 	oldOwnerName := repo.OwnerName | ||||||
| 		return TransferOwnership(ctx, doer, newOwner, repo, teams) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if user_model.IsUserBlockedBy(ctx, doer, newOwner.ID) { | 	if err := db.WithTx(ctx, func(ctx context.Context) error { | ||||||
| 		return user_model.ErrBlockedUser | 		// Admin is always allowed to transfer || user transfer repo back to his account, | ||||||
| 	} | 		// then it will transfer directly without acceptance. | ||||||
|  | 		if doer.IsAdmin || doer.ID == newOwner.ID { | ||||||
|  | 			isDirectTransfer = true | ||||||
|  | 			return transferOwnership(ctx, doer, newOwner.Name, repo, teams) | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 	// If new owner is an org and user can create repos he can transfer directly too | 		if user_model.IsUserBlockedBy(ctx, doer, newOwner.ID) { | ||||||
| 	if newOwner.IsOrganization() { | 			return user_model.ErrBlockedUser | ||||||
| 		allowed, err := organization.CanCreateOrgRepo(ctx, newOwner.ID, doer.ID) | 		} | ||||||
|  |  | ||||||
|  | 		// If new owner is an org and user can create repos he can transfer directly too | ||||||
|  | 		if newOwner.IsOrganization() { | ||||||
|  | 			allowed, err := organization.CanCreateOrgRepo(ctx, newOwner.ID, doer.ID) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 			if allowed { | ||||||
|  | 				isDirectTransfer = true | ||||||
|  | 				return transferOwnership(ctx, doer, newOwner.Name, repo, teams) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// In case the new owner would not have sufficient access to the repo, give access rights for read | ||||||
|  | 		hasAccess, err := access_model.HasAnyUnitAccess(ctx, newOwner.ID, repo) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 		if allowed { | 		if !hasAccess { | ||||||
| 			return TransferOwnership(ctx, doer, newOwner, repo, teams) | 			if err := AddOrUpdateCollaborator(ctx, repo, newOwner, perm.AccessModeRead); err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// In case the new owner would not have sufficient access to the repo, give access rights for read | 		// Make repo as pending for transfer | ||||||
| 	hasAccess, err := access_model.HasAnyUnitAccess(ctx, newOwner.ID, repo) | 		repo.Status = repo_model.RepositoryPendingTransfer | ||||||
| 	if err != nil { | 		return repo_model.CreatePendingRepositoryTransfer(ctx, doer, newOwner, repo.ID, teams) | ||||||
| 		return err | 	}); err != nil { | ||||||
| 	} |  | ||||||
| 	if !hasAccess { |  | ||||||
| 		if err := AddOrUpdateCollaborator(ctx, repo, newOwner, perm.AccessModeRead); err != nil { |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Make repo as pending for transfer |  | ||||||
| 	repo.Status = repo_model.RepositoryPendingTransfer |  | ||||||
| 	if err := repo_model.CreatePendingRepositoryTransfer(ctx, doer, newOwner, repo.ID, teams); err != nil { |  | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// notify users who are able to accept / reject transfer | 	if isDirectTransfer { | ||||||
| 	notify_service.RepoPendingTransfer(ctx, doer, newOwner, repo) | 		notify_service.TransferRepository(ctx, doer, repo, oldOwnerName) | ||||||
|  | 	} else { | ||||||
|  | 		// notify users who are able to accept / reject transfer | ||||||
|  | 		notify_service.RepoPendingTransfer(ctx, doer, newOwner, repo) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // CancelRepositoryTransfer marks the repository as ready and remove pending transfer entry, | // RejectRepositoryTransfer marks the repository as ready and remove pending transfer entry, | ||||||
| // thus cancel the transfer process. | // thus cancel the transfer process. | ||||||
| func CancelRepositoryTransfer(ctx context.Context, repo *repo_model.Repository) error { | // The accepter can reject the transfer. | ||||||
| 	ctx, committer, err := db.TxContext(ctx) | func RejectRepositoryTransfer(ctx context.Context, repo *repo_model.Repository, doer *user_model.User) error { | ||||||
| 	if err != nil { | 	return db.WithTx(ctx, func(ctx context.Context) error { | ||||||
| 		return err | 		repoTransfer, err := repo_model.GetPendingRepositoryTransfer(ctx, repo) | ||||||
| 	} | 		if err != nil { | ||||||
| 	defer committer.Close() | 			return err | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 	repo.Status = repo_model.RepositoryReady | 		if err := repoTransfer.LoadAttributes(ctx); err != nil { | ||||||
| 	if err := repo_model.UpdateRepositoryCols(ctx, repo, "status"); err != nil { | 			return err | ||||||
| 		return err | 		} | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if err := repo_model.DeleteRepositoryTransfer(ctx, repo.ID); err != nil { | 		if !repoTransfer.CanUserAcceptOrRejectTransfer(ctx, doer) { | ||||||
| 		return err | 			return util.ErrPermissionDenied | ||||||
| 	} | 		} | ||||||
|  |  | ||||||
| 	return committer.Commit() | 		repo.Status = repo_model.RepositoryReady | ||||||
|  | 		if err := repo_model.UpdateRepositoryCols(ctx, repo, "status"); err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return repo_model.DeleteRepositoryTransfer(ctx, repo.ID) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func canUserCancelTransfer(ctx context.Context, r *repo_model.RepoTransfer, u *user_model.User) bool { | ||||||
|  | 	if u.IsAdmin || u.ID == r.DoerID { | ||||||
|  | 		return true | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := r.LoadAttributes(ctx); err != nil { | ||||||
|  | 		log.Error("LoadAttributes: %v", err) | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := r.Repo.LoadOwner(ctx); err != nil { | ||||||
|  | 		log.Error("LoadOwner: %v", err) | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if !r.Repo.Owner.IsOrganization() { | ||||||
|  | 		return r.Repo.OwnerID == u.ID | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	perm, err := access_model.GetUserRepoPermission(ctx, r.Repo, u) | ||||||
|  | 	if err != nil { | ||||||
|  | 		log.Error("GetUserRepoPermission: %v", err) | ||||||
|  | 		return false | ||||||
|  | 	} | ||||||
|  | 	return perm.IsOwner() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // CancelRepositoryTransfer cancels the repository transfer process. The sender or | ||||||
|  | // the users who have admin permission of the original repository can cancel the transfer | ||||||
|  | func CancelRepositoryTransfer(ctx context.Context, repoTransfer *repo_model.RepoTransfer, doer *user_model.User) error { | ||||||
|  | 	return db.WithTx(ctx, func(ctx context.Context) error { | ||||||
|  | 		if err := repoTransfer.LoadAttributes(ctx); err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if !canUserCancelTransfer(ctx, repoTransfer, doer) { | ||||||
|  | 			return util.ErrPermissionDenied | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		repoTransfer.Repo.Status = repo_model.RepositoryReady | ||||||
|  | 		if err := repo_model.UpdateRepositoryCols(ctx, repoTransfer.Repo, "status"); err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return repo_model.DeleteRepositoryTransfer(ctx, repoTransfer.RepoID) | ||||||
|  | 	}) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -34,23 +34,26 @@ func TestTransferOwnership(t *testing.T) { | |||||||
|  |  | ||||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||||
|  |  | ||||||
| 	doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) | 	doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) | ||||||
| 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) | 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) | ||||||
| 	repo.Owner = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) | 	assert.NoError(t, repo.LoadOwner(db.DefaultContext)) | ||||||
| 	assert.NoError(t, TransferOwnership(db.DefaultContext, doer, doer, repo, nil)) | 	repoTransfer := unittest.AssertExistsAndLoadBean(t, &repo_model.RepoTransfer{ID: 1}) | ||||||
|  | 	assert.NoError(t, repoTransfer.LoadAttributes(db.DefaultContext)) | ||||||
|  | 	assert.NoError(t, AcceptTransferOwnership(db.DefaultContext, repo, doer)) | ||||||
|  |  | ||||||
| 	transferredRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) | 	transferredRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) | ||||||
| 	assert.EqualValues(t, 2, transferredRepo.OwnerID) | 	assert.EqualValues(t, 1, transferredRepo.OwnerID) // repo_transfer.yml id=1 | ||||||
|  | 	unittest.AssertNotExistsBean(t, &repo_model.RepoTransfer{ID: 1}) | ||||||
|  |  | ||||||
| 	exist, err := util.IsExist(repo_model.RepoPath("org3", "repo3")) | 	exist, err := util.IsExist(repo_model.RepoPath("org3", "repo3")) | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 	assert.False(t, exist) | 	assert.False(t, exist) | ||||||
| 	exist, err = util.IsExist(repo_model.RepoPath("user2", "repo3")) | 	exist, err = util.IsExist(repo_model.RepoPath("user1", "repo3")) | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| 	assert.True(t, exist) | 	assert.True(t, exist) | ||||||
| 	unittest.AssertExistsAndLoadBean(t, &activities_model.Action{ | 	unittest.AssertExistsAndLoadBean(t, &activities_model.Action{ | ||||||
| 		OpType:    activities_model.ActionTransferRepo, | 		OpType:    activities_model.ActionTransferRepo, | ||||||
| 		ActUserID: 2, | 		ActUserID: 1, | ||||||
| 		RepoID:    3, | 		RepoID:    3, | ||||||
| 		Content:   "org3/repo3", | 		Content:   "org3/repo3", | ||||||
| 	}) | 	}) | ||||||
| @@ -61,10 +64,10 @@ func TestTransferOwnership(t *testing.T) { | |||||||
| func TestStartRepositoryTransferSetPermission(t *testing.T) { | func TestStartRepositoryTransferSetPermission(t *testing.T) { | ||||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||||
|  |  | ||||||
| 	doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) | 	doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) | ||||||
| 	recipient := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) | 	recipient := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5}) | ||||||
| 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) | 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) | ||||||
| 	repo.Owner = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) | 	assert.NoError(t, repo.LoadOwner(db.DefaultContext)) | ||||||
|  |  | ||||||
| 	hasAccess, err := access_model.HasAnyUnitAccess(db.DefaultContext, recipient.ID, repo) | 	hasAccess, err := access_model.HasAnyUnitAccess(db.DefaultContext, recipient.ID, repo) | ||||||
| 	assert.NoError(t, err) | 	assert.NoError(t, err) | ||||||
| @@ -82,7 +85,7 @@ func TestStartRepositoryTransferSetPermission(t *testing.T) { | |||||||
| func TestRepositoryTransfer(t *testing.T) { | func TestRepositoryTransfer(t *testing.T) { | ||||||
| 	assert.NoError(t, unittest.PrepareTestDatabase()) | 	assert.NoError(t, unittest.PrepareTestDatabase()) | ||||||
|  |  | ||||||
| 	doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) | 	doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) | ||||||
| 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) | 	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) | ||||||
|  |  | ||||||
| 	transfer, err := repo_model.GetPendingRepositoryTransfer(db.DefaultContext, repo) | 	transfer, err := repo_model.GetPendingRepositoryTransfer(db.DefaultContext, repo) | ||||||
| @@ -90,7 +93,7 @@ func TestRepositoryTransfer(t *testing.T) { | |||||||
| 	assert.NotNil(t, transfer) | 	assert.NotNil(t, transfer) | ||||||
|  |  | ||||||
| 	// Cancel transfer | 	// Cancel transfer | ||||||
| 	assert.NoError(t, CancelRepositoryTransfer(db.DefaultContext, repo)) | 	assert.NoError(t, CancelRepositoryTransfer(db.DefaultContext, transfer, doer)) | ||||||
|  |  | ||||||
| 	transfer, err = repo_model.GetPendingRepositoryTransfer(db.DefaultContext, repo) | 	transfer, err = repo_model.GetPendingRepositoryTransfer(db.DefaultContext, repo) | ||||||
| 	assert.Error(t, err) | 	assert.Error(t, err) | ||||||
| @@ -113,10 +116,12 @@ func TestRepositoryTransfer(t *testing.T) { | |||||||
| 	assert.Error(t, err) | 	assert.Error(t, err) | ||||||
| 	assert.True(t, repo_model.IsErrRepoTransferInProgress(err)) | 	assert.True(t, repo_model.IsErrRepoTransferInProgress(err)) | ||||||
|  |  | ||||||
| 	// Unknown user | 	repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2}) | ||||||
| 	err = repo_model.CreatePendingRepositoryTransfer(db.DefaultContext, doer, &user_model.User{ID: 1000, LowerName: "user1000"}, repo.ID, nil) | 	// Unknown user, transfer non-existent transfer repo id = 2 | ||||||
|  | 	err = repo_model.CreatePendingRepositoryTransfer(db.DefaultContext, doer, &user_model.User{ID: 1000, LowerName: "user1000"}, repo2.ID, nil) | ||||||
| 	assert.Error(t, err) | 	assert.Error(t, err) | ||||||
|  |  | ||||||
| 	// Cancel transfer | 	// Reject transfer | ||||||
| 	assert.NoError(t, CancelRepositoryTransfer(db.DefaultContext, repo)) | 	err = RejectRepositoryTransfer(db.DefaultContext, repo2, doer) | ||||||
|  | 	assert.True(t, repo_model.IsErrNoPendingTransfer(err)) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -117,10 +117,10 @@ func BlockUser(ctx context.Context, doer, blocker, blockee *user_model.User, not | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// cancel each other repository transfers | 		// cancel each other repository transfers | ||||||
| 		if err := cancelRepositoryTransfers(ctx, blocker, blockee); err != nil { | 		if err := cancelRepositoryTransfers(ctx, doer, blocker, blockee); err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 		if err := cancelRepositoryTransfers(ctx, blockee, blocker); err != nil { | 		if err := cancelRepositoryTransfers(ctx, doer, blockee, blocker); err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| @@ -192,7 +192,7 @@ func unwatchRepos(ctx context.Context, watcher, repoOwner *user_model.User) erro | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func cancelRepositoryTransfers(ctx context.Context, sender, recipient *user_model.User) error { | func cancelRepositoryTransfers(ctx context.Context, doer, sender, recipient *user_model.User) error { | ||||||
| 	transfers, err := repo_model.GetPendingRepositoryTransfers(ctx, &repo_model.PendingRepositoryTransferOptions{ | 	transfers, err := repo_model.GetPendingRepositoryTransfers(ctx, &repo_model.PendingRepositoryTransferOptions{ | ||||||
| 		SenderID:    sender.ID, | 		SenderID:    sender.ID, | ||||||
| 		RecipientID: recipient.ID, | 		RecipientID: recipient.ID, | ||||||
| @@ -202,12 +202,7 @@ func cancelRepositoryTransfers(ctx context.Context, sender, recipient *user_mode | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for _, transfer := range transfers { | 	for _, transfer := range transfers { | ||||||
| 		repo, err := repo_model.GetRepositoryByID(ctx, transfer.RepoID) | 		if err := repo_service.CancelRepositoryTransfer(ctx, transfer, doer); err != nil { | ||||||
| 		if err != nil { |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if err := repo_service.CancelRepositoryTransfer(ctx, repo); err != nil { |  | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -39,16 +39,16 @@ | |||||||
| 					{{if $.RepoTransfer}} | 					{{if $.RepoTransfer}} | ||||||
| 						<form method="post" action="{{$.RepoLink}}/action/accept_transfer?redirect_to={{$.RepoLink}}"> | 						<form method="post" action="{{$.RepoLink}}/action/accept_transfer?redirect_to={{$.RepoLink}}"> | ||||||
| 							{{$.CsrfTokenHtml}} | 							{{$.CsrfTokenHtml}} | ||||||
| 							<div data-tooltip-content="{{if $.CanUserAcceptTransfer}}{{ctx.Locale.Tr "repo.transfer.accept_desc" $.RepoTransfer.Recipient.DisplayName}}{{else}}{{ctx.Locale.Tr "repo.transfer.no_permission_to_accept"}}{{end}}"> | 							<div data-tooltip-content="{{if $.CanUserAcceptOrRejectTransfer}}{{ctx.Locale.Tr "repo.transfer.accept_desc" $.RepoTransfer.Recipient.DisplayName}}{{else}}{{ctx.Locale.Tr "repo.transfer.no_permission_to_accept"}}{{end}}"> | ||||||
| 								<button type="submit" class="ui basic button {{if $.CanUserAcceptTransfer}}primary {{end}} ok small"{{if not $.CanUserAcceptTransfer}} disabled{{end}}> | 								<button type="submit" class="ui basic button {{if $.CanUserAcceptOrRejectTransfer}}primary {{end}} ok small"{{if not $.CanUserAcceptOrRejectTransfer}} disabled{{end}}> | ||||||
| 									{{ctx.Locale.Tr "repo.transfer.accept"}} | 									{{ctx.Locale.Tr "repo.transfer.accept"}} | ||||||
| 								</button> | 								</button> | ||||||
| 							</div> | 							</div> | ||||||
| 						</form> | 						</form> | ||||||
| 						<form method="post" action="{{$.RepoLink}}/action/reject_transfer?redirect_to={{$.RepoLink}}"> | 						<form method="post" action="{{$.RepoLink}}/action/reject_transfer?redirect_to={{$.RepoLink}}"> | ||||||
| 							{{$.CsrfTokenHtml}} | 							{{$.CsrfTokenHtml}} | ||||||
| 							<div data-tooltip-content="{{if $.CanUserAcceptTransfer}}{{ctx.Locale.Tr "repo.transfer.reject_desc" $.RepoTransfer.Recipient.DisplayName}}{{else}}{{ctx.Locale.Tr "repo.transfer.no_permission_to_reject"}}{{end}}"> | 							<div data-tooltip-content="{{if $.CanUserAcceptOrRejectTransfer}}{{ctx.Locale.Tr "repo.transfer.reject_desc" $.RepoTransfer.Recipient.DisplayName}}{{else}}{{ctx.Locale.Tr "repo.transfer.no_permission_to_reject"}}{{end}}"> | ||||||
| 								<button type="submit" class="ui basic button {{if $.CanUserAcceptTransfer}}red {{end}}ok small"{{if not $.CanUserAcceptTransfer}} disabled{{end}}> | 								<button type="submit" class="ui basic button {{if $.CanUserAcceptOrRejectTransfer}}red {{end}}ok small"{{if not $.CanUserAcceptOrRejectTransfer}} disabled{{end}}> | ||||||
| 									{{ctx.Locale.Tr "repo.transfer.reject"}} | 									{{ctx.Locale.Tr "repo.transfer.reject"}} | ||||||
| 								</button> | 								</button> | ||||||
| 							</div> | 							</div> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user