Skip to content

Commit f7e56ce

Browse files
committed
squash
1 parent 6bb0f20 commit f7e56ce

File tree

9 files changed

+262
-10
lines changed

9 files changed

+262
-10
lines changed

integrations/api_repo_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,3 +392,54 @@ func testAPIRepoCreateConflict(t *testing.T, u *url.URL) {
392392
assert.Equal(t, respJSON["message"], "The repository with the same name already exists.")
393393
})
394394
}
395+
396+
func TestAPIRepoTransfer(t *testing.T) {
397+
testCases := []struct {
398+
ctxUserID int64
399+
newOwner string
400+
teams *[]int64
401+
expectedStatus int
402+
}{
403+
{ctxUserID: 1, newOwner: "user2", teams: nil, expectedStatus: http.StatusAccepted},
404+
{ctxUserID: 2, newOwner: "user1", teams: nil, expectedStatus: http.StatusAccepted},
405+
{ctxUserID: 2, newOwner: "user6", teams: nil, expectedStatus: http.StatusForbidden},
406+
{ctxUserID: 1, newOwner: "user2", teams: &[]int64{2}, expectedStatus: http.StatusUnprocessableEntity},
407+
{ctxUserID: 1, newOwner: "user3", teams: &[]int64{5}, expectedStatus: http.StatusForbidden},
408+
{ctxUserID: 1, newOwner: "user3", teams: &[]int64{2}, expectedStatus: http.StatusAccepted},
409+
}
410+
411+
defer prepareTestEnv(t)()
412+
413+
//create repo to move
414+
user := models.AssertExistsAndLoadBean(t, &models.User{ID: 1}).(*models.User)
415+
session := loginUser(t, user.Name)
416+
token := getTokenForLoggedInUser(t, session)
417+
repoName := "moveME"
418+
repo := new(models.Repository)
419+
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/user/repos?token=%s", token), &api.CreateRepoOption{
420+
Name: repoName,
421+
Description: "repo move around",
422+
Private: false,
423+
Readme: "Default",
424+
AutoInit: true,
425+
})
426+
resp := session.MakeRequest(t, req, http.StatusCreated)
427+
DecodeJSON(t, resp, repo)
428+
429+
//start testing
430+
for _, testCase := range testCases {
431+
user = models.AssertExistsAndLoadBean(t, &models.User{ID: testCase.ctxUserID}).(*models.User)
432+
repo = models.AssertExistsAndLoadBean(t, &models.Repository{ID: repo.ID}).(*models.Repository)
433+
session = loginUser(t, user.Name)
434+
token = getTokenForLoggedInUser(t, session)
435+
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/transfer?token=%s", repo.OwnerName, repo.Name, token), &api.TransferRepoOption{
436+
NewOwner: testCase.newOwner,
437+
TeamIDs: testCase.teams,
438+
})
439+
session.MakeRequest(t, req, testCase.expectedStatus)
440+
}
441+
442+
//cleanup
443+
repo = models.AssertExistsAndLoadBean(t, &models.Repository{ID: repo.ID}).(*models.Repository)
444+
_ = models.DeleteRepository(user, repo.OwnerID, repo.ID)
445+
}

modules/structs/repo.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,15 @@ type EditRepoOption struct {
158158
Archived *bool `json:"archived,omitempty"`
159159
}
160160

161+
// TransferRepoOption options when transfer a repository's ownership
162+
// swagger:model
163+
type TransferRepoOption struct {
164+
// required: true
165+
NewOwner string `json:"new_owner"`
166+
// ID of the team or teams to add to the repository. Teams can only be added to organization-owned repositories.
167+
TeamIDs *[]int64 `json:"team_ids"`
168+
}
169+
161170
// GitServiceType represents a git service
162171
type GitServiceType int
163172

routers/api/v1/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,7 @@ func RegisterRoutes(m *macaron.Macaron) {
620620
m.Combo("").Get(reqAnyRepoReader(), repo.Get).
621621
Delete(reqToken(), reqOwner(), repo.Delete).
622622
Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), context.RepoRef(), repo.Edit)
623+
m.Post("/transfer", reqOwner(), bind(api.TransferRepoOption{}), repo.Transfer)
623624
m.Combo("/notifications").
624625
Get(reqToken(), notify.ListRepoNotifications).
625626
Put(reqToken(), notify.ReadRepoNotifications)

