Add API to manage repo tranfers (#17963)
This commit is contained in:
parent
5754080eb9
commit
7cc44491fa
6 changed files with 322 additions and 0 deletions
|
@ -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)()
|
||||
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
||||
// CreateRepoOption options when creating repository
|
||||
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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": [
|
||||
|
@ -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",
|
||||
|
@ -17042,6 +17140,9 @@
|
|||
"format": "int64",
|
||||
"x-go-name": "Releases"
|
||||
},
|
||||
"repo_transfer": {
|
||||
"$ref": "#/definitions/RepoTransfer"
|
||||
},
|
||||
"size": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
|
|
Reference in a new issue