1
1
mirror of https://github.com/go-gitea/gitea synced 2024-12-23 00:54:28 +00:00

Add API to manage repo tranfers (#17963)

This commit is contained in:
qwerty287 2021-12-24 05:26:52 +01:00 committed by GitHub
parent 5754080eb9
commit 7cc44491fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 322 additions and 0 deletions

View File

@ -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)()

View File

@ -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,
} }
} }

View File

@ -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"`
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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",