routers/api/v1/repo/transfer.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Copyright 2014 The Gogs Authors. All rights reserved.
2+
// Copyright 2018 The Gitea Authors. All rights reserved.
3+
// Use of this source code is governed by a MIT-style
4+
// license that can be found in the LICENSE file.
5+
6+
package repo
7+
8+
import (
9+
"fmt"
10+
"net/http"
11+
12+
"code.gitea.io/gitea/models"
13+
"code.gitea.io/gitea/modules/context"
14+
"code.gitea.io/gitea/modules/convert"
15+
"code.gitea.io/gitea/modules/log"
16+
api "code.gitea.io/gitea/modules/structs"
17+
repo_service "code.gitea.io/gitea/services/repository"
18+
)
19+
20+
// Transfer transfers the ownership of a repository
21+
func Transfer(ctx *context.APIContext, opts api.TransferRepoOption) {
22+
// swagger:operation POST /repos/{owner}/{repo}/transfer repository repoTransfer
23+
// ---
24+
// summary: Transfer a repo ownership
25+
// produces:
26+
// - application/json
27+
// parameters:
28+
// - name: owner
29+
// in: path
30+
// description: owner of the repo to transfer
31+
// type: string
32+
// required: true
33+
// - name: repo
34+
// in: path
35+
// description: name of the repo to transfer
36+
// type: string
37+
// required: true
38+
// - name: body
39+
// in: body
40+
// description: "Transfer Options"
41+
// required: true
42+
// schema:
43+
// "$ref": "#/definitions/TransferRepoOption"
44+
// responses:
45+
// "202":
46+
// "$ref": "#/responses/Repository"
47+
// "403":
48+
// "$ref": "#/responses/forbidden"
49+
// "404":
50+
// "$ref": "#/responses/notFound"
51+
// "422":
52+
// "$ref": "#/responses/validationError"
53+
54+
newOwner, err := models.GetUserByName(opts.NewOwner)
55+
if err != nil {
56+
if models.IsErrUserNotExist(err) {
57+
ctx.Error(http.StatusNotFound, "GetUserByName", err)
58+
return
59+
}
60+
ctx.InternalServerError(err)
61+
return
62+
}
63+
64+
var teams []*models.Team
65+
if opts.TeamIDs != nil {
66+
if !newOwner.IsOrganization() {
67+
ctx.Error(http.StatusUnprocessableEntity, "repoTransfer", "Teams can only be added to organization-owned repositories")
68+
return
69+
}
70+
71+
org := convert.ToOrganization(newOwner)
72+
for _, tID := range *opts.TeamIDs {
73+
team, err := models.GetTeamByID(tID)
74+
if err != nil {
75+
ctx.Error(http.StatusUnprocessableEntity, "team", fmt.Errorf("team %d not found", tID))
76+
return
77+
}
78+
79+
if team.OrgID != org.ID {
80+
ctx.Error(http.StatusForbidden, "team", fmt.Errorf("team %d belongs not to org %d", tID, org.ID))
81+
return
82+
}
83+
84+
teams = append(teams, team)
85+
}
86+
}
87+
88+
if err = repo_service.TransferOwnership(ctx.User, newOwner, ctx.Repo.Repository, teams); err != nil {
89+
ctx.InternalServerError(err)
90+
return
91+
}
92+
93+
newRepo, err := models.GetRepositoryByName(newOwner.ID, ctx.Repo.Repository.Name)
94+
if err != nil {
95+
ctx.InternalServerError(err)
96+
return
97+
}
98+
99+
log.Trace("Repository transferred: %s -> %s", ctx.Repo.Repository.FullName(), newOwner.Name)
100+
ctx.JSON(http.StatusAccepted, newRepo.APIFormat(models.AccessModeAdmin))
101+
}

routers/api/v1/swagger/options.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ type swaggerParameterBodies struct {
8484
// in:body
8585
EditRepoOption api.EditRepoOption
8686
// in:body
87+
TransferRepoOption api.TransferRepoOption
88+
// in:body
8789
CreateForkOption api.CreateForkOption
8890

8991
// in:body

routers/repo/setting.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -369,22 +369,22 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) {
369369
return
370370
}
371371

372-
newOwner := ctx.Query("new_owner_name")
373-
isExist, err := models.IsUserExist(0, newOwner)
372+
newOwner, err := models.GetUserByName(ctx.Query("new_owner_name"))
374373
if err != nil {
374+
if models.IsErrUserNotExist(err) {
375+
ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil)
376+
return
377+
}
375378
ctx.ServerError("IsUserExist", err)
376379
return
377-
} else if !isExist {
378-
ctx.RenderWithErr(ctx.Tr("form.enterred_invalid_owner_name"), tplSettingsOptions, nil)
379-
return
380380
}
381381

