* squash * optimize * fail before make any changes * fix-headertags/v1.10.5
assert.Equal(t, respJSON["message"], "The repository with the same name already exists.") | assert.Equal(t, respJSON["message"], "The repository with the same name already exists.") | ||||
}) | }) | ||||
} | } | ||||
func TestAPIRepoTransfer(t *testing.T) { | |||||
testCases := []struct { | |||||
ctxUserID int64 | |||||
newOwner string | |||||
teams *[]int64 | |||||
expectedStatus int | |||||
}{ | |||||
{ctxUserID: 1, newOwner: "user2", teams: nil, expectedStatus: http.StatusAccepted}, | |||||
{ctxUserID: 2, newOwner: "user1", teams: nil, expectedStatus: http.StatusAccepted}, | |||||
{ctxUserID: 2, newOwner: "user6", teams: nil, expectedStatus: http.StatusForbidden}, | |||||
{ctxUserID: 1, newOwner: "user2", teams: &[]int64{2}, expectedStatus: http.StatusUnprocessableEntity}, | |||||
{ctxUserID: 1, newOwner: "user3", teams: &[]int64{5}, expectedStatus: http.StatusForbidden}, | |||||
{ctxUserID: 1, newOwner: "user3", teams: &[]int64{2}, expectedStatus: http.StatusAccepted}, | |||||
} | |||||
defer prepareTestEnv(t)() | |||||
//create repo to move | |||||
user := models.AssertExistsAndLoadBean(t, &models.User{ID: 1}).(*models.User) | |||||
session := loginUser(t, user.Name) | |||||
token := getTokenForLoggedInUser(t, session) | |||||
repoName := "moveME" | |||||
repo := new(models.Repository) | |||||
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/user/repos?token=%s", token), &api.CreateRepoOption{ | |||||
Name: repoName, | |||||
Description: "repo move around", | |||||
Private: false, | |||||
Readme: "Default", | |||||
AutoInit: true, | |||||
}) | |||||
resp := session.MakeRequest(t, req, http.StatusCreated) | |||||
DecodeJSON(t, resp, repo) | |||||
//start testing | |||||
for _, testCase := range testCases { | |||||
user = models.AssertExistsAndLoadBean(t, &models.User{ID: testCase.ctxUserID}).(*models.User) | |||||
repo = models.AssertExistsAndLoadBean(t, &models.Repository{ID: repo.ID}).(*models.Repository) | |||||
session = loginUser(t, user.Name) | |||||
token = getTokenForLoggedInUser(t, session) | |||||
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/transfer?token=%s", repo.OwnerName, repo.Name, token), &api.TransferRepoOption{ | |||||
NewOwner: testCase.newOwner, | |||||
TeamIDs: testCase.teams, | |||||
}) | |||||
session.MakeRequest(t, req, testCase.expectedStatus) | |||||
} | |||||
//cleanup | |||||
repo = models.AssertExistsAndLoadBean(t, &models.Repository{ID: repo.ID}).(*models.Repository) | |||||
_ = models.DeleteRepository(user, repo.OwnerID, repo.ID) | |||||
} |
Archived *bool `json:"archived,omitempty"` | Archived *bool `json:"archived,omitempty"` | ||||
} | } | ||||
// TransferRepoOption options when transfer a repository's ownership | |||||
// swagger:model | |||||
type TransferRepoOption struct { | |||||
// required: true | |||||
NewOwner string `json:"new_owner"` | |||||
// ID of the team or teams to add to the repository. Teams can only be added to organization-owned repositories. | |||||
TeamIDs *[]int64 `json:"team_ids"` | |||||
} | |||||
// GitServiceType represents a git service | // GitServiceType represents a git service | ||||
type GitServiceType int | type GitServiceType int | ||||
m.Combo("").Get(reqAnyRepoReader(), repo.Get). | m.Combo("").Get(reqAnyRepoReader(), repo.Get). | ||||
Delete(reqToken(), reqOwner(), repo.Delete). | Delete(reqToken(), reqOwner(), repo.Delete). | ||||
Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), context.RepoRef(), repo.Edit) | Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), context.RepoRef(), repo.Edit) | ||||
m.Post("/transfer", reqOwner(), bind(api.TransferRepoOption{}), repo.Transfer) | |||||
m.Combo("/notifications"). | m.Combo("/notifications"). | ||||
Get(reqToken(), notify.ListRepoNotifications). | Get(reqToken(), notify.ListRepoNotifications). | ||||
Put(reqToken(), notify.ReadRepoNotifications) | Put(reqToken(), notify.ReadRepoNotifications) |
// Copyright 2020 The Gitea Authors. All rights reserved. | |||||
// Use of this source code is governed by a MIT-style | |||||
// license that can be found in the LICENSE file. | |||||
package repo | |||||
import ( | |||||
"fmt" | |||||
"net/http" | |||||
"code.gitea.io/gitea/models" | |||||
"code.gitea.io/gitea/modules/context" | |||||
"code.gitea.io/gitea/modules/convert" | |||||
"code.gitea.io/gitea/modules/log" | |||||
api "code.gitea.io/gitea/modules/structs" | |||||
repo_service "code.gitea.io/gitea/services/repository" | |||||
) | |||||
// Transfer transfers the ownership of a repository | |||||
func Transfer(ctx *context.APIContext, opts api.TransferRepoOption) { | |||||
// swagger:operation POST /repos/{owner}/{repo}/transfer repository repoTransfer | |||||
// --- | |||||
// summary: Transfer a repo ownership | |||||
// produces: | |||||
// - application/json | |||||
// parameters: | |||||
// - name: owner | |||||
// in: path | |||||
// description: owner of the repo to transfer | |||||
// type: string | |||||
// required: true | |||||
// - name: repo | |||||
// in: path | |||||
// description: name of the repo to transfer | |||||
// type: string | |||||
// required: true | |||||
// - name: body | |||||
// in: body | |||||
// description: "Transfer Options" | |||||
// required: true | |||||
// schema: | |||||
// "$ref": "#/definitions/TransferRepoOption" | |||||
// responses: | |||||
// "202": | |||||
// "$ref": "#/responses/Repository" | |||||
// "403": | |||||
// "$ref": "#/responses/forbidden" | |||||
// "404": | |||||
// "$ref": "#/responses/notFound" | |||||
// "422": | |||||
// "$ref": "#/responses/validationError" | |||||
newOwner, err := models.GetUserByName(opts.NewOwner) | |||||
if err != nil { | |||||
if models.IsErrUserNotExist(err) { | |||||
ctx.Error(http.StatusNotFound, "GetUserByName", err) | |||||
return | |||||
} | |||||
ctx.InternalServerError(err) | |||||
return | |||||
} | |||||
var teams []*models.Team | |||||
if opts.TeamIDs != nil { | |||||
if !newOwner.IsOrganization() { | |||||
ctx.Error(http.StatusUnprocessableEntity, "repoTransfer", "Teams can only be added to organization-owned repositories") | |||||
return | |||||
} | |||||
org := convert.ToOrganization(newOwner) | |||||
for _, tID := range *opts.TeamIDs { | |||||
team, err := models.GetTeamByID(tID) | |||||
if err != nil { | |||||
ctx.Error(http.StatusUnprocessableEntity, "team", fmt.Errorf("team %d not found", tID)) | |||||
return | |||||
} | |||||
if team.OrgID != org.ID { | |||||
ctx.Error(http.StatusForbidden, "team", fmt.Errorf("team %d belongs not to org %d", tID, org.ID)) | |||||
return | |||||
} | |||||
teams = append(teams, team) | |||||
} | |||||
} | |||||
if err = repo_service.TransferOwnership(ctx.User, newOwner, ctx.Repo.Repository, teams); err != nil { | |||||
ctx.InternalServerError(err) | |||||
return | |||||
} | |||||
newRepo, err := models.GetRepositoryByName(newOwner.ID, ctx.Repo.Repository.Name) | |||||
if err != nil { | |||||
ctx.InternalServerError(err) | |||||
return | |||||
} | |||||
log.Trace("Repository transferred: %s -> %s", ctx.Repo.Repository.FullName(), newOwner.Name) | |||||
ctx.JSON(http.StatusAccepted, newRepo.APIFormat(models.AccessModeAdmin)) | |||||
} |
// in:body | // in:body | ||||
EditRepoOption api.EditRepoOption | EditRepoOption api.EditRepoOption | ||||
// in:body | // in:body | ||||
TransferRepoOption api.TransferRepoOption | |||||
// in:body | |||||
CreateForkOption api.CreateForkOption | CreateForkOption api.CreateForkOption | ||||
// in:body | // in:body |
return | return | ||||
} | } | ||||
newOwner := ctx.Query("new_owner_name") | |||||
isExist, err := models.IsUserExist(0, newOwner) | |||||
newOwner, err := models.GetUserByName(ctx.Query("new_owner_name")) | |||||
if err != nil { | if err != nil { | ||||
if models.IsErrUserNotExist(err) { | |||||
ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil) | |||||
return | |||||
} | |||||
ctx.ServerError("IsUserExist", err) | ctx.ServerError("IsUserExist", err) | ||||
return | return | ||||
} else if !isExist { | |||||
ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil) | |||||
return | |||||
} | } | ||||
// Close the GitRepo if open | // Close the GitRepo if open | ||||
ctx.Repo.GitRepo.Close() | ctx.Repo.GitRepo.Close() | ||||
ctx.Repo.GitRepo = nil | ctx.Repo.GitRepo = nil | ||||
} | } | ||||
if err = repo_service.TransferOwnership(ctx.User, newOwner, repo); err != nil { | |||||
if err = repo_service.TransferOwnership(ctx.User, newOwner, repo, nil); err != nil { | |||||
if models.IsErrRepoAlreadyExist(err) { | if models.IsErrRepoAlreadyExist(err) { | ||||
ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil) | ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil) | ||||
} else { | } else { | ||||
log.Trace("Repository transferred: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newOwner) | log.Trace("Repository transferred: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newOwner) | ||||
ctx.Flash.Success(ctx.Tr("repo.settings.transfer_succeed")) | ctx.Flash.Success(ctx.Tr("repo.settings.transfer_succeed")) | ||||
ctx.Redirect(setting.AppSubURL + "/" + newOwner + "/" + repo.Name) | |||||
ctx.Redirect(setting.AppSubURL + "/" + newOwner.Name + "/" + repo.Name) | |||||
case "delete": | case "delete": | ||||
if !ctx.Repo.IsOwner() { | if !ctx.Repo.IsOwner() { |
package repository | package repository | ||||
import ( | import ( | ||||
"fmt" | |||||
"code.gitea.io/gitea/models" | "code.gitea.io/gitea/models" | ||||
"code.gitea.io/gitea/modules/notification" | "code.gitea.io/gitea/modules/notification" | ||||
"code.gitea.io/gitea/modules/sync" | "code.gitea.io/gitea/modules/sync" | ||||
var repoWorkingPool = sync.NewExclusivePool() | var repoWorkingPool = sync.NewExclusivePool() | ||||
// TransferOwnership transfers all corresponding setting from old user to new one. | // TransferOwnership transfers all corresponding setting from old user to new one. | ||||
func TransferOwnership(doer *models.User, newOwnerName string, repo *models.Repository) error { | |||||
func TransferOwnership(doer, newOwner *models.User, repo *models.Repository, teams []*models.Team) error { | |||||
if err := repo.GetOwner(); err != nil { | if err := repo.GetOwner(); err != nil { | ||||
return err | 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 | oldOwner := repo.Owner | ||||
repoWorkingPool.CheckIn(com.ToStr(repo.ID)) | repoWorkingPool.CheckIn(com.ToStr(repo.ID)) | ||||
if err := models.TransferOwnership(doer, newOwnerName, repo); err != nil { | |||||
if err := models.TransferOwnership(doer, newOwner.Name, repo); err != nil { | |||||
repoWorkingPool.CheckOut(com.ToStr(repo.ID)) | repoWorkingPool.CheckOut(com.ToStr(repo.ID)) | ||||
return err | return err | ||||
} | } | ||||
repoWorkingPool.CheckOut(com.ToStr(repo.ID)) | repoWorkingPool.CheckOut(com.ToStr(repo.ID)) | ||||
newRepo, err := models.GetRepositoryByID(repo.ID) | |||||
if err != nil { | |||||
return err | |||||
} | |||||
for _, team := range teams { | |||||
if err := team.AddRepository(newRepo); err != nil { | |||||
return err | |||||
} | |||||
} | |||||
notification.NotifyTransferRepository(doer, repo, oldOwner.Name) | notification.NotifyTransferRepository(doer, repo, oldOwner.Name) | ||||
return nil | return nil |
doer := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) | doer := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) | ||||
repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository) | repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository) | ||||
repo.Owner = models.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User) | repo.Owner = models.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User) | ||||
assert.NoError(t, TransferOwnership(doer, "user2", repo)) | |||||
assert.NoError(t, TransferOwnership(doer, doer, repo, nil)) | |||||
transferredRepo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository) | transferredRepo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository) | ||||
assert.EqualValues(t, 2, transferredRepo.OwnerID) | assert.EqualValues(t, 2, transferredRepo.OwnerID) |
} | } | ||||
} | } | ||||
}, | }, | ||||
"/repos/{owner}/{repo}/transfer": { | |||||
"post": { | |||||
"produces": [ | |||||
"application/json" | |||||
], | |||||
"tags": [ | |||||
"repository" | |||||
], | |||||
"summary": "Transfer a repo ownership", | |||||
"operationId": "repoTransfer", | |||||
"parameters": [ | |||||
{ | |||||
"type": "string", | |||||
"description": "owner of the repo to transfer", | |||||
"name": "owner", | |||||
"in": "path", | |||||
"required": true | |||||
}, | |||||
{ | |||||
"type": "string", | |||||
"description": "name of the repo to transfer", | |||||
"name": "repo", | |||||
"in": "path", | |||||
"required": true | |||||
}, | |||||
{ | |||||
"description": "Transfer Options", | |||||
"name": "body", | |||||
"in": "body", | |||||
"required": true, | |||||
"schema": { | |||||
"$ref": "#/definitions/TransferRepoOption" | |||||
} | |||||
} | |||||
], | |||||
"responses": { | |||||
"202": { | |||||
"$ref": "#/responses/Repository" | |||||
}, | |||||
"403": { | |||||
"$ref": "#/responses/forbidden" | |||||
}, | |||||
"404": { | |||||
"$ref": "#/responses/notFound" | |||||
}, | |||||
"422": { | |||||
"$ref": "#/responses/validationError" | |||||
} | |||||
} | |||||
} | |||||
}, | |||||
"/repositories/{id}": { | "/repositories/{id}": { | ||||
"get": { | "get": { | ||||
"produces": [ | "produces": [ | ||||
}, | }, | ||||
"x-go-package": "code.gitea.io/gitea/modules/structs" | "x-go-package": "code.gitea.io/gitea/modules/structs" | ||||
}, | }, | ||||
"TransferRepoOption": { | |||||
"description": "TransferRepoOption options when transfer a repository's ownership", | |||||
"type": "object", | |||||
"required": [ | |||||
"new_owner" | |||||
], | |||||
"properties": { | |||||
"new_owner": { | |||||
"type": "string", | |||||
"x-go-name": "NewOwner" | |||||
}, | |||||
"team_ids": { | |||||
"description": "ID of the team or teams to add to the repository. Teams can only be added to organization-owned repositories.", | |||||
"type": "array", | |||||
"items": { | |||||
"type": "integer", | |||||
"format": "int64" | |||||
}, | |||||
"x-go-name": "TeamIDs" | |||||
} | |||||
}, | |||||
"x-go-package": "code.gitea.io/gitea/modules/structs" | |||||
}, | |||||
"UpdateFileOptions": { | "UpdateFileOptions": { | ||||
"description": "UpdateFileOptions options for updating files\nNote: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)", | "description": "UpdateFileOptions options for updating files\nNote: `author` and `committer` are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)", | ||||
"type": "object", | "type": "object", |