* squash * optimize * fail before make any changes * fix-headertags/v1.21.12.1
| @@ -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", | |||