382382
// Close the GitRepo if open
383383
if ctx.Repo.GitRepo != nil {
384384
ctx.Repo.GitRepo.Close()
385385
ctx.Repo.GitRepo = nil
386386
}
387-
if err = repo_service.TransferOwnership(ctx.User, newOwner, repo); err != nil {
387+
if err = repo_service.TransferOwnership(ctx.User, newOwner, repo, nil); err != nil {
388388
if models.IsErrRepoAlreadyExist(err) {
389389
ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplSettingsOptions, nil)
390390
} else {
@@ -395,7 +395,7 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) {
395395

396396
log.Trace("Repository transferred: %s/%s -> %s", ctx.Repo.Owner.Name, repo.Name, newOwner)
397397
ctx.Flash.Success(ctx.Tr("repo.settings.transfer_succeed"))
398-
ctx.Redirect(setting.AppSubURL + "/" + newOwner + "/" + repo.Name)
398+
ctx.Redirect(setting.AppSubURL + "/" + newOwner.Name + "/" + repo.Name)
399399

400400
case "delete":
401401
if !ctx.Repo.IsOwner() {

services/repository/transfer.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,34 @@ import (
1616
var repoWorkingPool = sync.NewExclusivePool()
1717

1818
// TransferOwnership transfers all corresponding setting from old user to new one.
19-
func TransferOwnership(doer *models.User, newOwnerName string, repo *models.Repository) error {
19+
func TransferOwnership(doer, newOwner *models.User, repo *models.Repository, teams []*models.Team) error {
2020
if err := repo.GetOwner(); err != nil {
2121
return err
2222
}
2323

2424
oldOwner := repo.Owner
2525

2626
repoWorkingPool.CheckIn(com.ToStr(repo.ID))
27-
if err := models.TransferOwnership(doer, newOwnerName, repo); err != nil {
27+
if err := models.TransferOwnership(doer, newOwner.Name, repo); err != nil {
2828
repoWorkingPool.CheckOut(com.ToStr(repo.ID))
2929
return err
3030
}
3131
repoWorkingPool.CheckOut(com.ToStr(repo.ID))
3232

33+
newRepo, err := models.GetRepositoryByID(repo.ID)
34+
if err != nil {
35+
return err
36+
}
37+
for _, team := range teams {
38+
if models.HasTeamRepo(newOwner.ID, team.ID, repo.ID) {
39+
continue
40+
}
41+
42+
if err := team.AddRepository(newRepo); err != nil {
43+
return err
44+
}
45+
}
46+
3347
notification.NotifyTransferRepository(doer, repo, oldOwner.Name)
3448

3549
return nil

services/repository/transfer_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func TestTransferOwnership(t *testing.T) {
3232
doer := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
3333
repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository)
3434
repo.Owner = models.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User)
35-
assert.NoError(t, TransferOwnership(doer, "user2", repo))
35+
assert.NoError(t, TransferOwnership(doer, doer, repo, nil))
3636

3737
transferredRepo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository)
3838
assert.EqualValues(t, 2, transferredRepo.OwnerID)

templates/swagger/v1_json.tmpl

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7321,6 +7321,57 @@
73217321
}
73227322
}
73237323
},
7324+
"/repos/{owner}/{repo}/transfer": {
7325+
"post": {
7326+
"produces": [
7327+
"application/json"
7328+
],
7329+
"tags": [
7330+
"repository"
7331+
],
7332+
"summary": "Transfer a repo ownership",
7333+
"operationId": "repoTransfer",
7334+
"parameters": [
7335+
{
7336+
"type": "string",
7337+
"description": "owner of the repo to transfer",
7338+
"name": "owner",
7339+
"in": "path",
7340+
"required": true
7341+
},
7342+
{
7343+
"type": "string",
7344+
"description": "name of the repo to transfer",
7345+
"name": "repo",
7346+
"in": "path",
7347+
"required": true
7348+
},
7349+
{
7350+
"description": "Transfer Options",
7351+
"name": "body",
7352+
"in": "body",
7353+
"required": true,
7354+
"schema": {
7355+
"$ref": "#/definitions/TransferRepoOption"
7356+
}
7357+
}
7358+
],
7359+
"responses": {
7360+
"202": {
7361+
"$ref": "#/responses/Repository"
7362+
},
7363+
"403": {
7364+
"$ref": "#/responses/forbidden"
7365+
},
7366+
"404": {
7367+
"$ref": "#/responses/notFound"
7368+
},
7369+
"422": {
7370+
"$ref": "#/responses/validationError"
7371+
}
7372+
}
7373+
}
7374+
},
73247375
"/repositories/{id}": {
73257376
"get": {
73267377
"produces": [
@@ -12580,6 +12631,29 @@
1258012631
},
1258112632
"x-go-package": "code.gitea.io/gitea/modules/structs"
1258212633
},
12634+
"TransferRepoOption": {
12635+
"description": "TransferRepoOption options when transfer a repository's ownership",
12636+
"type": "object",
12637+
"required": [
12638+
"new_owner"
12639+
],
12640+
"properties": {
12641+
"new_owner": {
12642+
"type": "string",
12643+
"x-go-name": "NewOwner"
12644+
},
12645+
"team_ids": {
12646+
"description": "ID of the team or teams to add to the repository. Teams can only be added to organization-owned repositories.",
12647+
"type": "array",
12648+
"items": {
12649+
"type": "integer",
12650+
"format": "int64"
12651+
},
12652+
"x-go-name": "TeamIDs"
12653+
}
12654+
},
12655+
"x-go-package": "code.gitea.io/gitea/modules/structs"
12656+
},
1258312657
"UpdateFileOptions": {
1258412658
"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)",
1258512659
"type": "object",

0 commit comments

Comments
 (0)