* squash * optimize * fail before make any changes * fix-headertags/v1.10.5
@@ -392,3 +392,54 @@ func testAPIRepoCreateConflict(t *testing.T, u *url.URL) { | |||
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) | |||
} |
@@ -158,6 +158,15 @@ type EditRepoOption struct { | |||
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 | |||
type GitServiceType int | |||
@@ -620,6 +620,7 @@ func RegisterRoutes(m *macaron.Macaron) { | |||
m.Combo("").Get(reqAnyRepoReader(), repo.Get). | |||
Delete(reqToken(), reqOwner(), repo.Delete). | |||
Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), context.RepoRef(), repo.Edit) | |||
m.Post("/transfer", reqOwner(), bind(api.TransferRepoOption{}), repo.Transfer) | |||
m.Combo("/notifications"). | |||
Get(reqToken(), notify.ListRepoNotifications). | |||
Put(reqToken(), notify.ReadRepoNotifications) |
@@ -0,0 +1,100 @@ | |||
// 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)) | |||
} |
@@ -84,6 +84,8 @@ type swaggerParameterBodies struct { | |||
// in:body | |||
EditRepoOption api.EditRepoOption | |||
// in:body | |||
TransferRepoOption api.TransferRepoOption | |||
// in:body | |||
CreateForkOption api.CreateForkOption | |||
// in:body |
@@ -369,14 +369,14 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) { | |||
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 models.IsErrUserNotExist(err) { | |||
ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil) | |||
return | |||
} | |||
ctx.ServerError("IsUserExist", err) | |||
return | |||
} else if !isExist { | |||
ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil) | |||
return | |||
} | |||
// Close the GitRepo if open | |||
@@ -384,7 +384,7 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) { | |||
ctx.Repo.GitRepo.Close() | |||
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) { | |||
ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil) | |||
} else { | |||
@@ -395,7 +395,7 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) { | |||
log.Trace("Repository transferred: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newOwner) | |||
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": | |||
if !ctx.Repo.IsOwner() { |
@@ -5,6 +5,8 @@ | |||
package repository | |||
import ( | |||
"fmt" | |||
"code.gitea.io/gitea/models" | |||
"code.gitea.io/gitea/modules/notification" | |||
"code.gitea.io/gitea/modules/sync" | |||
@@ -16,20 +18,36 @@ import ( | |||
var repoWorkingPool = sync.NewExclusivePool() | |||
// 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 { | |||
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 | |||
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)) | |||
return err | |||
} | |||
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) | |||
return nil |
@@ -32,7 +32,7 @@ func TestTransferOwnership(t *testing.T) { | |||
doer := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) | |||
repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository) | |||
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) | |||
assert.EqualValues(t, 2, transferredRepo.OwnerID) |
@@ -7321,6 +7321,57 @@ | |||
} | |||
} | |||
}, | |||
"/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}": { | |||
"get": { | |||
"produces": [ | |||
@@ -12580,6 +12631,29 @@ | |||
}, | |||
"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": { | |||
"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", |