Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add API to manage repo tranfers #17963

Merged
merged 19 commits into from
Dec 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions integrations/api_repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,85 @@ func TestAPIRepoTransfer(t *testing.T) {
_ = models.DeleteRepository(user, repo.OwnerID, repo.ID)
}

func transfer(t *testing.T) *repo_model.Repository {
//create repo to move
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
session := loginUser(t, user.Name)
token := getTokenForLoggedInUser(t, session)
repoName := "moveME"
apiRepo := new(api.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, apiRepo)

repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: apiRepo.ID}).(*repo_model.Repository)
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/transfer?token=%s", repo.OwnerName, repo.Name, token), &api.TransferRepoOption{
NewOwner: "user4",
})
session.MakeRequest(t, req, http.StatusCreated)

return repo
}

func TestAPIAcceptTransfer(t *testing.T) {
defer prepareTestEnv(t)()

repo := transfer(t)

// try to accept with not authorized user
session := loginUser(t, "user2")
token := getTokenForLoggedInUser(t, session)
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/transfer/reject?token=%s", repo.OwnerName, repo.Name, token))
session.MakeRequest(t, req, http.StatusForbidden)

// try to accept repo that's not marked as transferred
req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/transfer/accept?token=%s", "user2", "repo1", token))
session.MakeRequest(t, req, http.StatusNotFound)

// accept transfer
session = loginUser(t, "user4")
token = getTokenForLoggedInUser(t, session)

req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/transfer/accept?token=%s", repo.OwnerName, repo.Name, token))
resp := session.MakeRequest(t, req, http.StatusAccepted)
apiRepo := new(api.Repository)
DecodeJSON(t, resp, apiRepo)
assert.Equal(t, "user4", apiRepo.Owner.UserName)
}

func TestAPIRejectTransfer(t *testing.T) {
defer prepareTestEnv(t)()

repo := transfer(t)

// try to reject with not authorized user
session := loginUser(t, "user2")
token := getTokenForLoggedInUser(t, session)
req := NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/transfer/reject?token=%s", repo.OwnerName, repo.Name, token))
session.MakeRequest(t, req, http.StatusForbidden)

// try to reject repo that's not marked as transferred
req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/transfer/reject?token=%s", "user2", "repo1", token))
session.MakeRequest(t, req, http.StatusNotFound)

// reject transfer
session = loginUser(t, "user4")
token = getTokenForLoggedInUser(t, session)

req = NewRequest(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/transfer/reject?token=%s", repo.OwnerName, repo.Name, token))
resp := session.MakeRequest(t, req, http.StatusOK)
apiRepo := new(api.Repository)
DecodeJSON(t, resp, apiRepo)
assert.Equal(t, "user2", apiRepo.Owner.UserName)
}

func TestAPIGenerateRepo(t *testing.T) {
defer prepareTestEnv(t)()

Expand Down
30 changes: 30 additions & 0 deletions modules/convert/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
unit_model "code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/log"
api "code.gitea.io/gitea/modules/structs"
)

Expand Down Expand Up @@ -106,6 +107,20 @@ func innerToRepo(repo *repo_model.Repository, mode perm.AccessMode, isParent boo
}
}

var transfer *api.RepoTransfer
if repo.Status == repo_model.RepositoryPendingTransfer {
t, err := models.GetPendingRepositoryTransfer(repo)
if err != nil && !models.IsErrNoPendingTransfer(err) {
log.Warn("GetPendingRepositoryTransfer: %v", err)
} else {
if err := t.LoadAttributes(); err != nil {
log.Warn("LoadAttributes of RepoTransfer: %v", err)
} else {
transfer = ToRepoTransfer(t)
}
}
}

return &api.Repository{
ID: repo.ID,
Owner: ToUserWithAccessMode(repo.Owner, mode),
Expand Down Expand Up @@ -151,5 +166,20 @@ func innerToRepo(repo *repo_model.Repository, mode perm.AccessMode, isParent boo
AvatarURL: repo.AvatarLink(),
Internal: !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePrivate,
MirrorInterval: mirrorInterval,
RepoTransfer: transfer,
}
}

// ToRepoTransfer convert a models.RepoTransfer to a structs.RepeTransfer
func ToRepoTransfer(t *models.RepoTransfer) *api.RepoTransfer {
var teams []*api.Team
for _, v := range t.Teams {
teams = append(teams, ToTeam(v))
}

return &api.RepoTransfer{
Doer: ToUser(t.Doer, nil),
Recipient: ToUser(t.Recipient, nil),
Teams: teams,
}
}
8 changes: 8 additions & 0 deletions modules/structs/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ type Repository struct {
AvatarURL string `json:"avatar_url"`
Internal bool `json:"internal"`
MirrorInterval string `json:"mirror_interval"`
RepoTransfer *RepoTransfer `json:"repo_transfer"`
lunny marked this conversation as resolved.
Show resolved Hide resolved
}

// CreateRepoOption options when creating repository
Expand Down Expand Up @@ -336,3 +337,10 @@ var (
CodebaseService,
}
)

