mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 19:06:18 +01:00 
			
		
		
		
	Add API to manage repo tranfers (#17963)
This commit is contained in:
		| @@ -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 New Issue
	
	Block a user