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