// RepoTransfer represents a pending repo transfer
type RepoTransfer struct {
Doer *User `json:"doer"`
Recipient *User `json:"recipient"`
Teams []*Team `json:"teams"`
}
2 changes: 2 additions & 0 deletions routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,8 @@ func Routes(sessioner func(http.Handler) http.Handler) *web.Route {
Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), repo.Edit)
m.Post("/generate", reqToken(), reqRepoReader(unit.TypeCode), bind(api.GenerateRepoOption{}), repo.Generate)
m.Post("/transfer", reqOwner(), bind(api.TransferRepoOption{}), repo.Transfer)
m.Post("/transfer/accept", reqToken(), repo.AcceptTransfer)
m.Post("/transfer/reject", reqToken(), repo.RejectTransfer)
m.Combo("/notifications").
Get(reqToken(), notify.ListRepoNotifications).
Put(reqToken(), notify.ReadRepoNotifications)
Expand Down
102 changes: 102 additions & 0 deletions routers/api/v1/repo/transfer.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,105 @@ func Transfer(ctx *context.APIContext) {
log.Trace("Repository transferred: %s -> %s", ctx.Repo.Repository.FullName(), newOwner.Name)
ctx.JSON(http.StatusAccepted, convert.ToRepo(ctx.Repo.Repository, perm.AccessModeAdmin))
}

// AcceptTransfer accept a repo transfer
func AcceptTransfer(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/transfer/accept repository acceptRepoTransfer
// ---
// summary: Accept a repo transfer
// 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
// responses:
// "202":
// "$ref": "#/responses/Repository"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"

err := acceptOrRejectRepoTransfer(ctx, true)
if ctx.Written() {
return
}
if err != nil {
ctx.Error(http.StatusInternalServerError, "acceptOrRejectRepoTransfer", err)
return
}

ctx.JSON(http.StatusAccepted, convert.ToRepo(ctx.Repo.Repository, ctx.Repo.AccessMode))
}

// RejectTransfer reject a repo transfer
func RejectTransfer(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/transfer/reject repository rejectRepoTransfer
// ---
// summary: Reject a repo transfer
// 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
// responses:
// "200":
// "$ref": "#/responses/Repository"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"

err := acceptOrRejectRepoTransfer(ctx, false)
if ctx.Written() {
return
}
if err != nil {
ctx.Error(http.StatusInternalServerError, "acceptOrRejectRepoTransfer", err)
return
}

ctx.JSON(http.StatusOK, convert.ToRepo(ctx.Repo.Repository, ctx.Repo.AccessMode))
}

func acceptOrRejectRepoTransfer(ctx *context.APIContext, accept bool) error {
repoTransfer, err := models.GetPendingRepositoryTransfer(ctx.Repo.Repository)
if err != nil {
6543 marked this conversation as resolved.
Show resolved Hide resolved
if models.IsErrNoPendingTransfer(err) {
ctx.NotFound()
return nil
}
return err
}

if err := repoTransfer.LoadAttributes(); err != nil {
return err
}

if !repoTransfer.CanUserAcceptTransfer(ctx.User) {
ctx.Error(http.StatusForbidden, "CanUserAcceptTransfer", nil)
return fmt.Errorf("user does not have permissions to do this")
}

if accept {
return repo_service.TransferOwnership(repoTransfer.Doer, repoTransfer.Recipient, ctx.Repo.Repository, repoTransfer.Teams)
}

return models.CancelRepositoryTransfer(ctx.Repo.Repository)
}
101 changes: 101 additions & 0 deletions templates/swagger/v1_json.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -9895,6 +9895,84 @@
}
}
},
"/repos/{owner}/{repo}/transfer/accept": {
"post": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Accept a repo transfer",
"operationId": "acceptRepoTransfer",
"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
}
],
"responses": {
"202": {
"$ref": "#/responses/Repository"
},
"403": {
"$ref": "#/responses/forbidden"
},
"404": {
"$ref": "#/responses/notFound"
}
}
}
},
"/repos/{owner}/{repo}/transfer/reject": {
"post": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Reject a repo transfer",
"operationId": "rejectRepoTransfer",
"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
}
],
"responses": {
"200": {
"$ref": "#/responses/Repository"
},
"403": {
"$ref": "#/responses/forbidden"
},
"404": {
"$ref": "#/responses/notFound"
}
}
}
},
"/repos/{owner}/{repo}/wiki/new": {
"post": {
"consumes": [
Expand Down Expand Up @@ -16890,6 +16968,26 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"RepoTransfer": {
"description": "RepoTransfer represents a pending repo transfer",
"type": "object",
"properties": {
"doer": {
"$ref": "#/definitions/User"
},
"recipient": {
"$ref": "#/definitions/User"
},
"teams": {
"type": "array",
"items": {
"$ref": "#/definitions/Team"
},
"x-go-name": "Teams"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"Repository": {
"description": "Repository represents a repository",
"type": "object",
Expand Down Expand Up @@ -17042,6 +17140,9 @@
"format": "int64",
"x-go-name": "Releases"
},
"repo_transfer": {
"$ref": "#/definitions/RepoTransfer"
},
"size": {
"type": "integer",
"format": "int64",
Expand Down