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)
|
_ = 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) {
|
func TestAPIGenerateRepo(t *testing.T) {
|
||||||
defer prepareTestEnv(t)()
|
defer prepareTestEnv(t)()
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"code.gitea.io/gitea/models/perm"
|
"code.gitea.io/gitea/models/perm"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
unit_model "code.gitea.io/gitea/models/unit"
|
unit_model "code.gitea.io/gitea/models/unit"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
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{
|
return &api.Repository{
|
||||||
ID: repo.ID,
|
ID: repo.ID,
|
||||||
Owner: ToUserWithAccessMode(repo.Owner, mode),
|
Owner: ToUserWithAccessMode(repo.Owner, mode),
|
||||||
|
@ -151,5 +166,20 @@ func innerToRepo(repo *repo_model.Repository, mode perm.AccessMode, isParent boo
|
||||||
AvatarURL: repo.AvatarLink(),
|
AvatarURL: repo.AvatarLink(),
|
||||||
Internal: !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePrivate,
|
Internal: !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePrivate,
|
||||||
MirrorInterval: mirrorInterval,
|
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"`
|
AvatarURL string `json:"avatar_url"`
|
||||||
Internal bool `json:"internal"`
|
Internal bool `json:"internal"`
|
||||||
MirrorInterval string `json:"mirror_interval"`
|
MirrorInterval string `json:"mirror_interval"`
|
||||||
|
RepoTransfer *RepoTransfer `json:"repo_transfer"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateRepoOption options when creating repository
|
// CreateRepoOption options when creating repository
|
||||||
|
@ -336,3 +337,10 @@ var (
|
||||||
CodebaseService,
|
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)
|
Patch(reqToken(), reqAdmin(), bind(api.EditRepoOption{}), repo.Edit)
|
||||||
m.Post("/generate", reqToken(), reqRepoReader(unit.TypeCode), bind(api.GenerateRepoOption{}), repo.Generate)
|
m.Post("/generate", reqToken(), reqRepoReader(unit.TypeCode), bind(api.GenerateRepoOption{}), repo.Generate)
|
||||||
m.Post("/transfer", reqOwner(), bind(api.TransferRepoOption{}), repo.Transfer)
|
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").
|
m.Combo("/notifications").
|
||||||
Get(reqToken(), notify.ListRepoNotifications).
|
Get(reqToken(), notify.ListRepoNotifications).
|
||||||
Put(reqToken(), notify.ReadRepoNotifications)
|
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)
|
log.Trace("Repository transferred: %s -> %s", ctx.Repo.Repository.FullName(), newOwner.Name)
|
||||||
ctx.JSON(http.StatusAccepted, convert.ToRepo(ctx.Repo.Repository, perm.AccessModeAdmin))
|
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": {
|
"/repos/{owner}/{repo}/wiki/new": {
|
||||||
"post": {
|
"post": {
|
||||||
"consumes": [
|
"consumes": [
|
||||||
|
@ -16890,6 +16968,26 @@
|
||||||
},
|
},
|
||||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
"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": {
|
"Repository": {
|
||||||
"description": "Repository represents a repository",
|
"description": "Repository represents a repository",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
@ -17042,6 +17140,9 @@
|
||||||
"format": "int64",
|
"format": "int64",
|
||||||
"x-go-name": "Releases"
|
"x-go-name": "Releases"
|
||||||
},
|
},
|
||||||
|
"repo_transfer": {
|
||||||
|
"$ref": "#/definitions/RepoTransfer"
|
||||||
|
},
|
||||||
"size": {
|
"size": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"format": "int64",
|
"format": "int64",
|
||||||
|
|
Reference in a new issue