From 4c0ec221faaed7ea6cea0ad5c19d3f79473a7c40 Mon Sep 17 00:00:00 2001 From: Dinesh Salunke Date: Sat, 18 Nov 2023 18:30:30 +0530 Subject: [PATCH 01/31] test: add 2 fixtures for project --- models/fixtures/project.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/models/fixtures/project.yml b/models/fixtures/project.yml index 1bf8030f6aa57..c07a4002a4d9c 100644 --- a/models/fixtures/project.yml +++ b/models/fixtures/project.yml @@ -45,3 +45,29 @@ type: 2 created_unix: 1688973000 updated_unix: 1688973000 + +# user projects +- + id: 5 + title: project on user2 + owner_id: 2 + repo_id: 0 + is_closed: false + creator_id: 2 + board_type: 1 + type: 1 + created_unix: 1688973000 + updated_unix: 1688973000 + +# org projects +- + id: 6 + title: project on org17 + owner_id: 17 + repo_id: 0 + is_closed: false + creator_id: 17 + board_type: 1 + type: 3 + created_unix: 1688973000 + updated_unix: 1688973000 From 6092b81563b5a7f3c9614512216ed4644c4c0fc4 Mon Sep 17 00:00:00 2001 From: Dinesh Salunke Date: Sat, 18 Nov 2023 18:31:43 +0530 Subject: [PATCH 02/31] refactor: add structs for project --- modules/structs/project.go | 40 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 modules/structs/project.go diff --git a/modules/structs/project.go b/modules/structs/project.go new file mode 100644 index 0000000000000..e70659e5ceeed --- /dev/null +++ b/modules/structs/project.go @@ -0,0 +1,40 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package structs + +import "time" + +type NewProjectPayload struct { + // required:true + Title string `json:"title" binding:"Required"` + // required:true + BoardType uint8 `json:"board_type"` + // required:true + CardType uint8 `json:"card_type"` + Description string `json:"description"` +} + +type UpdateProjectPayload struct { + // required:true + Title string `json:"title" binding:"Required"` + Description string `json:"description"` +} + +type Project struct { + ID int64 `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + BoardType uint8 `json:"board_type"` + IsClosed bool `json:"is_closed"` + // swagger:strfmt date-time + Created time.Time `json:"created_at"` + // swagger:strfmt date-time + Updated time.Time `json:"updated_at"` + // swagger:strfmt date-time + Closed time.Time `json:"closed_at"` + + Repo *RepositoryMeta `json:"repository"` + Creator *User `json:"creator"` + Owner *User `json:"owner"` +} From b6f9d0518032ffd86b32b851f804aa29ed8e3617 Mon Sep 17 00:00:00 2001 From: Dinesh Salunke Date: Sat, 18 Nov 2023 18:32:22 +0530 Subject: [PATCH 03/31] feat: api for projects --- models/project/project.go | 21 +- routers/api/v1/api.go | 372 +++++++++++++++++++---- routers/api/v1/projects/project.go | 407 ++++++++++++++++++++++++++ routers/api/v1/swagger/project.go | 22 ++ services/convert/project.go | 66 +++++ templates/swagger/v1_json.tmpl | 338 +++++++++++++++++++++ tests/integration/api_project_test.go | 228 +++++++++++++++ 7 files changed, 1396 insertions(+), 58 deletions(-) create mode 100644 routers/api/v1/projects/project.go create mode 100644 routers/api/v1/swagger/project.go create mode 100644 services/convert/project.go create mode 100644 tests/integration/api_project_test.go diff --git a/models/project/project.go b/models/project/project.go index 3a1bfe1dbd3ff..670ae39f87c66 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -95,6 +95,7 @@ type Project struct { RepoID int64 `xorm:"INDEX"` Repo *repo_model.Repository `xorm:"-"` CreatorID int64 `xorm:"NOT NULL"` + Creator *user_model.User `xorm:"-"` IsClosed bool `xorm:"INDEX"` BoardType BoardType CardType CardType @@ -115,6 +116,14 @@ func (p *Project) LoadOwner(ctx context.Context) (err error) { return err } +func (p *Project) LoadCreator(ctx context.Context) (err error) { + if p.Creator != nil { + return nil + } + p.Creator, err = user_model.GetUserByID(ctx, p.CreatorID) + return err +} + func (p *Project) LoadRepo(ctx context.Context) (err error) { if p.RepoID == 0 || p.Repo != nil { return nil @@ -348,7 +357,11 @@ func updateRepositoryProjectCount(ctx context.Context, repoID int64) error { } // ChangeProjectStatusByRepoIDAndID toggles a project between opened and closed -func ChangeProjectStatusByRepoIDAndID(ctx context.Context, repoID, projectID int64, isClosed bool) error { +func ChangeProjectStatusByRepoIDAndID( + ctx context.Context, + repoID, projectID int64, + isClosed bool, +) error { ctx, committer, err := db.TxContext(ctx) if err != nil { return err @@ -389,7 +402,11 @@ func ChangeProjectStatus(ctx context.Context, p *Project, isClosed bool) error { func changeProjectStatus(ctx context.Context, p *Project, isClosed bool) error { p.IsClosed = isClosed p.ClosedDateUnix = timeutil.TimeStampNow() - count, err := db.GetEngine(ctx).ID(p.ID).Where("repo_id = ? AND is_closed = ?", p.RepoID, !isClosed).Cols("is_closed", "closed_date_unix").Update(p) + count, err := db.GetEngine(ctx). + ID(p.ID). + Where("repo_id = ? AND is_closed = ?", p.RepoID, !isClosed). + Cols("is_closed", "closed_date_unix"). + Update(p) if err != nil { return err } diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index cadddb44c39ef..30b1c2a07fc13 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -88,6 +88,7 @@ import ( "code.gitea.io/gitea/routers/api/v1/notify" "code.gitea.io/gitea/routers/api/v1/org" "code.gitea.io/gitea/routers/api/v1/packages" + "code.gitea.io/gitea/routers/api/v1/projects" "code.gitea.io/gitea/routers/api/v1/repo" "code.gitea.io/gitea/routers/api/v1/settings" "code.gitea.io/gitea/routers/api/v1/user" @@ -231,7 +232,11 @@ func repoAssignment() func(ctx *context.APIContext) { func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.APIContext) { return func(ctx *context.APIContext) { if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() { - ctx.Error(http.StatusForbidden, "reqPackageAccess", "user should have specific permission or be a site admin") + ctx.Error( + http.StatusForbidden, + "reqPackageAccess", + "user should have specific permission or be a site admin", + ) return } } @@ -239,7 +244,9 @@ func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.APIContext) // if a token is being used for auth, we check that it contains the required scope // if a token is not being used, reqToken will enforce other sign in methods -func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeCategory) func(ctx *context.APIContext) { +func tokenRequiresScopes( + requiredScopeCategories ...auth_model.AccessTokenScopeCategory, +) func(ctx *context.APIContext) { return func(ctx *context.APIContext) { // no scope required if len(requiredScopeCategories) == 0 { @@ -257,27 +264,46 @@ func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeC // use the http method to determine the access level requiredScopeLevel := auth_model.Read - if ctx.Req.Method == "POST" || ctx.Req.Method == "PUT" || ctx.Req.Method == "PATCH" || ctx.Req.Method == "DELETE" { + if ctx.Req.Method == "POST" || ctx.Req.Method == "PUT" || ctx.Req.Method == "PATCH" || + ctx.Req.Method == "DELETE" { requiredScopeLevel = auth_model.Write } // get the required scope for the given access level and category - requiredScopes := auth_model.GetRequiredScopes(requiredScopeLevel, requiredScopeCategories...) + requiredScopes := auth_model.GetRequiredScopes( + requiredScopeLevel, + requiredScopeCategories...) // check if scope only applies to public resources publicOnly, err := scope.PublicOnly() if err != nil { - ctx.Error(http.StatusForbidden, "tokenRequiresScope", "parsing public resource scope failed: "+err.Error()) + ctx.Error( + http.StatusForbidden, + "tokenRequiresScope", + "parsing public resource scope failed: "+err.Error(), + ) return } // this context is used by the middleware in the specific route - ctx.Data["ApiTokenScopePublicRepoOnly"] = publicOnly && auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryRepository) - ctx.Data["ApiTokenScopePublicOrgOnly"] = publicOnly && auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryOrganization) + ctx.Data["ApiTokenScopePublicRepoOnly"] = publicOnly && + auth_model.ContainsCategory( + requiredScopeCategories, + auth_model.AccessTokenScopeCategoryRepository, + ) + ctx.Data["ApiTokenScopePublicOrgOnly"] = publicOnly && + auth_model.ContainsCategory( + requiredScopeCategories, + auth_model.AccessTokenScopeCategoryOrganization, + ) allow, err := scope.HasScope(requiredScopes...) if err != nil { - ctx.Error(http.StatusForbidden, "tokenRequiresScope", "checking scope failed: "+err.Error()) + ctx.Error( + http.StatusForbidden, + "tokenRequiresScope", + "checking scope failed: "+err.Error(), + ) return } @@ -285,7 +311,14 @@ func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeC return } - ctx.Error(http.StatusForbidden, "tokenRequiresScope", fmt.Sprintf("token does not have at least one of required scope(s): %v", requiredScopes)) + ctx.Error( + http.StatusForbidden, + "tokenRequiresScope", + fmt.Sprintf( + "token does not have at least one of required scope(s): %v", + requiredScopes, + ), + ) } } @@ -303,7 +336,11 @@ func reqToken() func(ctx *context.APIContext) { if pubRepoExists && publicRepo.(bool) && ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate { - ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public repos") + ctx.Error( + http.StatusForbidden, + "reqToken", + "token scope is limited to public repos", + ) return } @@ -326,14 +363,19 @@ func reqToken() func(ctx *context.APIContext) { func reqExploreSignIn() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { if setting.Service.Explore.RequireSigninView && !ctx.IsSigned { - ctx.Error(http.StatusUnauthorized, "reqExploreSignIn", "you must be signed in to search for users") + ctx.Error( + http.StatusUnauthorized, + "reqExploreSignIn", + "you must be signed in to search for users", + ) } } } func reqBasicOrRevProxyAuth() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { - if ctx.IsSigned && setting.Service.EnableReverseProxyAuthAPI && ctx.Data["AuthedMethod"].(string) == auth.ReverseProxyMethodName { + if ctx.IsSigned && setting.Service.EnableReverseProxyAuthAPI && + ctx.Data["AuthedMethod"].(string) == auth.ReverseProxyMethodName { return } if !ctx.IsBasicAuth { @@ -367,7 +409,11 @@ func reqOwner() func(ctx *context.APIContext) { func reqSelfOrAdmin() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { if !ctx.IsUserSiteAdmin() && ctx.ContextUser != ctx.Doer { - ctx.Error(http.StatusForbidden, "reqSelfOrAdmin", "doer should be the site admin or be same as the contextUser") + ctx.Error( + http.StatusForbidden, + "reqSelfOrAdmin", + "doer should be the site admin or be same as the contextUser", + ) return } } @@ -377,7 +423,11 @@ func reqSelfOrAdmin() func(ctx *context.APIContext) { func reqAdmin() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { if !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() { - ctx.Error(http.StatusForbidden, "reqAdmin", "user should be an owner or a collaborator with admin write of a repository") + ctx.Error( + http.StatusForbidden, + "reqAdmin", + "user should be an owner or a collaborator with admin write of a repository", + ) return } } @@ -387,7 +437,11 @@ func reqAdmin() func(ctx *context.APIContext) { func reqRepoWriter(unitTypes ...unit.Type) func(ctx *context.APIContext) { return func(ctx *context.APIContext) { if !ctx.IsUserRepoWriter(unitTypes) && !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() { - ctx.Error(http.StatusForbidden, "reqRepoWriter", "user should have a permission to write to a repo") + ctx.Error( + http.StatusForbidden, + "reqRepoWriter", + "user should have a permission to write to a repo", + ) return } } @@ -396,8 +450,13 @@ func reqRepoWriter(unitTypes ...unit.Type) func(ctx *context.APIContext) { // reqRepoBranchWriter user should have a permission to write to a branch, or be a site admin func reqRepoBranchWriter(ctx *context.APIContext) { options, ok := web.GetForm(ctx).(api.FileOptionInterface) - if !ok || (!ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, options.Branch()) && !ctx.IsUserSiteAdmin()) { - ctx.Error(http.StatusForbidden, "reqRepoBranchWriter", "user should have a permission to write to this branch") + if !ok || + (!ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, options.Branch()) && !ctx.IsUserSiteAdmin()) { + ctx.Error( + http.StatusForbidden, + "reqRepoBranchWriter", + "user should have a permission to write to this branch", + ) return } } @@ -406,7 +465,11 @@ func reqRepoBranchWriter(ctx *context.APIContext) { func reqRepoReader(unitType unit.Type) func(ctx *context.APIContext) { return func(ctx *context.APIContext) { if !ctx.Repo.CanRead(unitType) && !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() { - ctx.Error(http.StatusForbidden, "reqRepoReader", "user should have specific read permission or be a repo admin or a site admin") + ctx.Error( + http.StatusForbidden, + "reqRepoReader", + "user should have specific read permission or be a repo admin or a site admin", + ) return } } @@ -416,7 +479,11 @@ func reqRepoReader(unitType unit.Type) func(ctx *context.APIContext) { func reqAnyRepoReader() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { if !ctx.Repo.HasAccess() && !ctx.IsUserSiteAdmin() { - ctx.Error(http.StatusForbidden, "reqAnyRepoReader", "user should have any permission to read repository or permissions of site admin") + ctx.Error( + http.StatusForbidden, + "reqAnyRepoReader", + "user should have any permission to read repository or permissions of site admin", + ) return } } @@ -671,7 +738,11 @@ func mustEnableWiki(ctx *context.APIContext) { func mustNotBeArchived(ctx *context.APIContext) { if ctx.Repo.Repository.IsArchived { - ctx.Error(http.StatusLocked, "RepoArchived", fmt.Errorf("%s is archived", ctx.Repo.Repository.LogString())) + ctx.Error( + http.StatusLocked, + "RepoArchived", + fmt.Errorf("%s is archived", ctx.Repo.Repository.LogString()), + ) return } } @@ -689,7 +760,11 @@ func bind[T any](_ T) any { theObj := new(T) // create a new form obj for every request but not use obj directly errs := binding.Bind(ctx.Req, theObj) if len(errs) > 0 { - ctx.Error(http.StatusUnprocessableEntity, "validationError", fmt.Sprintf("%s: %s", errs[0].FieldNames, errs[0].Error())) + ctx.Error( + http.StatusUnprocessableEntity, + "validationError", + fmt.Sprintf("%s: %s", errs[0].FieldNames, errs[0].Error()), + ) return } web.SetForm(ctx, theObj) @@ -739,7 +814,11 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.APIC return } if !ctx.Doer.IsActive || ctx.Doer.ProhibitLogin { - log.Info("Failed authentication attempt for %s from %s", ctx.Doer.Name, ctx.RemoteAddr()) + log.Info( + "Failed authentication attempt for %s from %s", + ctx.Doer.Name, + ctx.RemoteAddr(), + ) ctx.Data["Title"] = ctx.Tr("auth.prohibit_login") ctx.JSON(http.StatusForbidden, map[string]string{ "message": "This account is prohibited from signing in, please contact your site administrator.", @@ -800,8 +879,10 @@ func Routes() *web.Route { // setting.CORSConfig.AllowSubdomain // FIXME: the cors middleware needs allowSubdomain option AllowedMethods: setting.CORSConfig.Methods, AllowCredentials: setting.CORSConfig.AllowCredentials, - AllowedHeaders: append([]string{"Authorization", "X-Gitea-OTP"}, setting.CORSConfig.Headers...), - MaxAge: int(setting.CORSConfig.MaxAge.Seconds()), + AllowedHeaders: append( + []string{"Authorization", "X-Gitea-OTP"}, + setting.CORSConfig.Headers...), + MaxAge: int(setting.CORSConfig.MaxAge.Seconds()), })) } m.Use(context.APIContexter()) @@ -880,7 +961,12 @@ func Routes() *web.Route { m.Get("/heatmap", user.GetUserHeatmapData) } - m.Get("/repos", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), reqExploreSignIn(), user.ListUserRepos) + m.Get( + "/repos", + tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), + reqExploreSignIn(), + user.ListUserRepos, + ) m.Group("/tokens", func() { m.Combo("").Get(user.ListAccessTokens). Post(bind(api.CreateAccessTokenOption{}), reqToken(), user.CreateAccessToken) @@ -968,7 +1054,8 @@ func Routes() *web.Route { m.Post("/gpg_key_verify", bind(api.VerifyGPGKeyOption{}), user.VerifyUserGPGKey) // (repo scope) - m.Combo("/repos", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)).Get(user.ListMyRepos). + m.Combo("/repos", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)). + Get(user.ListMyRepos). Post(bind(api.CreateRepoOption{}), repo.Create) // (repo scope) @@ -996,17 +1083,27 @@ func Routes() *web.Route { m.Post("", bind(api.UpdateUserAvatarOption{}), user.UpdateAvatar) m.Delete("", user.DeleteAvatar) }, reqToken()) + + m.Combo("/projects", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue)). + Get(projects.ListUserProjects). + Post(bind(api.NewProjectPayload{}), projects.CreateUserProject) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken()) // Repositories (requires repo scope, org scope) - m.Post("/org/{org}/repos", - tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization, auth_model.AccessTokenScopeCategoryRepository), + m.Post( + "/org/{org}/repos", + tokenRequiresScopes( + auth_model.AccessTokenScopeCategoryOrganization, + auth_model.AccessTokenScopeCategoryRepository, + ), reqToken(), bind(api.CreateRepoOption{}), - repo.CreateOrgRepoDeprecated) + repo.CreateOrgRepoDeprecated, + ) // requires repo scope - m.Combo("/repositories/{id}", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)).Get(repo.GetByID) + m.Combo("/repositories/{id}", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)). + Get(repo.GetByID) // Repos (requires repo scope) m.Group("/repos", func() { @@ -1019,7 +1116,13 @@ func Routes() *web.Route { m.Combo("").Get(reqAnyRepoReader(), repo.Get). Delete(reqToken(), reqOwner(), repo.Delete). 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.Group("/transfer", func() { m.Post("", reqOwner(), bind(api.TransferRepoOption{}), repo.Transfer) m.Post("/accept", repo.AcceptTransfer) @@ -1045,7 +1148,12 @@ func Routes() *web.Route { m.Combo("").Get(repo.GetHook). Patch(bind(api.EditHookOption{}), repo.EditHook). Delete(repo.DeleteHook) - m.Post("/tests", context.ReferencesGitRepo(), context.RepoRefForAPI, repo.TestHook) + m.Post( + "/tests", + context.ReferencesGitRepo(), + context.RepoRefForAPI, + repo.TestHook, + ) }) }, reqToken(), reqAdmin(), reqWebhooksEnabled()) m.Group("/collaborators", func() { @@ -1065,31 +1173,79 @@ func Routes() *web.Route { Put(reqAdmin(), repo.AddTeam). Delete(reqAdmin(), repo.DeleteTeam) }, reqToken()) - m.Get("/raw/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFile) - m.Get("/media/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFileOrLFS) + m.Get( + "/raw/*", + context.ReferencesGitRepo(), + context.RepoRefForAPI, + reqRepoReader(unit.TypeCode), + repo.GetRawFile, + ) + m.Get( + "/media/*", + context.ReferencesGitRepo(), + context.RepoRefForAPI, + reqRepoReader(unit.TypeCode), + repo.GetRawFileOrLFS, + ) m.Get("/archive/*", reqRepoReader(unit.TypeCode), repo.GetArchive) m.Combo("/forks").Get(repo.ListForks). Post(reqToken(), reqRepoReader(unit.TypeCode), bind(api.CreateForkOption{}), repo.CreateFork) m.Group("/branches", func() { m.Get("", repo.ListBranches) m.Get("/*", repo.GetBranch) - m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteBranch) - m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateBranchRepoOption{}), repo.CreateBranch) + m.Delete( + "/*", + reqToken(), + reqRepoWriter(unit.TypeCode), + mustNotBeArchived, + repo.DeleteBranch, + ) + m.Post( + "", + reqToken(), + reqRepoWriter(unit.TypeCode), + mustNotBeArchived, + bind(api.CreateBranchRepoOption{}), + repo.CreateBranch, + ) }, context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode)) m.Group("/branch_protections", func() { m.Get("", repo.ListBranchProtections) - m.Post("", bind(api.CreateBranchProtectionOption{}), mustNotBeArchived, repo.CreateBranchProtection) + m.Post( + "", + bind(api.CreateBranchProtectionOption{}), + mustNotBeArchived, + repo.CreateBranchProtection, + ) m.Group("/{name}", func() { m.Get("", repo.GetBranchProtection) - m.Patch("", bind(api.EditBranchProtectionOption{}), mustNotBeArchived, repo.EditBranchProtection) + m.Patch( + "", + bind(api.EditBranchProtectionOption{}), + mustNotBeArchived, + repo.EditBranchProtection, + ) m.Delete("", repo.DeleteBranchProtection) }) }, reqToken(), reqAdmin()) m.Group("/tags", func() { m.Get("", repo.ListTags) m.Get("/*", repo.GetTag) - m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateTagOption{}), repo.CreateTag) - m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteTag) + m.Post( + "", + reqToken(), + reqRepoWriter(unit.TypeCode), + mustNotBeArchived, + bind(api.CreateTagOption{}), + repo.CreateTag, + ) + m.Delete( + "/*", + reqToken(), + reqRepoWriter(unit.TypeCode), + mustNotBeArchived, + repo.DeleteTag, + ) }, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(true)) m.Group("/keys", func() { m.Combo("").Get(repo.ListDeployKeys). @@ -1107,7 +1263,14 @@ func Routes() *web.Route { Patch(mustNotBeArchived, reqToken(), reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), repo.EditWikiPage). Delete(mustNotBeArchived, reqToken(), reqRepoWriter(unit.TypeWiki), repo.DeleteWikiPage) m.Get("/revisions/{pageName}", repo.ListPageRevisions) - m.Post("/new", reqToken(), mustNotBeArchived, reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), repo.NewWikiPage) + m.Post( + "/new", + reqToken(), + mustNotBeArchived, + reqRepoWriter(unit.TypeWiki), + bind(api.CreateWikiPageOptions{}), + repo.NewWikiPage, + ) m.Get("/pages", repo.ListWikiPages) }, mustEnableWiki) m.Post("/markup", reqToken(), bind(api.MarkupOption{}), misc.Markup) @@ -1152,7 +1315,13 @@ func Routes() *web.Route { Get(repo.GetPushMirrorByName) }, reqAdmin(), reqToken()) - m.Get("/editorconfig/{filename}", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetEditorconfig) + m.Get( + "/editorconfig/{filename}", + context.ReferencesGitRepo(), + context.RepoRefForAPI, + reqRepoReader(unit.TypeCode), + repo.GetEditorconfig, + ) m.Group("/pulls", func() { m.Combo("").Get(repo.ListPullRequests). Post(reqToken(), mustNotBeArchived, bind(api.CreatePullRequestOption{}), repo.CreatePullRequest) @@ -1178,7 +1347,12 @@ func Routes() *web.Route { Post(reqToken(), bind(api.SubmitPullReviewOptions{}), repo.SubmitPullReview) m.Combo("/comments"). Get(repo.GetPullReviewComments) - m.Post("/dismissals", reqToken(), bind(api.DismissPullReviewOptions{}), repo.DismissPullReview) + m.Post( + "/dismissals", + reqToken(), + bind(api.DismissPullReviewOptions{}), + repo.DismissPullReview, + ) m.Post("/undismissals", reqToken(), repo.UnDismissPullReview) }) }) @@ -1210,15 +1384,47 @@ func Routes() *web.Route { m.Get("/tags/{sha}", repo.GetAnnotatedTag) m.Get("/notes/{sha}", repo.GetNote) }, context.ReferencesGitRepo(true), reqRepoReader(unit.TypeCode)) - m.Post("/diffpatch", reqRepoWriter(unit.TypeCode), reqToken(), bind(api.ApplyDiffPatchFileOptions{}), mustNotBeArchived, repo.ApplyDiffPatch) + m.Post( + "/diffpatch", + reqRepoWriter(unit.TypeCode), + reqToken(), + bind(api.ApplyDiffPatchFileOptions{}), + mustNotBeArchived, + repo.ApplyDiffPatch, + ) m.Group("/contents", func() { m.Get("", repo.GetContentsList) - m.Post("", reqToken(), bind(api.ChangeFilesOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.ChangeFiles) + m.Post( + "", + reqToken(), + bind(api.ChangeFilesOptions{}), + reqRepoBranchWriter, + mustNotBeArchived, + repo.ChangeFiles, + ) m.Get("/*", repo.GetContents) m.Group("/*", func() { - m.Post("", bind(api.CreateFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.CreateFile) - m.Put("", bind(api.UpdateFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.UpdateFile) - m.Delete("", bind(api.DeleteFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.DeleteFile) + m.Post( + "", + bind(api.CreateFileOptions{}), + reqRepoBranchWriter, + mustNotBeArchived, + repo.CreateFile, + ) + m.Put( + "", + bind(api.UpdateFileOptions{}), + reqRepoBranchWriter, + mustNotBeArchived, + repo.UpdateFile, + ) + m.Delete( + "", + bind(api.DeleteFileOptions{}), + reqRepoBranchWriter, + mustNotBeArchived, + repo.DeleteFile, + ) }, reqToken()) }, reqRepoReader(unit.TypeCode)) m.Get("/signing-key.gpg", misc.SigningKey) @@ -1232,7 +1438,11 @@ func Routes() *web.Route { }, reqAnyRepoReader()) m.Get("/issue_templates", context.ReferencesGitRepo(), repo.GetIssueTemplates) m.Get("/issue_config", context.ReferencesGitRepo(), repo.GetIssueConfig) - m.Get("/issue_config/validate", context.ReferencesGitRepo(), repo.ValidateIssueConfig) + m.Get( + "/issue_config/validate", + context.ReferencesGitRepo(), + repo.ValidateIssueConfig, + ) m.Get("/languages", reqRepoReader(unit.TypeCode), repo.GetLanguages) m.Get("/activities/feeds", repo.ListRepoActivityFeeds) m.Get("/new_pin_allowed", repo.AreNewIssuePinsAllowed) @@ -1290,7 +1500,8 @@ func Routes() *web.Route { m.Group("/comments", func() { m.Combo("").Get(repo.ListIssueComments). Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueCommentOption{}), repo.CreateIssueComment) - m.Combo("/{id}", reqToken()).Patch(bind(api.EditIssueCommentOption{}), repo.EditIssueCommentDeprecated). + m.Combo("/{id}", reqToken()). + Patch(bind(api.EditIssueCommentOption{}), repo.EditIssueCommentDeprecated). Delete(repo.DeleteIssueCommentDeprecated) }) m.Get("/timeline", repo.ListIssueCommentsAndTimeline) @@ -1308,7 +1519,8 @@ func Routes() *web.Route { Delete(repo.ResetIssueTime) m.Delete("/{id}", repo.DeleteTime) }, reqToken()) - m.Combo("/deadline").Post(reqToken(), bind(api.EditDeadlineOption{}), repo.UpdateIssueDeadline) + m.Combo("/deadline"). + Post(reqToken(), bind(api.EditDeadlineOption{}), repo.UpdateIssueDeadline) m.Group("/stopwatch", func() { m.Post("/start", repo.StartIssueStopwatch) m.Post("/stop", repo.StopIssueStopwatch) @@ -1363,6 +1575,12 @@ func Routes() *web.Route { Patch(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), bind(api.EditMilestoneOption{}), repo.EditMilestone). Delete(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteMilestone) }) + m.Group("/projects", func() { + m. + Combo(""). + Get(projects.ListRepoProjects). + Post(bind(api.NewProjectPayload{}), projects.CreateRepoProject) + }, mustEnableIssues) }, repoAssignment()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue)) @@ -1370,20 +1588,43 @@ func Routes() *web.Route { m.Group("/packages/{username}", func() { m.Group("/{type}/{name}/{version}", func() { m.Get("", reqToken(), packages.GetPackage) - m.Delete("", reqToken(), reqPackageAccess(perm.AccessModeWrite), packages.DeletePackage) + m.Delete( + "", + reqToken(), + reqPackageAccess(perm.AccessModeWrite), + packages.DeletePackage, + ) m.Get("/files", reqToken(), packages.ListPackageFiles) }) m.Get("/", reqToken(), packages.ListPackages) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context_service.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead)) // Organizations - m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), org.ListMyOrgs) + m.Get( + "/user/orgs", + reqToken(), + tokenRequiresScopes( + auth_model.AccessTokenScopeCategoryUser, + auth_model.AccessTokenScopeCategoryOrganization, + ), + org.ListMyOrgs, + ) m.Group("/users/{username}/orgs", func() { m.Get("", reqToken(), org.ListUserOrgs) m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context_service.UserAssignmentAPI()) - m.Post("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), reqToken(), bind(api.CreateOrgOption{}), org.Create) - m.Get("/orgs", org.GetAll, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization)) + m.Post( + "/orgs", + tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), + reqToken(), + bind(api.CreateOrgOption{}), + org.Create, + ) + m.Get( + "/orgs", + org.GetAll, + tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), + ) m.Group("/orgs/{org}", func() { m.Combo("").Get(org.Get). Patch(reqToken(), reqOrgOwnership(), bind(api.EditOrgOption{}), org.Edit). @@ -1414,7 +1655,13 @@ func Routes() *web.Route { }, reqToken(), reqOrgMembership()) m.Group("/labels", func() { m.Get("", org.ListLabels) - m.Post("", reqToken(), reqOrgOwnership(), bind(api.CreateLabelOption{}), org.CreateLabel) + m.Post( + "", + reqToken(), + reqOrgOwnership(), + bind(api.CreateLabelOption{}), + org.CreateLabel, + ) m.Combo("/{id}").Get(reqToken(), org.GetLabel). Patch(reqToken(), reqOrgOwnership(), bind(api.EditLabelOption{}), org.EditLabel). Delete(reqToken(), reqOrgOwnership(), org.DeleteLabel) @@ -1431,6 +1678,12 @@ func Routes() *web.Route { m.Delete("", org.DeleteAvatar) }, reqToken(), reqOrgOwnership()) m.Get("/activities/feeds", org.ListOrgActivityFeeds) + + m.Group("/projects", func() { + m.Combo(""). + Get(projects.ListOrgProjects). + Post(bind(api.NewProjectPayload{}), projects.CreateOrgProject) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue), reqToken(), reqOrgMembership()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true)) m.Group("/teams/{teamid}", func() { m.Combo("").Get(reqToken(), org.GetTeam). @@ -1493,6 +1746,13 @@ func Routes() *web.Route { }) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryAdmin), reqToken(), reqSiteAdmin()) + m.Group("/projects", func() { + m. + Combo("/{id}"). + Get(projects.GetProject). + Patch(bind(api.UpdateProjectPayload{}), projects.UpdateProject). + Delete(projects.DeleteProject) + }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue), reqToken()) m.Group("/topics", func() { m.Get("/search", repo.TopicSearch) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)) diff --git a/routers/api/v1/projects/project.go b/routers/api/v1/projects/project.go new file mode 100644 index 0000000000000..d5c5cbfe0ec09 --- /dev/null +++ b/routers/api/v1/projects/project.go @@ -0,0 +1,407 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package projects + +import ( + "net/http" + + project_model "code.gitea.io/gitea/models/project" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/setting" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/convert" +) + +func innerCreateProject( + ctx *context.APIContext, + project_type project_model.Type, +) { + form := web.GetForm(ctx).(*api.NewProjectPayload) + project := &project_model.Project{ + RepoID: 0, + OwnerID: ctx.Doer.ID, + Title: form.Title, + Description: form.Description, + CreatorID: ctx.Doer.ID, + BoardType: project_model.BoardType(form.BoardType), + Type: project_type, + } + + if ctx.ContextUser != nil { + project.OwnerID = ctx.ContextUser.ID + } + + if project_type == project_model.TypeRepository { + project.RepoID = ctx.Repo.Repository.ID + } + + if err := project_model.NewProject(ctx, project); err != nil { + ctx.Error(http.StatusInternalServerError, "NewProject", err) + return + } + + project, err := project_model.GetProjectByID(ctx, project.ID) + if err != nil { + ctx.Error(http.StatusInternalServerError, "NewProject", err) + return + } + + ctx.JSON(http.StatusCreated, convert.ToAPIProject(ctx, project)) +} + +func CreateUserProject(ctx *context.APIContext) { + // swagger:operation POST /user/projects project projectCreateUserProject + // --- + // summary: Create a user project + // produces: + // - application/json + // consumes: + // - application/json + // parameters: + // - name: project + // in: body + // required: true + // schema: { "$ref": "#/definitions/NewProjectPayload" } + // responses: + // "201": + // "$ref": "#/responses/Project" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + innerCreateProject(ctx, project_model.TypeIndividual) +} + +func CreateOrgProject(ctx *context.APIContext) { + // swagger:operation POST /orgs/{org}/projects project projectCreateOrgProject + // --- + // summary: Create a organization project + // produces: + // - application/json + // consumes: + // - application/json + // parameters: + // - name: org + // in: path + // description: owner of repo + // type: string + // required: true + // - name: project + // in: body + // required: true + // schema: { "$ref": "#/definitions/NewProjectPayload" } + // responses: + // "201": + // "$ref": "#/responses/Project" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + innerCreateProject(ctx, project_model.TypeOrganization) +} + +func CreateRepoProject(ctx *context.APIContext) { + // swagger:operation POST /repos/{owner}/{repo}/projects project projectCreateRepositoryProject + // --- + // summary: Create a repository project + // produces: + // - application/json + // consumes: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of repo + // type: string + // required: true + // - name: repo + // in: path + // description: repo + // type: string + // required: true + // - name: project + // in: body + // required: true + // schema: { "$ref": "#/definitions/NewProjectPayload" } + // responses: + // "201": + // "$ref": "#/responses/Project" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + innerCreateProject(ctx, project_model.TypeRepository) +} + +func GetProject(ctx *context.APIContext) { + // swagger:operation GET /projects/{id} project projectGetProject + // --- + // summary: Get project + // produces: + // - application/json + // parameters: + // - name: id + // in: path + // description: id of the project + // type: string + // required: true + // responses: + // "200": + // "$ref": "#/responses/Project" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) + if err != nil { + if project_model.IsErrProjectNotExist(err) { + ctx.NotFound() + } else { + ctx.Error(http.StatusInternalServerError, "GetProjectByID", err) + } + return + } + + ctx.JSON(http.StatusOK, convert.ToAPIProject(ctx, project)) +} + +func UpdateProject(ctx *context.APIContext) { + // swagger:operation PATCH /projects/{id} project projectUpdateProject + // --- + // summary: Update project + // produces: + // - application/json + // consumes: + // - application/json + // parameters: + // - name: id + // in: path + // description: id of the project + // type: string + // required: true + // - name: project + // in: body + // required: true + // schema: { "$ref": "#/definitions/UpdateProjectPayload" } + // responses: + // "200": + // "$ref": "#/responses/Project" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + form := web.GetForm(ctx).(*api.UpdateProjectPayload) + project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64("id")) + if err != nil { + if project_model.IsErrProjectNotExist(err) { + ctx.NotFound() + } else { + ctx.Error(http.StatusInternalServerError, "UpdateProject", err) + } + return + } + if project.Title != form.Title { + project.Title = form.Title + } + if project.Description != form.Description { + project.Description = form.Description + } + + err = project_model.UpdateProject(ctx, project) + if err != nil { + ctx.Error(http.StatusInternalServerError, "UpdateProject", err) + return + } + ctx.JSON(http.StatusOK, convert.ToAPIProject(ctx, project)) +} + +func DeleteProject(ctx *context.APIContext) { + // swagger:operation DELETE /projects/{id} project projectDeleteProject + // --- + // summary: Delete project + // parameters: + // - name: id + // in: path + // description: id of the project + // type: string + // required: true + // responses: + // "204": + // "description": "Deleted the project" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + + if err := project_model.DeleteProjectByID(ctx, ctx.ParamsInt64(":id")); err != nil { + ctx.Error(http.StatusInternalServerError, "DeleteProjectByID", err) + return + } + + ctx.Status(http.StatusNoContent) + +} + +func ListUserProjects(ctx *context.APIContext) { + // swagger:operation GET /user/projects project projectListUserProjects + // --- + // summary: List repository projects + // produces: + // - application/json + // parameters: + // - name: closed + // in: query + // description: include closed issues or not + // type: boolean + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/ProjectList" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + projects, count, err := project_model.FindProjects(ctx, project_model.SearchOptions{ + OwnerID: ctx.Doer.ID, + Page: ctx.FormInt("page"), + IsClosed: ctx.FormOptionalBool("closed"), + Type: project_model.TypeIndividual, + }) + if err != nil { + ctx.Error(http.StatusInternalServerError, "Projects", err) + return + } + + ctx.SetLinkHeader(int(count), setting.UI.IssuePagingNum) + ctx.SetTotalCountHeader(count) + + apiProjects, err := convert.ToAPIProjectList(ctx, projects) + if err != nil { + ctx.Error(http.StatusInternalServerError, "Projects", err) + return + } + + ctx.JSON(http.StatusOK, apiProjects) +} + +func ListOrgProjects(ctx *context.APIContext) { + // swagger:operation GET /orgs/{org}/projects project projectListOrgProjects + // --- + // summary: List repository projects + // produces: + // - application/json + // parameters: + // - name: org + // in: path + // description: owner of the repository + // type: string + // required: true + // - name: closed + // in: query + // description: include closed issues or not + // type: boolean + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/ProjectList" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + projects, count, err := project_model.FindProjects(ctx, project_model.SearchOptions{ + OwnerID: ctx.Org.Organization.AsUser().ID, + Page: ctx.FormInt("page"), + IsClosed: ctx.FormOptionalBool("closed"), + Type: project_model.TypeOrganization, + }) + if err != nil { + ctx.Error(http.StatusInternalServerError, "Projects", err) + return + } + + ctx.SetLinkHeader(int(count), setting.UI.IssuePagingNum) + ctx.SetTotalCountHeader(count) + + apiProjects, err := convert.ToAPIProjectList(ctx, projects) + if err != nil { + ctx.Error(http.StatusInternalServerError, "Projects", err) + return + } + + ctx.JSON(http.StatusOK, apiProjects) +} + +func ListRepoProjects(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/projects project projectListRepositoryProjects + // --- + // summary: List repository projects + // produces: + // - application/json + // parameters: + // - name: owner + // in: path + // description: owner of the repository + // type: string + // required: true + // - name: repo + // in: path + // description: repo + // type: string + // required: true + // - name: closed + // in: query + // description: include closed issues or not + // type: boolean + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer + // responses: + // "200": + // "$ref": "#/responses/ProjectList" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + projects, count, err := project_model.FindProjects(ctx, project_model.SearchOptions{ + RepoID: ctx.Repo.Repository.ID, + Page: ctx.FormInt("page"), + IsClosed: ctx.FormOptionalBool("closed"), + Type: project_model.TypeRepository, + }) + if err != nil { + ctx.Error(http.StatusInternalServerError, "Projects", err) + return + } + + ctx.SetLinkHeader(int(count), setting.UI.IssuePagingNum) + ctx.SetTotalCountHeader(count) + + apiProjects, err := convert.ToAPIProjectList(ctx, projects) + if err != nil { + ctx.Error(http.StatusInternalServerError, "Projects", err) + return + } + + ctx.JSON(http.StatusOK, apiProjects) +} diff --git a/routers/api/v1/swagger/project.go b/routers/api/v1/swagger/project.go new file mode 100644 index 0000000000000..cfa00c7faeab3 --- /dev/null +++ b/routers/api/v1/swagger/project.go @@ -0,0 +1,22 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package swagger + +import ( + api "code.gitea.io/gitea/modules/structs" +) + +// Project +// swagger:response Project +type swaggerResponseProject struct { + // in:body + Body api.Project `json:"body"` +} + +// ProjectList +// swagger:response ProjectList +type swaggerResponseProjectList struct { + // in:body + Body []api.Project `json:"body"` +} diff --git a/services/convert/project.go b/services/convert/project.go new file mode 100644 index 0000000000000..281c1c1a236f7 --- /dev/null +++ b/services/convert/project.go @@ -0,0 +1,66 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package convert + +import ( + "context" + + project_model "code.gitea.io/gitea/models/project" + api "code.gitea.io/gitea/modules/structs" +) + +func ToAPIProject(ctx context.Context, project *project_model.Project) *api.Project { + + apiProject := &api.Project{ + Title: project.Title, + Description: project.Description, + BoardType: uint8(project.BoardType), + IsClosed: project.IsClosed, + Created: project.CreatedUnix.AsTime(), + Updated: project.UpdatedUnix.AsTime(), + Closed: project.ClosedDateUnix.AsTime(), + } + + // try to laod the repo + project.LoadRepo(ctx) + if project.Repo != nil { + apiProject.Repo = &api.RepositoryMeta{ + ID: project.RepoID, + Name: project.Repo.Name, + Owner: project.Repo.OwnerName, + FullName: project.Repo.FullName(), + } + } + + project.LoadCreator(ctx) + if project.Creator != nil { + apiProject.Creator = &api.User{ + ID: project.Creator.ID, + UserName: project.Creator.Name, + FullName: project.Creator.FullName, + } + } + + project.LoadOwner(ctx) + if project.Owner != nil { + apiProject.Owner = &api.User{ + ID: project.Owner.ID, + UserName: project.Owner.Name, + FullName: project.Owner.FullName, + } + } + + return apiProject +} + +func ToAPIProjectList( + ctx context.Context, + projects []*project_model.Project, +) ([]*api.Project, error) { + result := make([]*api.Project, len(projects)) + for i := range projects { + result[i] = ToAPIProject(ctx, projects[i]) + } + return result, nil +} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 75a45dc68ac56..43b31534a286b 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -2311,6 +2311,75 @@ } } }, + "/orgs/{org}/projects": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "project" + ], + "summary": "List repository projects", + "operationId": "projectListOrgProjects", + "parameters": [ + { + "type": "string", + "description": "owner of the repository", + "name": "org", + "in": "path", + "required": true + }, + { + "type": "boolean", + "description": "include closed issues or not", + "name": "closed", + "in": "query" + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ] + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "project" + ], + "summary": "Create a organization project", + "operationId": "projectCreateOrgProject", + "parameters": [ + { + "type": "string", + "description": "owner of repo", + "name": "org", + "in": "path", + "required": true + }, + { + "name": "project", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/NewProjectPayload" + } + } + ] + } + }, "/orgs/{org}/public_members": { "get": { "produces": [ @@ -2912,6 +2981,73 @@ } } }, + "/projects/{id}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "project" + ], + "summary": "Get project", + "operationId": "projectGetProject", + "parameters": [ + { + "type": "string", + "description": "id of the project", + "name": "id", + "in": "path", + "required": true + } + ] + }, + "delete": { + "tags": [ + "project" + ], + "summary": "Delete project", + "operationId": "projectDeleteProject", + "parameters": [ + { + "type": "string", + "description": "id of the project", + "name": "id", + "in": "path", + "required": true + } + ] + }, + "patch": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "project" + ], + "summary": "Update project", + "operationId": "projectUpdateProject", + "parameters": [ + { + "type": "string", + "description": "id of the project", + "name": "id", + "in": "path", + "required": true + }, + { + "name": "project", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/UpdateProjectPayload" + } + } + ] + } + }, "/repos/issues/search": { "get": { "produces": [ @@ -10140,6 +10276,89 @@ } } }, + "/repos/{owner}/{repo}/projects": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "project" + ], + "summary": "List repository projects", + "operationId": "projectListRepositoryProjects", + "parameters": [ + { + "type": "string", + "description": "owner of the repository", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "boolean", + "description": "include closed issues or not", + "name": "closed", + "in": "query" + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ] + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "project" + ], + "summary": "Create a repository project", + "operationId": "projectCreateRepositoryProject", + "parameters": [ + { + "type": "string", + "description": "owner of repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "name": "project", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/NewProjectPayload" + } + } + ] + } + }, "/repos/{owner}/{repo}/pulls": { "get": { "produces": [ @@ -15481,6 +15700,61 @@ } } }, + "/user/projects": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "project" + ], + "summary": "List repository projects", + "operationId": "projectListUserProjects", + "parameters": [ + { + "type": "boolean", + "description": "include closed issues or not", + "name": "closed", + "in": "query" + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" + } + ] + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "project" + ], + "summary": "Create a user project", + "operationId": "projectCreateUserProject", + "parameters": [ + { + "name": "project", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/NewProjectPayload" + } + } + ] + } + }, "/user/repos": { "get": { "produces": [ @@ -21310,6 +21584,55 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "Project": { + "type": "object", + "properties": { + "board_type": { + "type": "integer", + "format": "uint8", + "x-go-name": "BoardType" + }, + "closed_at": { + "type": "string", + "format": "date-time", + "x-go-name": "Closed" + }, + "created_at": { + "type": "string", + "format": "date-time", + "x-go-name": "Created" + }, + "creator": { + "$ref": "#/definitions/User" + }, + "description": { + "type": "string", + "x-go-name": "Description" + }, + "id": { + "type": "integer", + "format": "int64", + "x-go-name": "ID" + }, + "is_closed": { + "type": "boolean", + "x-go-name": "IsClosed" + }, + "repository": { + "$ref": "#/definitions/RepositoryMeta" + }, + "title": { + "type": "string", + "x-go-name": "Title" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "x-go-name": "Updated" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "PublicKey": { "description": "PublicKey publickey is a user key to push code to repository", "type": "object", @@ -23628,6 +23951,21 @@ } } }, + "Project": { + "description": "Project", + "schema": { + "$ref": "#/definitions/Project" + } + }, + "ProjectList": { + "description": "ProjectList", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Project" + } + } + }, "PublicKey": { "description": "PublicKey", "schema": { diff --git a/tests/integration/api_project_test.go b/tests/integration/api_project_test.go new file mode 100644 index 0000000000000..75cb480c282b3 --- /dev/null +++ b/tests/integration/api_project_test.go @@ -0,0 +1,228 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "fmt" + "net/http" + "net/url" + "testing" + + auth_model "code.gitea.io/gitea/models/auth" + project_model "code.gitea.io/gitea/models/project" + "code.gitea.io/gitea/models/unittest" + api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/tests" + "github.com/stretchr/testify/assert" +) + +func TestAPICreateUserProject(t *testing.T) { + defer tests.PrepareTestEnv(t)() + const title, description, board_type = "project_name", "project_description", uint8(project_model.BoardTypeBasicKanban) + + token := getUserToken( + t, + "user2", + auth_model.AccessTokenScopeWriteIssue, + auth_model.AccessTokenScopeWriteUser, + ) + urlStr := fmt.Sprintf("/api/v1/user/projects?token=%s", token) + + req := NewRequestWithJSON(t, "POST", urlStr, &api.NewProjectPayload{ + Title: title, + Description: description, + BoardType: board_type, + }) + resp := MakeRequest(t, req, http.StatusCreated) + var apiProject api.Project + DecodeJSON(t, resp, &apiProject) + assert.Equal(t, title, apiProject.Title) + assert.Equal(t, description, apiProject.Description) + assert.Equal(t, board_type, apiProject.BoardType) + assert.Equal(t, "user2", apiProject.Creator.UserName) +} + +func TestAPICreateOrgProject(t *testing.T) { + defer tests.PrepareTestEnv(t)() + const title, description, board_type = "project_name", "project_description", uint8(project_model.BoardTypeBasicKanban) + + orgName := "org17" + token := getUserToken( + t, + "user2", + auth_model.AccessTokenScopeWriteIssue, + auth_model.AccessTokenScopeWriteOrganization, + ) + urlStr := fmt.Sprintf("/api/v1/orgs/%s/projects?token=%s", orgName, token) + + req := NewRequestWithJSON(t, "POST", urlStr, &api.NewProjectPayload{ + Title: title, + Description: description, + BoardType: board_type, + }) + resp := MakeRequest(t, req, http.StatusCreated) + var apiProject api.Project + DecodeJSON(t, resp, &apiProject) + assert.Equal(t, title, apiProject.Title) + assert.Equal(t, description, apiProject.Description) + assert.Equal(t, board_type, apiProject.BoardType) + assert.Equal(t, "org17", apiProject.Creator.UserName) +} + +func TestAPICreateRepoProject(t *testing.T) { + defer tests.PrepareTestEnv(t)() + const title, description, board_type = "project_name", "project_description", uint8(project_model.BoardTypeBasicKanban) + + ownerName := "user2" + repoName := "repo1" + token := getUserToken( + t, + ownerName, + auth_model.AccessTokenScopeWriteIssue, + auth_model.AccessTokenScopeWriteOrganization, + ) + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/projects?token=%s", ownerName, repoName, token) + + req := NewRequestWithJSON(t, "POST", urlStr, &api.NewProjectPayload{ + Title: title, + Description: description, + BoardType: board_type, + }) + resp := MakeRequest(t, req, http.StatusCreated) + var apiProject api.Project + DecodeJSON(t, resp, &apiProject) + assert.Equal(t, title, apiProject.Title) + assert.Equal(t, description, apiProject.Description) + assert.Equal(t, board_type, apiProject.BoardType) + assert.Equal(t, "repo1", apiProject.Repo.Name) +} + +func TestAPIListUserProjects(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + token := getUserToken( + t, + "user2", + auth_model.AccessTokenScopeReadUser, + auth_model.AccessTokenScopeReadIssue, + ) + link, _ := url.Parse(fmt.Sprintf("/api/v1/user/projects")) + + link.RawQuery = url.Values{"token": {token}}.Encode() + + req := NewRequest(t, "GET", link.String()) + var apiProjects []*api.Project + + resp := MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiProjects) + assert.Len(t, apiProjects, 1) +} + +func TestAPIListOrgProjects(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + orgName := "org17" + token := getUserToken( + t, + "user2", + auth_model.AccessTokenScopeReadOrganization, + auth_model.AccessTokenScopeReadIssue, + ) + link, _ := url.Parse(fmt.Sprintf("/api/v1/orgs/%s/projects", orgName)) + + link.RawQuery = url.Values{"token": {token}}.Encode() + + req := NewRequest(t, "GET", link.String()) + var apiProjects []*api.Project + + resp := MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiProjects) + assert.Len(t, apiProjects, 1) +} + +func TestAPIListRepoProjects(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + ownerName := "user2" + repoName := "repo1" + token := getUserToken( + t, + "user2", + auth_model.AccessTokenScopeReadRepository, + auth_model.AccessTokenScopeReadIssue, + ) + link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/projects", ownerName, repoName)) + + link.RawQuery = url.Values{"token": {token}}.Encode() + + req := NewRequest(t, "GET", link.String()) + var apiProjects []*api.Project + + resp := MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiProjects) + assert.Len(t, apiProjects, 1) +} + +func TestAPIGetProject(t *testing.T) { + defer tests.PrepareTestEnv(t)() + token := getUserToken( + t, + "user2", + auth_model.AccessTokenScopeReadUser, + auth_model.AccessTokenScopeReadIssue, + ) + link, _ := url.Parse(fmt.Sprintf("/api/v1/projects/%d", 1)) + + link.RawQuery = url.Values{"token": {token}}.Encode() + + req := NewRequest(t, "GET", link.String()) + var apiProject *api.Project + + resp := MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiProject) + assert.Equal(t, "First project", apiProject.Title) + assert.Equal(t, "repo1", apiProject.Repo.Name) + assert.Equal(t, "user2", apiProject.Creator.UserName) +} + +func TestAPIUpdateProject(t *testing.T) { + defer tests.PrepareTestEnv(t)() + token := getUserToken( + t, + "user2", + auth_model.AccessTokenScopeWriteUser, + auth_model.AccessTokenScopeWriteIssue, + ) + link, _ := url.Parse(fmt.Sprintf("/api/v1/projects/%d", 1)) + + link.RawQuery = url.Values{"token": {token}}.Encode() + + req := NewRequestWithJSON(t, "PATCH", link.String(), &api.UpdateProjectPayload{ + Title: "First project updated", + }) + + var apiProject *api.Project + + resp := MakeRequest(t, req, http.StatusOK) + DecodeJSON(t, resp, &apiProject) + assert.Equal(t, "First project updated", apiProject.Title) +} + +func TestAPIDeleteProject(t *testing.T) { + defer tests.PrepareTestEnv(t)() + token := getUserToken( + t, + "user2", + auth_model.AccessTokenScopeWriteUser, + auth_model.AccessTokenScopeWriteIssue, + ) + link, _ := url.Parse(fmt.Sprintf("/api/v1/projects/%d", 1)) + + link.RawQuery = url.Values{"token": {token}}.Encode() + + req := NewRequest(t, "DELETE", link.String()) + + MakeRequest(t, req, http.StatusNoContent) + unittest.AssertNotExistsBean(t, &project_model.Project{ID: 1}) +} From a0f8dbd928b972ba585cf7d18ba2918a918d0e94 Mon Sep 17 00:00:00 2001 From: Dinesh Salunke Date: Tue, 16 Jan 2024 20:38:15 +0530 Subject: [PATCH 04/31] chore: remove unnecessary go lines formatting --- models/project/project.go | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/models/project/project.go b/models/project/project.go index 670ae39f87c66..c63b83061276c 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -357,11 +357,7 @@ func updateRepositoryProjectCount(ctx context.Context, repoID int64) error { } // ChangeProjectStatusByRepoIDAndID toggles a project between opened and closed -func ChangeProjectStatusByRepoIDAndID( - ctx context.Context, - repoID, projectID int64, - isClosed bool, -) error { +func ChangeProjectStatusByRepoIDAndID(ctx context.Context, repoID, projectID int64, isClosed bool) error { ctx, committer, err := db.TxContext(ctx) if err != nil { return err @@ -402,11 +398,7 @@ func ChangeProjectStatus(ctx context.Context, p *Project, isClosed bool) error { func changeProjectStatus(ctx context.Context, p *Project, isClosed bool) error { p.IsClosed = isClosed p.ClosedDateUnix = timeutil.TimeStampNow() - count, err := db.GetEngine(ctx). - ID(p.ID). - Where("repo_id = ? AND is_closed = ?", p.RepoID, !isClosed). - Cols("is_closed", "closed_date_unix"). - Update(p) + count, err := db.GetEngine(ctx).ID(p.ID).Where("repo_id = ? AND is_closed = ?", p.RepoID, !isClosed).Cols("is_closed", "closed_date_unix").Update(p) if err != nil { return err } From df22b6dd128edd2ced9d63b77bd1bbebb7f1a14a Mon Sep 17 00:00:00 2001 From: dineshsalunke Date: Tue, 16 Jan 2024 20:44:55 +0530 Subject: [PATCH 05/31] chore: remove the goline formatting --- services/convert/project.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/services/convert/project.go b/services/convert/project.go index 281c1c1a236f7..0e25d6c10dd87 100644 --- a/services/convert/project.go +++ b/services/convert/project.go @@ -54,10 +54,7 @@ func ToAPIProject(ctx context.Context, project *project_model.Project) *api.Proj return apiProject } -func ToAPIProjectList( - ctx context.Context, - projects []*project_model.Project, -) ([]*api.Project, error) { +func ToAPIProjectList(ctx context.Context, projects []*project_model.Project) ([]*api.Project, error) { result := make([]*api.Project, len(projects)) for i := range projects { result[i] = ToAPIProject(ctx, projects[i]) From 5bbbade0268fffb1dde50d90b13d41ecd34e22a0 Mon Sep 17 00:00:00 2001 From: dineshsalunke Date: Tue, 16 Jan 2024 21:04:27 +0530 Subject: [PATCH 06/31] chore: remove unwanted indentation on the line --- tests/integration/api_project_test.go | 63 ++++----------------------- 1 file changed, 9 insertions(+), 54 deletions(-) diff --git a/tests/integration/api_project_test.go b/tests/integration/api_project_test.go index 75cb480c282b3..017a5bf3f1a35 100644 --- a/tests/integration/api_project_test.go +++ b/tests/integration/api_project_test.go @@ -21,12 +21,7 @@ func TestAPICreateUserProject(t *testing.T) { defer tests.PrepareTestEnv(t)() const title, description, board_type = "project_name", "project_description", uint8(project_model.BoardTypeBasicKanban) - token := getUserToken( - t, - "user2", - auth_model.AccessTokenScopeWriteIssue, - auth_model.AccessTokenScopeWriteUser, - ) + token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteIssue, auth_model.AccessTokenScopeWriteUser) urlStr := fmt.Sprintf("/api/v1/user/projects?token=%s", token) req := NewRequestWithJSON(t, "POST", urlStr, &api.NewProjectPayload{ @@ -48,12 +43,7 @@ func TestAPICreateOrgProject(t *testing.T) { const title, description, board_type = "project_name", "project_description", uint8(project_model.BoardTypeBasicKanban) orgName := "org17" - token := getUserToken( - t, - "user2", - auth_model.AccessTokenScopeWriteIssue, - auth_model.AccessTokenScopeWriteOrganization, - ) + token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteIssue, auth_model.AccessTokenScopeWriteOrganization) urlStr := fmt.Sprintf("/api/v1/orgs/%s/projects?token=%s", orgName, token) req := NewRequestWithJSON(t, "POST", urlStr, &api.NewProjectPayload{ @@ -76,12 +66,7 @@ func TestAPICreateRepoProject(t *testing.T) { ownerName := "user2" repoName := "repo1" - token := getUserToken( - t, - ownerName, - auth_model.AccessTokenScopeWriteIssue, - auth_model.AccessTokenScopeWriteOrganization, - ) + token := getUserToken(t, ownerName, auth_model.AccessTokenScopeWriteIssue, auth_model.AccessTokenScopeWriteOrganization) urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/projects?token=%s", ownerName, repoName, token) req := NewRequestWithJSON(t, "POST", urlStr, &api.NewProjectPayload{ @@ -101,12 +86,7 @@ func TestAPICreateRepoProject(t *testing.T) { func TestAPIListUserProjects(t *testing.T) { defer tests.PrepareTestEnv(t)() - token := getUserToken( - t, - "user2", - auth_model.AccessTokenScopeReadUser, - auth_model.AccessTokenScopeReadIssue, - ) + token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadIssue) link, _ := url.Parse(fmt.Sprintf("/api/v1/user/projects")) link.RawQuery = url.Values{"token": {token}}.Encode() @@ -123,12 +103,7 @@ func TestAPIListOrgProjects(t *testing.T) { defer tests.PrepareTestEnv(t)() orgName := "org17" - token := getUserToken( - t, - "user2", - auth_model.AccessTokenScopeReadOrganization, - auth_model.AccessTokenScopeReadIssue, - ) + token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopeReadIssue) link, _ := url.Parse(fmt.Sprintf("/api/v1/orgs/%s/projects", orgName)) link.RawQuery = url.Values{"token": {token}}.Encode() @@ -146,12 +121,7 @@ func TestAPIListRepoProjects(t *testing.T) { ownerName := "user2" repoName := "repo1" - token := getUserToken( - t, - "user2", - auth_model.AccessTokenScopeReadRepository, - auth_model.AccessTokenScopeReadIssue, - ) + token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopeReadIssue) link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/projects", ownerName, repoName)) link.RawQuery = url.Values{"token": {token}}.Encode() @@ -166,12 +136,7 @@ func TestAPIListRepoProjects(t *testing.T) { func TestAPIGetProject(t *testing.T) { defer tests.PrepareTestEnv(t)() - token := getUserToken( - t, - "user2", - auth_model.AccessTokenScopeReadUser, - auth_model.AccessTokenScopeReadIssue, - ) + token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadIssue) link, _ := url.Parse(fmt.Sprintf("/api/v1/projects/%d", 1)) link.RawQuery = url.Values{"token": {token}}.Encode() @@ -188,12 +153,7 @@ func TestAPIGetProject(t *testing.T) { func TestAPIUpdateProject(t *testing.T) { defer tests.PrepareTestEnv(t)() - token := getUserToken( - t, - "user2", - auth_model.AccessTokenScopeWriteUser, - auth_model.AccessTokenScopeWriteIssue, - ) + token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteUser, auth_model.AccessTokenScopeWriteIssue) link, _ := url.Parse(fmt.Sprintf("/api/v1/projects/%d", 1)) link.RawQuery = url.Values{"token": {token}}.Encode() @@ -211,12 +171,7 @@ func TestAPIUpdateProject(t *testing.T) { func TestAPIDeleteProject(t *testing.T) { defer tests.PrepareTestEnv(t)() - token := getUserToken( - t, - "user2", - auth_model.AccessTokenScopeWriteUser, - auth_model.AccessTokenScopeWriteIssue, - ) + token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteUser, auth_model.AccessTokenScopeWriteIssue) link, _ := url.Parse(fmt.Sprintf("/api/v1/projects/%d", 1)) link.RawQuery = url.Values{"token": {token}}.Encode() From aa32ad74af97507e6c268dc95bf95a65e75046e0 Mon Sep 17 00:00:00 2001 From: dineshsalunke Date: Wed, 17 Jan 2024 19:55:42 +0530 Subject: [PATCH 07/31] refactor: update for db find method usage --- routers/api/v1/projects/project.go | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/routers/api/v1/projects/project.go b/routers/api/v1/projects/project.go index d5c5cbfe0ec09..e258d6e760733 100644 --- a/routers/api/v1/projects/project.go +++ b/routers/api/v1/projects/project.go @@ -6,6 +6,7 @@ package projects import ( "net/http" + "code.gitea.io/gitea/models/db" project_model "code.gitea.io/gitea/models/project" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/setting" @@ -270,11 +271,13 @@ func ListUserProjects(ctx *context.APIContext) { // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" - projects, count, err := project_model.FindProjects(ctx, project_model.SearchOptions{ - OwnerID: ctx.Doer.ID, - Page: ctx.FormInt("page"), - IsClosed: ctx.FormOptionalBool("closed"), + projects, count, err := db.FindAndCount[project_model.Project](ctx, project_model.SearchOptions{ Type: project_model.TypeIndividual, + IsClosed: ctx.FormOptionalBool("closed"), + OwnerID: ctx.Doer.ID, + ListOptions: db.ListOptions{ + Page: ctx.FormInt("page"), + }, }) if err != nil { ctx.Error(http.StatusInternalServerError, "Projects", err) @@ -324,9 +327,11 @@ func ListOrgProjects(ctx *context.APIContext) { // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" - projects, count, err := project_model.FindProjects(ctx, project_model.SearchOptions{ - OwnerID: ctx.Org.Organization.AsUser().ID, - Page: ctx.FormInt("page"), + projects, count, err := db.FindAndCount[project_model.Project](ctx, project_model.SearchOptions{ + OwnerID: ctx.Org.Organization.AsUser().ID, + ListOptions: db.ListOptions{ + Page: ctx.FormInt("page"), + }, IsClosed: ctx.FormOptionalBool("closed"), Type: project_model.TypeOrganization, }) @@ -383,11 +388,13 @@ func ListRepoProjects(ctx *context.APIContext) { // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" - projects, count, err := project_model.FindProjects(ctx, project_model.SearchOptions{ + projects, count, err := db.FindAndCount[project_model.Project](ctx, project_model.SearchOptions{ RepoID: ctx.Repo.Repository.ID, - Page: ctx.FormInt("page"), IsClosed: ctx.FormOptionalBool("closed"), Type: project_model.TypeRepository, + ListOptions: db.ListOptions{ + Page: ctx.FormInt("page"), + }, }) if err != nil { ctx.Error(http.StatusInternalServerError, "Projects", err) From 81c3d0ce34ec988fa2a5197a29ffc7cc32b566ec Mon Sep 17 00:00:00 2001 From: dineshsalunke Date: Wed, 17 Jan 2024 20:16:37 +0530 Subject: [PATCH 08/31] fix: add missing swagger tags --- modules/structs/project.go | 2 ++ routers/api/v1/swagger/options.go | 6 ++++ templates/swagger/v1_json.tmpl | 51 ++++++++++++++++++++++++++++++- 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/modules/structs/project.go b/modules/structs/project.go index e70659e5ceeed..4f1683e96c116 100644 --- a/modules/structs/project.go +++ b/modules/structs/project.go @@ -5,6 +5,7 @@ package structs import "time" +// swagger:model type NewProjectPayload struct { // required:true Title string `json:"title" binding:"Required"` @@ -15,6 +16,7 @@ type NewProjectPayload struct { Description string `json:"description"` } +// swagger:model type UpdateProjectPayload struct { // required:true Title string `json:"title" binding:"Required"` diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index 6f7859df62ed4..4937d5e99a352 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -190,4 +190,10 @@ type swaggerParameterBodies struct { // in:body CreateOrUpdateSecretOption api.CreateOrUpdateSecretOption + + // in:body + NewProjectPayload api.NewProjectPayload + + // in:body + UpdateProjectPayload api.UpdateProjectPayload } diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 2c5e0108a95be..044a1703a31cb 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -21119,6 +21119,35 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "NewProjectPayload": { + "type": "object", + "required": [ + "title", + "board_type", + "card_type" + ], + "properties": { + "board_type": { + "type": "integer", + "format": "uint8", + "x-go-name": "BoardType" + }, + "card_type": { + "type": "integer", + "format": "uint8", + "x-go-name": "CardType" + }, + "description": { + "type": "string", + "x-go-name": "Description" + }, + "title": { + "type": "string", + "x-go-name": "Title" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "NodeInfo": { "description": "NodeInfo contains standardized way of exposing metadata about a server running one of the distributed social networks", "type": "object", @@ -21719,6 +21748,9 @@ "type": "boolean", "x-go-name": "IsClosed" }, + "owner": { + "$ref": "#/definitions/User" + }, "repository": { "$ref": "#/definitions/RepositoryMeta" }, @@ -23111,6 +23143,23 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "UpdateProjectPayload": { + "type": "object", + "required": [ + "title" + ], + "properties": { + "description": { + "type": "string", + "x-go-name": "Description" + }, + "title": { + "type": "string", + "x-go-name": "Title" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "UpdateRepoAvatarOption": { "description": "UpdateRepoAvatarUserOption options when updating the repo avatar", "type": "object", @@ -24462,7 +24511,7 @@ "parameterBodies": { "description": "parameterBodies", "schema": { - "$ref": "#/definitions/CreateOrUpdateSecretOption" + "$ref": "#/definitions/UpdateProjectPayload" } }, "redirect": { From 852c0df3825d906a5de5bff040f6da561916f91b Mon Sep 17 00:00:00 2001 From: dineshsalunke Date: Wed, 17 Jan 2024 21:44:47 +0530 Subject: [PATCH 09/31] chore: fix lint issues --- tests/integration/api_project_test.go | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/tests/integration/api_project_test.go b/tests/integration/api_project_test.go index 017a5bf3f1a35..c001bc9f08d9a 100644 --- a/tests/integration/api_project_test.go +++ b/tests/integration/api_project_test.go @@ -14,12 +14,13 @@ import ( "code.gitea.io/gitea/models/unittest" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/tests" + "github.com/stretchr/testify/assert" ) func TestAPICreateUserProject(t *testing.T) { defer tests.PrepareTestEnv(t)() - const title, description, board_type = "project_name", "project_description", uint8(project_model.BoardTypeBasicKanban) + const title, description, boardType = "project_name", "project_description", uint8(project_model.BoardTypeBasicKanban) token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteIssue, auth_model.AccessTokenScopeWriteUser) urlStr := fmt.Sprintf("/api/v1/user/projects?token=%s", token) @@ -27,20 +28,20 @@ func TestAPICreateUserProject(t *testing.T) { req := NewRequestWithJSON(t, "POST", urlStr, &api.NewProjectPayload{ Title: title, Description: description, - BoardType: board_type, + BoardType: boardType, }) resp := MakeRequest(t, req, http.StatusCreated) var apiProject api.Project DecodeJSON(t, resp, &apiProject) assert.Equal(t, title, apiProject.Title) assert.Equal(t, description, apiProject.Description) - assert.Equal(t, board_type, apiProject.BoardType) + assert.Equal(t, boardType, apiProject.BoardType) assert.Equal(t, "user2", apiProject.Creator.UserName) } func TestAPICreateOrgProject(t *testing.T) { defer tests.PrepareTestEnv(t)() - const title, description, board_type = "project_name", "project_description", uint8(project_model.BoardTypeBasicKanban) + const title, description, boardType = "project_name", "project_description", uint8(project_model.BoardTypeBasicKanban) orgName := "org17" token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteIssue, auth_model.AccessTokenScopeWriteOrganization) @@ -49,20 +50,20 @@ func TestAPICreateOrgProject(t *testing.T) { req := NewRequestWithJSON(t, "POST", urlStr, &api.NewProjectPayload{ Title: title, Description: description, - BoardType: board_type, + BoardType: boardType, }) resp := MakeRequest(t, req, http.StatusCreated) var apiProject api.Project DecodeJSON(t, resp, &apiProject) assert.Equal(t, title, apiProject.Title) assert.Equal(t, description, apiProject.Description) - assert.Equal(t, board_type, apiProject.BoardType) + assert.Equal(t, boardType, apiProject.BoardType) assert.Equal(t, "org17", apiProject.Creator.UserName) } func TestAPICreateRepoProject(t *testing.T) { defer tests.PrepareTestEnv(t)() - const title, description, board_type = "project_name", "project_description", uint8(project_model.BoardTypeBasicKanban) + const title, description, boardType = "project_name", "project_description", uint8(project_model.BoardTypeBasicKanban) ownerName := "user2" repoName := "repo1" @@ -72,14 +73,14 @@ func TestAPICreateRepoProject(t *testing.T) { req := NewRequestWithJSON(t, "POST", urlStr, &api.NewProjectPayload{ Title: title, Description: description, - BoardType: board_type, + BoardType: boardType, }) resp := MakeRequest(t, req, http.StatusCreated) var apiProject api.Project DecodeJSON(t, resp, &apiProject) assert.Equal(t, title, apiProject.Title) assert.Equal(t, description, apiProject.Description) - assert.Equal(t, board_type, apiProject.BoardType) + assert.Equal(t, boardType, apiProject.BoardType) assert.Equal(t, "repo1", apiProject.Repo.Name) } @@ -87,7 +88,7 @@ func TestAPIListUserProjects(t *testing.T) { defer tests.PrepareTestEnv(t)() token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadIssue) - link, _ := url.Parse(fmt.Sprintf("/api/v1/user/projects")) + link, _ := url.Parse("/api/v1/user/projects") link.RawQuery = url.Values{"token": {token}}.Encode() From dbfecced25d63c9191e19d57bd60691563d955aa Mon Sep 17 00:00:00 2001 From: dineshsalunke Date: Wed, 17 Jan 2024 21:45:36 +0530 Subject: [PATCH 10/31] fix: return error when converting project to api project --- routers/api/v1/projects/project.go | 29 ++++++++++++++++++++++------- services/convert/project.go | 29 ++++++++++++++++++++++------- 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/routers/api/v1/projects/project.go b/routers/api/v1/projects/project.go index e258d6e760733..8b24e4d488632 100644 --- a/routers/api/v1/projects/project.go +++ b/routers/api/v1/projects/project.go @@ -17,7 +17,7 @@ import ( func innerCreateProject( ctx *context.APIContext, - project_type project_model.Type, + projectType project_model.Type, ) { form := web.GetForm(ctx).(*api.NewProjectPayload) project := &project_model.Project{ @@ -27,14 +27,14 @@ func innerCreateProject( Description: form.Description, CreatorID: ctx.Doer.ID, BoardType: project_model.BoardType(form.BoardType), - Type: project_type, + Type: projectType, } if ctx.ContextUser != nil { project.OwnerID = ctx.ContextUser.ID } - if project_type == project_model.TypeRepository { + if projectType == project_model.TypeRepository { project.RepoID = ctx.Repo.Repository.ID } @@ -49,7 +49,13 @@ func innerCreateProject( return } - ctx.JSON(http.StatusCreated, convert.ToAPIProject(ctx, project)) + projectResponse, err := convert.ToAPIProject(ctx, project) + if err != nil { + ctx.Error(http.StatusInternalServerError, "NewProject", err) + return + } + + ctx.JSON(http.StatusCreated, projectResponse) } func CreateUserProject(ctx *context.APIContext) { @@ -165,7 +171,12 @@ func GetProject(ctx *context.APIContext) { return } - ctx.JSON(http.StatusOK, convert.ToAPIProject(ctx, project)) + projectResponse, err := convert.ToAPIProject(ctx, project) + if err != nil { + ctx.Error(http.StatusInternalServerError, "GetProjectByID", err) + return + } + ctx.JSON(http.StatusOK, projectResponse) } func UpdateProject(ctx *context.APIContext) { @@ -215,7 +226,12 @@ func UpdateProject(ctx *context.APIContext) { ctx.Error(http.StatusInternalServerError, "UpdateProject", err) return } - ctx.JSON(http.StatusOK, convert.ToAPIProject(ctx, project)) + projectResponse, err := convert.ToAPIProject(ctx, project) + if err != nil { + ctx.Error(http.StatusInternalServerError, "UpdateProject", err) + return + } + ctx.JSON(http.StatusOK, projectResponse) } func DeleteProject(ctx *context.APIContext) { @@ -242,7 +258,6 @@ func DeleteProject(ctx *context.APIContext) { } ctx.Status(http.StatusNoContent) - } func ListUserProjects(ctx *context.APIContext) { diff --git a/services/convert/project.go b/services/convert/project.go index 0e25d6c10dd87..4292037f9e9ea 100644 --- a/services/convert/project.go +++ b/services/convert/project.go @@ -10,8 +10,7 @@ import ( api "code.gitea.io/gitea/modules/structs" ) -func ToAPIProject(ctx context.Context, project *project_model.Project) *api.Project { - +func ToAPIProject(ctx context.Context, project *project_model.Project) (*api.Project, error) { apiProject := &api.Project{ Title: project.Title, Description: project.Description, @@ -23,7 +22,10 @@ func ToAPIProject(ctx context.Context, project *project_model.Project) *api.Proj } // try to laod the repo - project.LoadRepo(ctx) + err := project.LoadRepo(ctx) + if err != nil { + return nil, err + } if project.Repo != nil { apiProject.Repo = &api.RepositoryMeta{ ID: project.RepoID, @@ -33,7 +35,10 @@ func ToAPIProject(ctx context.Context, project *project_model.Project) *api.Proj } } - project.LoadCreator(ctx) + err = project.LoadCreator(ctx) + if err != nil { + return nil, err + } if project.Creator != nil { apiProject.Creator = &api.User{ ID: project.Creator.ID, @@ -42,7 +47,10 @@ func ToAPIProject(ctx context.Context, project *project_model.Project) *api.Proj } } - project.LoadOwner(ctx) + err = project.LoadOwner(ctx) + if err != nil { + return nil, err + } if project.Owner != nil { apiProject.Owner = &api.User{ ID: project.Owner.ID, @@ -51,13 +59,20 @@ func ToAPIProject(ctx context.Context, project *project_model.Project) *api.Proj } } - return apiProject + return apiProject, nil } func ToAPIProjectList(ctx context.Context, projects []*project_model.Project) ([]*api.Project, error) { result := make([]*api.Project, len(projects)) + var err error for i := range projects { - result[i] = ToAPIProject(ctx, projects[i]) + result[i], err = ToAPIProject(ctx, projects[i]) + if err != nil { + break + } + } + if err != nil { + return nil, err } return result, nil } From fec855e323330ee6cf1078525a6acf0cfe41aa0b Mon Sep 17 00:00:00 2001 From: dineshsalunke Date: Wed, 17 Jan 2024 22:14:39 +0530 Subject: [PATCH 11/31] chore: fix swagger documentation --- modules/structs/project.go | 1 + routers/api/v1/projects/project.go | 108 +++++++++++++-------------- templates/swagger/v1_json.tmpl | 114 ++++++++++++++++++++++++++--- 3 files changed, 160 insertions(+), 63 deletions(-) diff --git a/modules/structs/project.go b/modules/structs/project.go index 4f1683e96c116..46e7bebfcd9b8 100644 --- a/modules/structs/project.go +++ b/modules/structs/project.go @@ -23,6 +23,7 @@ type UpdateProjectPayload struct { Description string `json:"description"` } +// swagger:model type Project struct { ID int64 `json:"id"` Title string `json:"title"` diff --git a/routers/api/v1/projects/project.go b/routers/api/v1/projects/project.go index 8b24e4d488632..4aed86d7de4dd 100644 --- a/routers/api/v1/projects/project.go +++ b/routers/api/v1/projects/project.go @@ -72,12 +72,12 @@ func CreateUserProject(ctx *context.APIContext) { // required: true // schema: { "$ref": "#/definitions/NewProjectPayload" } // responses: - // "201": - // "$ref": "#/responses/Project" - // "403": - // "$ref": "#/responses/forbidden" - // "404": - // "$ref": "#/responses/notFound" + // "201": + // "$ref": "#/responses/Project" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" innerCreateProject(ctx, project_model.TypeIndividual) } @@ -100,12 +100,12 @@ func CreateOrgProject(ctx *context.APIContext) { // required: true // schema: { "$ref": "#/definitions/NewProjectPayload" } // responses: - // "201": - // "$ref": "#/responses/Project" - // "403": - // "$ref": "#/responses/forbidden" - // "404": - // "$ref": "#/responses/notFound" + // "201": + // "$ref": "#/responses/Project" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" innerCreateProject(ctx, project_model.TypeOrganization) } @@ -133,12 +133,12 @@ func CreateRepoProject(ctx *context.APIContext) { // required: true // schema: { "$ref": "#/definitions/NewProjectPayload" } // responses: - // "201": - // "$ref": "#/responses/Project" - // "403": - // "$ref": "#/responses/forbidden" - // "404": - // "$ref": "#/responses/notFound" + // "201": + // "$ref": "#/responses/Project" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" innerCreateProject(ctx, project_model.TypeRepository) } @@ -155,12 +155,12 @@ func GetProject(ctx *context.APIContext) { // type: string // required: true // responses: - // "200": - // "$ref": "#/responses/Project" - // "403": - // "$ref": "#/responses/forbidden" - // "404": - // "$ref": "#/responses/notFound" + // "200": + // "$ref": "#/responses/Project" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) if err != nil { if project_model.IsErrProjectNotExist(err) { @@ -198,12 +198,12 @@ func UpdateProject(ctx *context.APIContext) { // required: true // schema: { "$ref": "#/definitions/UpdateProjectPayload" } // responses: - // "200": - // "$ref": "#/responses/Project" - // "403": - // "$ref": "#/responses/forbidden" - // "404": - // "$ref": "#/responses/notFound" + // "200": + // "$ref": "#/responses/Project" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" form := web.GetForm(ctx).(*api.UpdateProjectPayload) project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64("id")) if err != nil { @@ -245,12 +245,12 @@ func DeleteProject(ctx *context.APIContext) { // type: string // required: true // responses: - // "204": - // "description": "Deleted the project" - // "403": - // "$ref": "#/responses/forbidden" - // "404": - // "$ref": "#/responses/notFound" + // "204": + // "description": "Deleted the project" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" if err := project_model.DeleteProjectByID(ctx, ctx.ParamsInt64(":id")); err != nil { ctx.Error(http.StatusInternalServerError, "DeleteProjectByID", err) @@ -280,12 +280,12 @@ func ListUserProjects(ctx *context.APIContext) { // description: page size of results // type: integer // responses: - // "200": - // "$ref": "#/responses/ProjectList" - // "403": - // "$ref": "#/responses/forbidden" - // "404": - // "$ref": "#/responses/notFound" + // "200": + // "$ref": "#/responses/ProjectList" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" projects, count, err := db.FindAndCount[project_model.Project](ctx, project_model.SearchOptions{ Type: project_model.TypeIndividual, IsClosed: ctx.FormOptionalBool("closed"), @@ -336,12 +336,12 @@ func ListOrgProjects(ctx *context.APIContext) { // description: page size of results // type: integer // responses: - // "200": - // "$ref": "#/responses/ProjectList" - // "403": - // "$ref": "#/responses/forbidden" - // "404": - // "$ref": "#/responses/notFound" + // "200": + // "$ref": "#/responses/ProjectList" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" projects, count, err := db.FindAndCount[project_model.Project](ctx, project_model.SearchOptions{ OwnerID: ctx.Org.Organization.AsUser().ID, ListOptions: db.ListOptions{ @@ -397,12 +397,12 @@ func ListRepoProjects(ctx *context.APIContext) { // description: page size of results // type: integer // responses: - // "200": - // "$ref": "#/responses/ProjectList" - // "403": - // "$ref": "#/responses/forbidden" - // "404": - // "$ref": "#/responses/notFound" + // "200": + // "$ref": "#/responses/ProjectList" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" projects, count, err := db.FindAndCount[project_model.Project](ctx, project_model.SearchOptions{ RepoID: ctx.Repo.Repository.ID, IsClosed: ctx.FormOptionalBool("closed"), diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 044a1703a31cb..8f49333daf112 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -2393,7 +2393,18 @@ "name": "limit", "in": "query" } - ] + ], + "responses": { + "200": { + "$ref": "#/responses/ProjectList" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } }, "post": { "consumes": [ @@ -2423,7 +2434,15 @@ "$ref": "#/definitions/NewProjectPayload" } } - ] + ], + "responses": { + "201": { + "$ref": "#/responses/Project" + }, + "403": { + "$ref": "#/responses/forbidden" + } + } } }, "/orgs/{org}/public_members": { @@ -3045,7 +3064,18 @@ "in": "path", "required": true } - ] + ], + "responses": { + "200": { + "$ref": "#/responses/Project" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } }, "delete": { "tags": [ @@ -3061,7 +3091,18 @@ "in": "path", "required": true } - ] + ], + "responses": { + "204": { + "description": "Deleted the project" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } }, "patch": { "consumes": [ @@ -3091,7 +3132,18 @@ "$ref": "#/definitions/UpdateProjectPayload" } } - ] + ], + "responses": { + "200": { + "$ref": "#/responses/Project" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } } }, "/repos/issues/search": { @@ -10365,7 +10417,18 @@ "name": "limit", "in": "query" } - ] + ], + "responses": { + "200": { + "$ref": "#/responses/ProjectList" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } }, "post": { "consumes": [ @@ -10402,7 +10465,18 @@ "$ref": "#/definitions/NewProjectPayload" } } - ] + ], + "responses": { + "201": { + "$ref": "#/responses/Project" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } } }, "/repos/{owner}/{repo}/pulls": { @@ -15819,7 +15893,18 @@ "name": "limit", "in": "query" } - ] + ], + "responses": { + "200": { + "$ref": "#/responses/ProjectList" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } }, "post": { "consumes": [ @@ -15842,7 +15927,18 @@ "$ref": "#/definitions/NewProjectPayload" } } - ] + ], + "responses": { + "201": { + "$ref": "#/responses/Project" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + } + } } }, "/user/repos": { From 6238b9e7465a4ce1ee06a5fb4c3cfd7228f24d38 Mon Sep 17 00:00:00 2001 From: dineshsalunke Date: Wed, 17 Jan 2024 22:55:07 +0530 Subject: [PATCH 12/31] fix: ignore errors when loading data for converting project to response --- services/convert/project.go | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/services/convert/project.go b/services/convert/project.go index 4292037f9e9ea..42ce611e15bbe 100644 --- a/services/convert/project.go +++ b/services/convert/project.go @@ -22,10 +22,7 @@ func ToAPIProject(ctx context.Context, project *project_model.Project) (*api.Pro } // try to laod the repo - err := project.LoadRepo(ctx) - if err != nil { - return nil, err - } + _ = project.LoadRepo(ctx) if project.Repo != nil { apiProject.Repo = &api.RepositoryMeta{ ID: project.RepoID, @@ -35,10 +32,7 @@ func ToAPIProject(ctx context.Context, project *project_model.Project) (*api.Pro } } - err = project.LoadCreator(ctx) - if err != nil { - return nil, err - } + _ = project.LoadCreator(ctx) if project.Creator != nil { apiProject.Creator = &api.User{ ID: project.Creator.ID, @@ -47,10 +41,7 @@ func ToAPIProject(ctx context.Context, project *project_model.Project) (*api.Pro } } - err = project.LoadOwner(ctx) - if err != nil { - return nil, err - } + _ = project.LoadOwner(ctx) if project.Owner != nil { apiProject.Owner = &api.User{ ID: project.Owner.ID, From 8819a0a4fc8c1e6ebd489b313fefa3f94e3741d2 Mon Sep 17 00:00:00 2001 From: dineshsalunke Date: Wed, 17 Jan 2024 22:55:31 +0530 Subject: [PATCH 13/31] refactor: use the add token auth method for token in tests --- tests/integration/api_project_test.go | 33 +++++++++------------------ 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/tests/integration/api_project_test.go b/tests/integration/api_project_test.go index c001bc9f08d9a..8904170f51fe6 100644 --- a/tests/integration/api_project_test.go +++ b/tests/integration/api_project_test.go @@ -23,13 +23,12 @@ func TestAPICreateUserProject(t *testing.T) { const title, description, boardType = "project_name", "project_description", uint8(project_model.BoardTypeBasicKanban) token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteIssue, auth_model.AccessTokenScopeWriteUser) - urlStr := fmt.Sprintf("/api/v1/user/projects?token=%s", token) - req := NewRequestWithJSON(t, "POST", urlStr, &api.NewProjectPayload{ + req := NewRequestWithJSON(t, "POST", "/api/v1/user/projects", &api.NewProjectPayload{ Title: title, Description: description, BoardType: boardType, - }) + }).AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusCreated) var apiProject api.Project DecodeJSON(t, resp, &apiProject) @@ -45,13 +44,13 @@ func TestAPICreateOrgProject(t *testing.T) { orgName := "org17" token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteIssue, auth_model.AccessTokenScopeWriteOrganization) - urlStr := fmt.Sprintf("/api/v1/orgs/%s/projects?token=%s", orgName, token) + urlStr := fmt.Sprintf("/api/v1/orgs/%s/projects", orgName) req := NewRequestWithJSON(t, "POST", urlStr, &api.NewProjectPayload{ Title: title, Description: description, BoardType: boardType, - }) + }).AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusCreated) var apiProject api.Project DecodeJSON(t, resp, &apiProject) @@ -68,13 +67,13 @@ func TestAPICreateRepoProject(t *testing.T) { ownerName := "user2" repoName := "repo1" token := getUserToken(t, ownerName, auth_model.AccessTokenScopeWriteIssue, auth_model.AccessTokenScopeWriteOrganization) - urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/projects?token=%s", ownerName, repoName, token) + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/projects", ownerName, repoName) req := NewRequestWithJSON(t, "POST", urlStr, &api.NewProjectPayload{ Title: title, Description: description, BoardType: boardType, - }) + }).AddTokenAuth(token) resp := MakeRequest(t, req, http.StatusCreated) var apiProject api.Project DecodeJSON(t, resp, &apiProject) @@ -107,9 +106,7 @@ func TestAPIListOrgProjects(t *testing.T) { token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadOrganization, auth_model.AccessTokenScopeReadIssue) link, _ := url.Parse(fmt.Sprintf("/api/v1/orgs/%s/projects", orgName)) - link.RawQuery = url.Values{"token": {token}}.Encode() - - req := NewRequest(t, "GET", link.String()) + req := NewRequest(t, "GET", link.String()).AddTokenAuth(token) var apiProjects []*api.Project resp := MakeRequest(t, req, http.StatusOK) @@ -125,9 +122,7 @@ func TestAPIListRepoProjects(t *testing.T) { token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository, auth_model.AccessTokenScopeReadIssue) link, _ := url.Parse(fmt.Sprintf("/api/v1/repos/%s/%s/projects", ownerName, repoName)) - link.RawQuery = url.Values{"token": {token}}.Encode() - - req := NewRequest(t, "GET", link.String()) + req := NewRequest(t, "GET", link.String()).AddTokenAuth(token) var apiProjects []*api.Project resp := MakeRequest(t, req, http.StatusOK) @@ -140,9 +135,7 @@ func TestAPIGetProject(t *testing.T) { token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadIssue) link, _ := url.Parse(fmt.Sprintf("/api/v1/projects/%d", 1)) - link.RawQuery = url.Values{"token": {token}}.Encode() - - req := NewRequest(t, "GET", link.String()) + req := NewRequest(t, "GET", link.String()).AddTokenAuth(token) var apiProject *api.Project resp := MakeRequest(t, req, http.StatusOK) @@ -157,11 +150,9 @@ func TestAPIUpdateProject(t *testing.T) { token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteUser, auth_model.AccessTokenScopeWriteIssue) link, _ := url.Parse(fmt.Sprintf("/api/v1/projects/%d", 1)) - link.RawQuery = url.Values{"token": {token}}.Encode() - req := NewRequestWithJSON(t, "PATCH", link.String(), &api.UpdateProjectPayload{ Title: "First project updated", - }) + }).AddTokenAuth(token) var apiProject *api.Project @@ -175,9 +166,7 @@ func TestAPIDeleteProject(t *testing.T) { token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteUser, auth_model.AccessTokenScopeWriteIssue) link, _ := url.Parse(fmt.Sprintf("/api/v1/projects/%d", 1)) - link.RawQuery = url.Values{"token": {token}}.Encode() - - req := NewRequest(t, "DELETE", link.String()) + req := NewRequest(t, "DELETE", link.String()).AddTokenAuth(token) MakeRequest(t, req, http.StatusNoContent) unittest.AssertNotExistsBean(t, &project_model.Project{ID: 1}) From 3b2943a0023c52374c8807d317a23d0aae00fe53 Mon Sep 17 00:00:00 2001 From: dineshsalunke Date: Wed, 17 Jan 2024 23:20:02 +0530 Subject: [PATCH 14/31] test: use the add token auth method for token --- tests/integration/api_project_test.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/integration/api_project_test.go b/tests/integration/api_project_test.go index 8904170f51fe6..2754e1f3a4baf 100644 --- a/tests/integration/api_project_test.go +++ b/tests/integration/api_project_test.go @@ -89,9 +89,7 @@ func TestAPIListUserProjects(t *testing.T) { token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadUser, auth_model.AccessTokenScopeReadIssue) link, _ := url.Parse("/api/v1/user/projects") - link.RawQuery = url.Values{"token": {token}}.Encode() - - req := NewRequest(t, "GET", link.String()) + req := NewRequest(t, "GET", link.String()).AddTokenAuth(token) var apiProjects []*api.Project resp := MakeRequest(t, req, http.StatusOK) From d2fd13836c2d6a1d72e70d858f37325d1b0c5fe1 Mon Sep 17 00:00:00 2001 From: dineshsalunke Date: Sat, 20 Jan 2024 10:18:05 +0530 Subject: [PATCH 15/31] test: use the correct fields for the test case --- tests/integration/api_project_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/integration/api_project_test.go b/tests/integration/api_project_test.go index 2754e1f3a4baf..03ada63336729 100644 --- a/tests/integration/api_project_test.go +++ b/tests/integration/api_project_test.go @@ -57,7 +57,8 @@ func TestAPICreateOrgProject(t *testing.T) { assert.Equal(t, title, apiProject.Title) assert.Equal(t, description, apiProject.Description) assert.Equal(t, boardType, apiProject.BoardType) - assert.Equal(t, "org17", apiProject.Creator.UserName) + assert.Equal(t, "user2", apiProject.Creator.UserName) + assert.Equal(t, "org17", apiProject.Owner.UserName) } func TestAPICreateRepoProject(t *testing.T) { From 897c67b5555dcc3f6b371f35df94183dfde95c67 Mon Sep 17 00:00:00 2001 From: Denys Konovalov Date: Sun, 21 Jan 2024 15:29:23 +0100 Subject: [PATCH 16/31] revert formatting changes --- routers/api/v1/api.go | 360 +++++++----------------------------------- 1 file changed, 58 insertions(+), 302 deletions(-) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index fb9ea820c913b..df2222acb8b48 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -234,11 +234,7 @@ func repoAssignment() func(ctx *context.APIContext) { func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.APIContext) { return func(ctx *context.APIContext) { if ctx.Package.AccessMode < accessMode && !ctx.IsUserSiteAdmin() { - ctx.Error( - http.StatusForbidden, - "reqPackageAccess", - "user should have specific permission or be a site admin", - ) + ctx.Error(http.StatusForbidden, "reqPackageAccess", "user should have specific permission or be a site admin") return } } @@ -246,9 +242,7 @@ func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.APIContext) // if a token is being used for auth, we check that it contains the required scope // if a token is not being used, reqToken will enforce other sign in methods -func tokenRequiresScopes( - requiredScopeCategories ...auth_model.AccessTokenScopeCategory, -) func(ctx *context.APIContext) { +func tokenRequiresScopes(requiredScopeCategories ...auth_model.AccessTokenScopeCategory) func(ctx *context.APIContext) { return func(ctx *context.APIContext) { // no scope required if len(requiredScopeCategories) == 0 { @@ -266,46 +260,27 @@ func tokenRequiresScopes( // use the http method to determine the access level requiredScopeLevel := auth_model.Read - if ctx.Req.Method == "POST" || ctx.Req.Method == "PUT" || ctx.Req.Method == "PATCH" || - ctx.Req.Method == "DELETE" { + if ctx.Req.Method == "POST" || ctx.Req.Method == "PUT" || ctx.Req.Method == "PATCH" || ctx.Req.Method == "DELETE" { requiredScopeLevel = auth_model.Write } // get the required scope for the given access level and category - requiredScopes := auth_model.GetRequiredScopes( - requiredScopeLevel, - requiredScopeCategories...) + requiredScopes := auth_model.GetRequiredScopes(requiredScopeLevel, requiredScopeCategories...) // check if scope only applies to public resources publicOnly, err := scope.PublicOnly() if err != nil { - ctx.Error( - http.StatusForbidden, - "tokenRequiresScope", - "parsing public resource scope failed: "+err.Error(), - ) + ctx.Error(http.StatusForbidden, "tokenRequiresScope", "parsing public resource scope failed: "+err.Error()) return } // this context is used by the middleware in the specific route - ctx.Data["ApiTokenScopePublicRepoOnly"] = publicOnly && - auth_model.ContainsCategory( - requiredScopeCategories, - auth_model.AccessTokenScopeCategoryRepository, - ) - ctx.Data["ApiTokenScopePublicOrgOnly"] = publicOnly && - auth_model.ContainsCategory( - requiredScopeCategories, - auth_model.AccessTokenScopeCategoryOrganization, - ) + ctx.Data["ApiTokenScopePublicRepoOnly"] = publicOnly && auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryRepository) + ctx.Data["ApiTokenScopePublicOrgOnly"] = publicOnly && auth_model.ContainsCategory(requiredScopeCategories, auth_model.AccessTokenScopeCategoryOrganization) allow, err := scope.HasScope(requiredScopes...) if err != nil { - ctx.Error( - http.StatusForbidden, - "tokenRequiresScope", - "checking scope failed: "+err.Error(), - ) + ctx.Error(http.StatusForbidden, "tokenRequiresScope", "checking scope failed: "+err.Error()) return } @@ -313,14 +288,7 @@ func tokenRequiresScopes( return } - ctx.Error( - http.StatusForbidden, - "tokenRequiresScope", - fmt.Sprintf( - "token does not have at least one of required scope(s): %v", - requiredScopes, - ), - ) + ctx.Error(http.StatusForbidden, "tokenRequiresScope", fmt.Sprintf("token does not have at least one of required scope(s): %v", requiredScopes)) } } @@ -338,11 +306,7 @@ func reqToken() func(ctx *context.APIContext) { if pubRepoExists && publicRepo.(bool) && ctx.Repo.Repository != nil && ctx.Repo.Repository.IsPrivate { - ctx.Error( - http.StatusForbidden, - "reqToken", - "token scope is limited to public repos", - ) + ctx.Error(http.StatusForbidden, "reqToken", "token scope is limited to public repos") return } @@ -365,19 +329,14 @@ func reqToken() func(ctx *context.APIContext) { func reqExploreSignIn() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { if setting.Service.Explore.RequireSigninView && !ctx.IsSigned { - ctx.Error( - http.StatusUnauthorized, - "reqExploreSignIn", - "you must be signed in to search for users", - ) + ctx.Error(http.StatusUnauthorized, "reqExploreSignIn", "you must be signed in to search for users") } } } func reqBasicOrRevProxyAuth() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { - if ctx.IsSigned && setting.Service.EnableReverseProxyAuthAPI && - ctx.Data["AuthedMethod"].(string) == auth.ReverseProxyMethodName { + if ctx.IsSigned && setting.Service.EnableReverseProxyAuthAPI && ctx.Data["AuthedMethod"].(string) == auth.ReverseProxyMethodName { return } if !ctx.IsBasicAuth { @@ -411,11 +370,7 @@ func reqOwner() func(ctx *context.APIContext) { func reqSelfOrAdmin() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { if !ctx.IsUserSiteAdmin() && ctx.ContextUser != ctx.Doer { - ctx.Error( - http.StatusForbidden, - "reqSelfOrAdmin", - "doer should be the site admin or be same as the contextUser", - ) + ctx.Error(http.StatusForbidden, "reqSelfOrAdmin", "doer should be the site admin or be same as the contextUser") return } } @@ -425,11 +380,7 @@ func reqSelfOrAdmin() func(ctx *context.APIContext) { func reqAdmin() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { if !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() { - ctx.Error( - http.StatusForbidden, - "reqAdmin", - "user should be an owner or a collaborator with admin write of a repository", - ) + ctx.Error(http.StatusForbidden, "reqAdmin", "user should be an owner or a collaborator with admin write of a repository") return } } @@ -439,11 +390,7 @@ func reqAdmin() func(ctx *context.APIContext) { func reqRepoWriter(unitTypes ...unit.Type) func(ctx *context.APIContext) { return func(ctx *context.APIContext) { if !ctx.IsUserRepoWriter(unitTypes) && !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() { - ctx.Error( - http.StatusForbidden, - "reqRepoWriter", - "user should have a permission to write to a repo", - ) + ctx.Error(http.StatusForbidden, "reqRepoWriter", "user should have a permission to write to a repo") return } } @@ -452,13 +399,8 @@ func reqRepoWriter(unitTypes ...unit.Type) func(ctx *context.APIContext) { // reqRepoBranchWriter user should have a permission to write to a branch, or be a site admin func reqRepoBranchWriter(ctx *context.APIContext) { options, ok := web.GetForm(ctx).(api.FileOptionInterface) - if !ok || - (!ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, options.Branch()) && !ctx.IsUserSiteAdmin()) { - ctx.Error( - http.StatusForbidden, - "reqRepoBranchWriter", - "user should have a permission to write to this branch", - ) + if !ok || (!ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, options.Branch()) && !ctx.IsUserSiteAdmin()) { + ctx.Error(http.StatusForbidden, "reqRepoBranchWriter", "user should have a permission to write to this branch") return } } @@ -467,11 +409,7 @@ func reqRepoBranchWriter(ctx *context.APIContext) { func reqRepoReader(unitType unit.Type) func(ctx *context.APIContext) { return func(ctx *context.APIContext) { if !ctx.Repo.CanRead(unitType) && !ctx.IsUserRepoAdmin() && !ctx.IsUserSiteAdmin() { - ctx.Error( - http.StatusForbidden, - "reqRepoReader", - "user should have specific read permission or be a repo admin or a site admin", - ) + ctx.Error(http.StatusForbidden, "reqRepoReader", "user should have specific read permission or be a repo admin or a site admin") return } } @@ -481,11 +419,7 @@ func reqRepoReader(unitType unit.Type) func(ctx *context.APIContext) { func reqAnyRepoReader() func(ctx *context.APIContext) { return func(ctx *context.APIContext) { if !ctx.Repo.HasAccess() && !ctx.IsUserSiteAdmin() { - ctx.Error( - http.StatusForbidden, - "reqAnyRepoReader", - "user should have any permission to read repository or permissions of site admin", - ) + ctx.Error(http.StatusForbidden, "reqAnyRepoReader", "user should have any permission to read repository or permissions of site admin") return } } @@ -740,11 +674,7 @@ func mustEnableWiki(ctx *context.APIContext) { func mustNotBeArchived(ctx *context.APIContext) { if ctx.Repo.Repository.IsArchived { - ctx.Error( - http.StatusLocked, - "RepoArchived", - fmt.Errorf("%s is archived", ctx.Repo.Repository.LogString()), - ) + ctx.Error(http.StatusLocked, "RepoArchived", fmt.Errorf("%s is archived", ctx.Repo.Repository.LogString())) return } } @@ -762,11 +692,7 @@ func bind[T any](_ T) any { theObj := new(T) // create a new form obj for every request but not use obj directly errs := binding.Bind(ctx.Req, theObj) if len(errs) > 0 { - ctx.Error( - http.StatusUnprocessableEntity, - "validationError", - fmt.Sprintf("%s: %s", errs[0].FieldNames, errs[0].Error()), - ) + ctx.Error(http.StatusUnprocessableEntity, "validationError", fmt.Sprintf("%s: %s", errs[0].FieldNames, errs[0].Error())) return } web.SetForm(ctx, theObj) @@ -816,11 +742,7 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.APIC return } if !ctx.Doer.IsActive || ctx.Doer.ProhibitLogin { - log.Info( - "Failed authentication attempt for %s from %s", - ctx.Doer.Name, - ctx.RemoteAddr(), - ) + log.Info("Failed authentication attempt for %s from %s", ctx.Doer.Name, ctx.RemoteAddr()) ctx.Data["Title"] = ctx.Tr("auth.prohibit_login") ctx.JSON(http.StatusForbidden, map[string]string{ "message": "This account is prohibited from signing in, please contact your site administrator.", @@ -904,10 +826,8 @@ func Routes() *web.Route { AllowedOrigins: setting.CORSConfig.AllowDomain, AllowedMethods: setting.CORSConfig.Methods, AllowCredentials: setting.CORSConfig.AllowCredentials, - AllowedHeaders: append( - []string{"Authorization", "X-Gitea-OTP"}, - setting.CORSConfig.Headers...), - MaxAge: int(setting.CORSConfig.MaxAge.Seconds()), + AllowedHeaders: append([]string{"Authorization", "X-Gitea-OTP"}, setting.CORSConfig.Headers...), + MaxAge: int(setting.CORSConfig.MaxAge.Seconds()), })) } m.Use(context.APIContexter()) @@ -988,12 +908,7 @@ func Routes() *web.Route { m.Get("/heatmap", user.GetUserHeatmapData) } - m.Get( - "/repos", - tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), - reqExploreSignIn(), - user.ListUserRepos, - ) + m.Get("/repos", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository), reqExploreSignIn(), user.ListUserRepos) m.Group("/tokens", func() { m.Combo("").Get(user.ListAccessTokens). Post(bind(api.CreateAccessTokenOption{}), reqToken(), user.CreateAccessToken) @@ -1087,8 +1002,7 @@ func Routes() *web.Route { m.Post("/gpg_key_verify", bind(api.VerifyGPGKeyOption{}), user.VerifyUserGPGKey) // (repo scope) - m.Combo("/repos", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)). - Get(user.ListMyRepos). + m.Combo("/repos", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)).Get(user.ListMyRepos). Post(bind(api.CreateRepoOption{}), repo.Create) // (repo scope) @@ -1123,20 +1037,13 @@ func Routes() *web.Route { }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken()) // Repositories (requires repo scope, org scope) - m.Post( - "/org/{org}/repos", - tokenRequiresScopes( - auth_model.AccessTokenScopeCategoryOrganization, - auth_model.AccessTokenScopeCategoryRepository, - ), + m.Post("/org/{org}/repos", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization, auth_model.AccessTokenScopeCategoryRepository), reqToken(), bind(api.CreateRepoOption{}), - repo.CreateOrgRepoDeprecated, - ) + repo.CreateOrgRepoDeprecated) // requires repo scope - m.Combo("/repositories/{id}", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)). - Get(repo.GetByID) + m.Combo("/repositories/{id}", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository)).Get(repo.GetByID) // Repos (requires repo scope) m.Group("/repos", func() { @@ -1149,13 +1056,7 @@ func Routes() *web.Route { m.Combo("").Get(reqAnyRepoReader(), repo.Get). Delete(reqToken(), reqOwner(), repo.Delete). 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.Group("/transfer", func() { m.Post("", reqOwner(), bind(api.TransferRepoOption{}), repo.Transfer) m.Post("/accept", repo.AcceptTransfer) @@ -1187,12 +1088,7 @@ func Routes() *web.Route { m.Combo("").Get(repo.GetHook). Patch(bind(api.EditHookOption{}), repo.EditHook). Delete(repo.DeleteHook) - m.Post( - "/tests", - context.ReferencesGitRepo(), - context.RepoRefForAPI, - repo.TestHook, - ) + m.Post("/tests", context.ReferencesGitRepo(), context.RepoRefForAPI, repo.TestHook) }) }, reqToken(), reqAdmin(), reqWebhooksEnabled()) m.Group("/collaborators", func() { @@ -1212,79 +1108,31 @@ func Routes() *web.Route { Put(reqAdmin(), repo.AddTeam). Delete(reqAdmin(), repo.DeleteTeam) }, reqToken()) - m.Get( - "/raw/*", - context.ReferencesGitRepo(), - context.RepoRefForAPI, - reqRepoReader(unit.TypeCode), - repo.GetRawFile, - ) - m.Get( - "/media/*", - context.ReferencesGitRepo(), - context.RepoRefForAPI, - reqRepoReader(unit.TypeCode), - repo.GetRawFileOrLFS, - ) + m.Get("/raw/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFile) + m.Get("/media/*", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetRawFileOrLFS) m.Get("/archive/*", reqRepoReader(unit.TypeCode), repo.GetArchive) m.Combo("/forks").Get(repo.ListForks). Post(reqToken(), reqRepoReader(unit.TypeCode), bind(api.CreateForkOption{}), repo.CreateFork) m.Group("/branches", func() { m.Get("", repo.ListBranches) m.Get("/*", repo.GetBranch) - m.Delete( - "/*", - reqToken(), - reqRepoWriter(unit.TypeCode), - mustNotBeArchived, - repo.DeleteBranch, - ) - m.Post( - "", - reqToken(), - reqRepoWriter(unit.TypeCode), - mustNotBeArchived, - bind(api.CreateBranchRepoOption{}), - repo.CreateBranch, - ) + m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteBranch) + m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateBranchRepoOption{}), repo.CreateBranch) }, context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode)) m.Group("/branch_protections", func() { m.Get("", repo.ListBranchProtections) - m.Post( - "", - bind(api.CreateBranchProtectionOption{}), - mustNotBeArchived, - repo.CreateBranchProtection, - ) + m.Post("", bind(api.CreateBranchProtectionOption{}), mustNotBeArchived, repo.CreateBranchProtection) m.Group("/{name}", func() { m.Get("", repo.GetBranchProtection) - m.Patch( - "", - bind(api.EditBranchProtectionOption{}), - mustNotBeArchived, - repo.EditBranchProtection, - ) + m.Patch("", bind(api.EditBranchProtectionOption{}), mustNotBeArchived, repo.EditBranchProtection) m.Delete("", repo.DeleteBranchProtection) }) }, reqToken(), reqAdmin()) m.Group("/tags", func() { m.Get("", repo.ListTags) m.Get("/*", repo.GetTag) - m.Post( - "", - reqToken(), - reqRepoWriter(unit.TypeCode), - mustNotBeArchived, - bind(api.CreateTagOption{}), - repo.CreateTag, - ) - m.Delete( - "/*", - reqToken(), - reqRepoWriter(unit.TypeCode), - mustNotBeArchived, - repo.DeleteTag, - ) + m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateTagOption{}), repo.CreateTag) + m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteTag) }, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo(true)) m.Group("/keys", func() { m.Combo("").Get(repo.ListDeployKeys). @@ -1302,14 +1150,7 @@ func Routes() *web.Route { Patch(mustNotBeArchived, reqToken(), reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), repo.EditWikiPage). Delete(mustNotBeArchived, reqToken(), reqRepoWriter(unit.TypeWiki), repo.DeleteWikiPage) m.Get("/revisions/{pageName}", repo.ListPageRevisions) - m.Post( - "/new", - reqToken(), - mustNotBeArchived, - reqRepoWriter(unit.TypeWiki), - bind(api.CreateWikiPageOptions{}), - repo.NewWikiPage, - ) + m.Post("/new", reqToken(), mustNotBeArchived, reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), repo.NewWikiPage) m.Get("/pages", repo.ListWikiPages) }, mustEnableWiki) m.Post("/markup", reqToken(), bind(api.MarkupOption{}), misc.Markup) @@ -1354,13 +1195,7 @@ func Routes() *web.Route { Get(repo.GetPushMirrorByName) }, reqAdmin(), reqToken()) - m.Get( - "/editorconfig/{filename}", - context.ReferencesGitRepo(), - context.RepoRefForAPI, - reqRepoReader(unit.TypeCode), - repo.GetEditorconfig, - ) + m.Get("/editorconfig/{filename}", context.ReferencesGitRepo(), context.RepoRefForAPI, reqRepoReader(unit.TypeCode), repo.GetEditorconfig) m.Group("/pulls", func() { m.Combo("").Get(repo.ListPullRequests). Post(reqToken(), mustNotBeArchived, bind(api.CreatePullRequestOption{}), repo.CreatePullRequest) @@ -1386,12 +1221,7 @@ func Routes() *web.Route { Post(reqToken(), bind(api.SubmitPullReviewOptions{}), repo.SubmitPullReview) m.Combo("/comments"). Get(repo.GetPullReviewComments) - m.Post( - "/dismissals", - reqToken(), - bind(api.DismissPullReviewOptions{}), - repo.DismissPullReview, - ) + m.Post("/dismissals", reqToken(), bind(api.DismissPullReviewOptions{}), repo.DismissPullReview) m.Post("/undismissals", reqToken(), repo.UnDismissPullReview) }) }) @@ -1423,47 +1253,15 @@ func Routes() *web.Route { m.Get("/tags/{sha}", repo.GetAnnotatedTag) m.Get("/notes/{sha}", repo.GetNote) }, context.ReferencesGitRepo(true), reqRepoReader(unit.TypeCode)) - m.Post( - "/diffpatch", - reqRepoWriter(unit.TypeCode), - reqToken(), - bind(api.ApplyDiffPatchFileOptions{}), - mustNotBeArchived, - repo.ApplyDiffPatch, - ) + m.Post("/diffpatch", reqRepoWriter(unit.TypeCode), reqToken(), bind(api.ApplyDiffPatchFileOptions{}), mustNotBeArchived, repo.ApplyDiffPatch) m.Group("/contents", func() { m.Get("", repo.GetContentsList) - m.Post( - "", - reqToken(), - bind(api.ChangeFilesOptions{}), - reqRepoBranchWriter, - mustNotBeArchived, - repo.ChangeFiles, - ) + m.Post("", reqToken(), bind(api.ChangeFilesOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.ChangeFiles) m.Get("/*", repo.GetContents) m.Group("/*", func() { - m.Post( - "", - bind(api.CreateFileOptions{}), - reqRepoBranchWriter, - mustNotBeArchived, - repo.CreateFile, - ) - m.Put( - "", - bind(api.UpdateFileOptions{}), - reqRepoBranchWriter, - mustNotBeArchived, - repo.UpdateFile, - ) - m.Delete( - "", - bind(api.DeleteFileOptions{}), - reqRepoBranchWriter, - mustNotBeArchived, - repo.DeleteFile, - ) + m.Post("", bind(api.CreateFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.CreateFile) + m.Put("", bind(api.UpdateFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.UpdateFile) + m.Delete("", bind(api.DeleteFileOptions{}), reqRepoBranchWriter, mustNotBeArchived, repo.DeleteFile) }, reqToken()) }, reqRepoReader(unit.TypeCode)) m.Get("/signing-key.gpg", misc.SigningKey) @@ -1477,11 +1275,7 @@ func Routes() *web.Route { }, reqAnyRepoReader()) m.Get("/issue_templates", context.ReferencesGitRepo(), repo.GetIssueTemplates) m.Get("/issue_config", context.ReferencesGitRepo(), repo.GetIssueConfig) - m.Get( - "/issue_config/validate", - context.ReferencesGitRepo(), - repo.ValidateIssueConfig, - ) + m.Get("/issue_config/validate", context.ReferencesGitRepo(), repo.ValidateIssueConfig) m.Get("/languages", reqRepoReader(unit.TypeCode), repo.GetLanguages) m.Get("/activities/feeds", repo.ListRepoActivityFeeds) m.Get("/new_pin_allowed", repo.AreNewIssuePinsAllowed) @@ -1539,8 +1333,7 @@ func Routes() *web.Route { m.Group("/comments", func() { m.Combo("").Get(repo.ListIssueComments). Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueCommentOption{}), repo.CreateIssueComment) - m.Combo("/{id}", reqToken()). - Patch(bind(api.EditIssueCommentOption{}), repo.EditIssueCommentDeprecated). + m.Combo("/{id}", reqToken()).Patch(bind(api.EditIssueCommentOption{}), repo.EditIssueCommentDeprecated). Delete(repo.DeleteIssueCommentDeprecated) }) m.Get("/timeline", repo.ListIssueCommentsAndTimeline) @@ -1558,8 +1351,7 @@ func Routes() *web.Route { Delete(repo.ResetIssueTime) m.Delete("/{id}", repo.DeleteTime) }, reqToken()) - m.Combo("/deadline"). - Post(reqToken(), bind(api.EditDeadlineOption{}), repo.UpdateIssueDeadline) + m.Combo("/deadline").Post(reqToken(), bind(api.EditDeadlineOption{}), repo.UpdateIssueDeadline) m.Group("/stopwatch", func() { m.Post("/start", repo.StartIssueStopwatch) m.Post("/stop", repo.StopIssueStopwatch) @@ -1615,9 +1407,7 @@ func Routes() *web.Route { Delete(reqToken(), reqRepoWriter(unit.TypeIssues, unit.TypePullRequests), repo.DeleteMilestone) }) m.Group("/projects", func() { - m. - Combo(""). - Get(projects.ListRepoProjects). + m.Combo("").Get(projects.ListRepoProjects). Post(bind(api.NewProjectPayload{}), projects.CreateRepoProject) }, mustEnableIssues) }, repoAssignment()) @@ -1627,43 +1417,20 @@ func Routes() *web.Route { m.Group("/packages/{username}", func() { m.Group("/{type}/{name}/{version}", func() { m.Get("", reqToken(), packages.GetPackage) - m.Delete( - "", - reqToken(), - reqPackageAccess(perm.AccessModeWrite), - packages.DeletePackage, - ) + m.Delete("", reqToken(), reqPackageAccess(perm.AccessModeWrite), packages.DeletePackage) m.Get("/files", reqToken(), packages.ListPackageFiles) }) m.Get("/", reqToken(), packages.ListPackages) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context_service.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead)) // Organizations - m.Get( - "/user/orgs", - reqToken(), - tokenRequiresScopes( - auth_model.AccessTokenScopeCategoryUser, - auth_model.AccessTokenScopeCategoryOrganization, - ), - org.ListMyOrgs, - ) + m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), org.ListMyOrgs) m.Group("/users/{username}/orgs", func() { m.Get("", reqToken(), org.ListUserOrgs) m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context_service.UserAssignmentAPI()) - m.Post( - "/orgs", - tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), - reqToken(), - bind(api.CreateOrgOption{}), - org.Create, - ) - m.Get( - "/orgs", - org.GetAll, - tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), - ) + m.Post("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), reqToken(), bind(api.CreateOrgOption{}), org.Create) + m.Get("/orgs", org.GetAll, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization)) m.Group("/orgs/{org}", func() { m.Combo("").Get(org.Get). Patch(reqToken(), reqOrgOwnership(), bind(api.EditOrgOption{}), org.Edit). @@ -1700,13 +1467,7 @@ func Routes() *web.Route { }, reqToken(), reqOrgMembership()) m.Group("/labels", func() { m.Get("", org.ListLabels) - m.Post( - "", - reqToken(), - reqOrgOwnership(), - bind(api.CreateLabelOption{}), - org.CreateLabel, - ) + m.Post("", reqToken(), reqOrgOwnership(), bind(api.CreateLabelOption{}), org.CreateLabel) m.Combo("/{id}").Get(reqToken(), org.GetLabel). Patch(reqToken(), reqOrgOwnership(), bind(api.EditLabelOption{}), org.EditLabel). Delete(reqToken(), reqOrgOwnership(), org.DeleteLabel) @@ -1723,10 +1484,8 @@ func Routes() *web.Route { m.Delete("", org.DeleteAvatar) }, reqToken(), reqOrgOwnership()) m.Get("/activities/feeds", org.ListOrgActivityFeeds) - m.Group("/projects", func() { - m.Combo(""). - Get(projects.ListOrgProjects). + m.Combo("").Get(projects.ListOrgProjects). Post(bind(api.NewProjectPayload{}), projects.CreateOrgProject) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue), reqToken(), reqOrgMembership()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true)) @@ -1793,11 +1552,8 @@ func Routes() *web.Route { m.Get("/registration-token", admin.GetRegistrationToken) }) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryAdmin), reqToken(), reqSiteAdmin()) - m.Group("/projects", func() { - m. - Combo("/{id}"). - Get(projects.GetProject). + m.Combo("/{id}").Get(projects.GetProject). Patch(bind(api.UpdateProjectPayload{}), projects.UpdateProject). Delete(projects.DeleteProject) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryIssue), reqToken()) From 065001c0815d3efdaa4fc8519ef1d1f89a27ba28 Mon Sep 17 00:00:00 2001 From: Denys Konovalov Date: Sun, 21 Jan 2024 15:32:42 +0100 Subject: [PATCH 17/31] more reverts --- routers/api/v1/api.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index df2222acb8b48..408952edb0390 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1037,7 +1037,8 @@ func Routes() *web.Route { }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken()) // Repositories (requires repo scope, org scope) - m.Post("/org/{org}/repos", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization, auth_model.AccessTokenScopeCategoryRepository), + m.Post("/org/{org}/repos", + tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization, auth_model.AccessTokenScopeCategoryRepository), reqToken(), bind(api.CreateRepoOption{}), repo.CreateOrgRepoDeprecated) @@ -1552,6 +1553,7 @@ func Routes() *web.Route { m.Get("/registration-token", admin.GetRegistrationToken) }) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryAdmin), reqToken(), reqSiteAdmin()) + m.Group("/projects", func() { m.Combo("/{id}").Get(projects.GetProject). Patch(bind(api.UpdateProjectPayload{}), projects.UpdateProject). From 58e56cd0c67b9945194ffe338f8c7efeca2b205c Mon Sep 17 00:00:00 2001 From: Denys Konovalov Date: Sun, 21 Jan 2024 15:39:55 +0100 Subject: [PATCH 18/31] more formatting fixes --- routers/api/v1/projects/project.go | 35 ++++++++++----------------- tests/integration/api_project_test.go | 4 +-- 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/routers/api/v1/projects/project.go b/routers/api/v1/projects/project.go index 4aed86d7de4dd..4bcf241082801 100644 --- a/routers/api/v1/projects/project.go +++ b/routers/api/v1/projects/project.go @@ -15,10 +15,7 @@ import ( "code.gitea.io/gitea/services/convert" ) -func innerCreateProject( - ctx *context.APIContext, - projectType project_model.Type, -) { +func innerCreateProject(ctx *context.APIContext, projectType project_model.Type) { form := web.GetForm(ctx).(*api.NewProjectPayload) project := &project_model.Project{ RepoID: 0, @@ -287,12 +284,10 @@ func ListUserProjects(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" projects, count, err := db.FindAndCount[project_model.Project](ctx, project_model.SearchOptions{ - Type: project_model.TypeIndividual, - IsClosed: ctx.FormOptionalBool("closed"), - OwnerID: ctx.Doer.ID, - ListOptions: db.ListOptions{ - Page: ctx.FormInt("page"), - }, + Type: project_model.TypeIndividual, + IsClosed: ctx.FormOptionalBool("closed"), + OwnerID: ctx.Doer.ID, + ListOptions: db.ListOptions{Page: ctx.FormInt("page")}, }) if err != nil { ctx.Error(http.StatusInternalServerError, "Projects", err) @@ -343,12 +338,10 @@ func ListOrgProjects(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" projects, count, err := db.FindAndCount[project_model.Project](ctx, project_model.SearchOptions{ - OwnerID: ctx.Org.Organization.AsUser().ID, - ListOptions: db.ListOptions{ - Page: ctx.FormInt("page"), - }, - IsClosed: ctx.FormOptionalBool("closed"), - Type: project_model.TypeOrganization, + OwnerID: ctx.Org.Organization.AsUser().ID, + ListOptions: db.ListOptions{Page: ctx.FormInt("page")}, + IsClosed: ctx.FormOptionalBool("closed"), + Type: project_model.TypeOrganization, }) if err != nil { ctx.Error(http.StatusInternalServerError, "Projects", err) @@ -404,12 +397,10 @@ func ListRepoProjects(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" projects, count, err := db.FindAndCount[project_model.Project](ctx, project_model.SearchOptions{ - RepoID: ctx.Repo.Repository.ID, - IsClosed: ctx.FormOptionalBool("closed"), - Type: project_model.TypeRepository, - ListOptions: db.ListOptions{ - Page: ctx.FormInt("page"), - }, + RepoID: ctx.Repo.Repository.ID, + IsClosed: ctx.FormOptionalBool("closed"), + Type: project_model.TypeRepository, + ListOptions: db.ListOptions{Page: ctx.FormInt("page")}, }) if err != nil { ctx.Error(http.StatusInternalServerError, "Projects", err) diff --git a/tests/integration/api_project_test.go b/tests/integration/api_project_test.go index 03ada63336729..15f431683c55c 100644 --- a/tests/integration/api_project_test.go +++ b/tests/integration/api_project_test.go @@ -149,9 +149,7 @@ func TestAPIUpdateProject(t *testing.T) { token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteUser, auth_model.AccessTokenScopeWriteIssue) link, _ := url.Parse(fmt.Sprintf("/api/v1/projects/%d", 1)) - req := NewRequestWithJSON(t, "PATCH", link.String(), &api.UpdateProjectPayload{ - Title: "First project updated", - }).AddTokenAuth(token) + req := NewRequestWithJSON(t, "PATCH", link.String(), &api.UpdateProjectPayload{Title: "First project updated"}).AddTokenAuth(token) var apiProject *api.Project From 74043f7b4c1b9048c9be2d6ce587315b85b0ef31 Mon Sep 17 00:00:00 2001 From: Denys Konovalov Date: Sun, 21 Jan 2024 15:48:45 +0100 Subject: [PATCH 19/31] fix project sort test --- models/project/project_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/models/project/project_test.go b/models/project/project_test.go index 7a37c1faf2908..8a6dcfee9b52d 100644 --- a/models/project/project_test.go +++ b/models/project/project_test.go @@ -92,19 +92,19 @@ func TestProjectsSort(t *testing.T) { }{ { sortType: "default", - wants: []int64{1, 3, 2, 4}, + wants: []int64{1, 3, 2, 4, 5, 6}, }, { sortType: "oldest", - wants: []int64{4, 2, 3, 1}, + wants: []int64{4, 5, 6, 2, 3, 1}, }, { sortType: "recentupdate", - wants: []int64{1, 3, 2, 4}, + wants: []int64{1, 3, 2, 4, 5, 6}, }, { sortType: "leastupdate", - wants: []int64{4, 2, 3, 1}, + wants: []int64{4, 5, 6, 2, 3, 1}, }, } @@ -113,8 +113,8 @@ func TestProjectsSort(t *testing.T) { OrderBy: GetSearchOrderByBySortType(tt.sortType), }) assert.NoError(t, err) - assert.EqualValues(t, int64(4), count) - if assert.Len(t, projects, 4) { + assert.EqualValues(t, int64(6), count) + if assert.Len(t, projects, 6) { for i := range projects { assert.EqualValues(t, tt.wants[i], projects[i].ID) } From 4475c4f6fc43d988d50868a0aa062c1d2bbbc339 Mon Sep 17 00:00:00 2001 From: Denys Konovalov Date: Mon, 22 Jan 2024 13:43:10 +0100 Subject: [PATCH 20/31] actually fix test! --- models/project/project_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/models/project/project_test.go b/models/project/project_test.go index 8a6dcfee9b52d..8fbbdedecf012 100644 --- a/models/project/project_test.go +++ b/models/project/project_test.go @@ -92,7 +92,7 @@ func TestProjectsSort(t *testing.T) { }{ { sortType: "default", - wants: []int64{1, 3, 2, 4, 5, 6}, + wants: []int64{1, 3, 2, 6, 5, 4}, }, { sortType: "oldest", @@ -100,7 +100,7 @@ func TestProjectsSort(t *testing.T) { }, { sortType: "recentupdate", - wants: []int64{1, 3, 2, 4, 5, 6}, + wants: []int64{1, 3, 2, 6, 5, 4}, }, { sortType: "leastupdate", From 927da10445beb48a0c558c87d9c6ef8c82f2a478 Mon Sep 17 00:00:00 2001 From: dineshsalunke Date: Fri, 26 Jan 2024 10:07:47 +0530 Subject: [PATCH 21/31] chore: swagger doc typo corrections --- routers/api/v1/projects/project.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/routers/api/v1/projects/project.go b/routers/api/v1/projects/project.go index 4bcf241082801..8758f538e740d 100644 --- a/routers/api/v1/projects/project.go +++ b/routers/api/v1/projects/project.go @@ -260,13 +260,13 @@ func DeleteProject(ctx *context.APIContext) { func ListUserProjects(ctx *context.APIContext) { // swagger:operation GET /user/projects project projectListUserProjects // --- - // summary: List repository projects + // summary: List user projects // produces: // - application/json // parameters: // - name: closed // in: query - // description: include closed issues or not + // description: include closed projects or not // type: boolean // - name: page // in: query @@ -290,7 +290,7 @@ func ListUserProjects(ctx *context.APIContext) { ListOptions: db.ListOptions{Page: ctx.FormInt("page")}, }) if err != nil { - ctx.Error(http.StatusInternalServerError, "Projects", err) + ctx.Error(http.StatusInternalServerError, "ListUserProjets", err) return } @@ -299,7 +299,7 @@ func ListUserProjects(ctx *context.APIContext) { apiProjects, err := convert.ToAPIProjectList(ctx, projects) if err != nil { - ctx.Error(http.StatusInternalServerError, "Projects", err) + ctx.Error(http.StatusInternalServerError, "ListUserProjects", err) return } @@ -309,7 +309,7 @@ func ListUserProjects(ctx *context.APIContext) { func ListOrgProjects(ctx *context.APIContext) { // swagger:operation GET /orgs/{org}/projects project projectListOrgProjects // --- - // summary: List repository projects + // summary: List org projects // produces: // - application/json // parameters: @@ -320,7 +320,7 @@ func ListOrgProjects(ctx *context.APIContext) { // required: true // - name: closed // in: query - // description: include closed issues or not + // description: include closed projects or not // type: boolean // - name: page // in: query @@ -344,7 +344,7 @@ func ListOrgProjects(ctx *context.APIContext) { Type: project_model.TypeOrganization, }) if err != nil { - ctx.Error(http.StatusInternalServerError, "Projects", err) + ctx.Error(http.StatusInternalServerError, "ListOrgProjects", err) return } @@ -353,7 +353,7 @@ func ListOrgProjects(ctx *context.APIContext) { apiProjects, err := convert.ToAPIProjectList(ctx, projects) if err != nil { - ctx.Error(http.StatusInternalServerError, "Projects", err) + ctx.Error(http.StatusInternalServerError, "ListOrgProjects", err) return } @@ -379,7 +379,7 @@ func ListRepoProjects(ctx *context.APIContext) { // required: true // - name: closed // in: query - // description: include closed issues or not + // description: include closed projects or not // type: boolean // - name: page // in: query @@ -403,7 +403,7 @@ func ListRepoProjects(ctx *context.APIContext) { ListOptions: db.ListOptions{Page: ctx.FormInt("page")}, }) if err != nil { - ctx.Error(http.StatusInternalServerError, "Projects", err) + ctx.Error(http.StatusInternalServerError, "ListRepoProjects", err) return } @@ -412,7 +412,7 @@ func ListRepoProjects(ctx *context.APIContext) { apiProjects, err := convert.ToAPIProjectList(ctx, projects) if err != nil { - ctx.Error(http.StatusInternalServerError, "Projects", err) + ctx.Error(http.StatusInternalServerError, "ListRepoProjects", err) return } From 975d98d8f1645c91045875d07b0fab718e18405c Mon Sep 17 00:00:00 2001 From: dineshsalunke Date: Fri, 26 Jan 2024 10:08:00 +0530 Subject: [PATCH 22/31] chore: remove redundant comment --- services/convert/project.go | 1 - 1 file changed, 1 deletion(-) diff --git a/services/convert/project.go b/services/convert/project.go index 42ce611e15bbe..f721dc9ca2fcf 100644 --- a/services/convert/project.go +++ b/services/convert/project.go @@ -21,7 +21,6 @@ func ToAPIProject(ctx context.Context, project *project_model.Project) (*api.Pro Closed: project.ClosedDateUnix.AsTime(), } - // try to laod the repo _ = project.LoadRepo(ctx) if project.Repo != nil { apiProject.Repo = &api.RepositoryMeta{ From d4ea5a57d465cab30ab71feb35b3fe9363068abb Mon Sep 17 00:00:00 2001 From: dineshsalunke Date: Fri, 26 Jan 2024 10:08:24 +0530 Subject: [PATCH 23/31] fix: use the page field from parameters --- routers/api/v1/projects/project.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/routers/api/v1/projects/project.go b/routers/api/v1/projects/project.go index 8758f538e740d..04ed55f22363d 100644 --- a/routers/api/v1/projects/project.go +++ b/routers/api/v1/projects/project.go @@ -396,18 +396,20 @@ func ListRepoProjects(ctx *context.APIContext) { // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" + + page := ctx.FormInt("page") projects, count, err := db.FindAndCount[project_model.Project](ctx, project_model.SearchOptions{ RepoID: ctx.Repo.Repository.ID, IsClosed: ctx.FormOptionalBool("closed"), Type: project_model.TypeRepository, - ListOptions: db.ListOptions{Page: ctx.FormInt("page")}, + ListOptions: db.ListOptions{Page: page}, }) if err != nil { ctx.Error(http.StatusInternalServerError, "ListRepoProjects", err) return } - ctx.SetLinkHeader(int(count), setting.UI.IssuePagingNum) + ctx.SetLinkHeader(int(count), page) ctx.SetTotalCountHeader(count) apiProjects, err := convert.ToAPIProjectList(ctx, projects) From 0a0837f47b67180311556636e7fe98530b76f618 Mon Sep 17 00:00:00 2001 From: dineshsalunke Date: Fri, 26 Jan 2024 11:15:37 +0530 Subject: [PATCH 24/31] chore: update the swagger file --- templates/swagger/v1_json.tmpl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index ee320f6d621ac..3c14c36da44ce 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -2365,7 +2365,7 @@ "tags": [ "project" ], - "summary": "List repository projects", + "summary": "List org projects", "operationId": "projectListOrgProjects", "parameters": [ { @@ -2377,7 +2377,7 @@ }, { "type": "boolean", - "description": "include closed issues or not", + "description": "include closed projects or not", "name": "closed", "in": "query" }, @@ -10401,7 +10401,7 @@ }, { "type": "boolean", - "description": "include closed issues or not", + "description": "include closed projects or not", "name": "closed", "in": "query" }, @@ -15872,12 +15872,12 @@ "tags": [ "project" ], - "summary": "List repository projects", + "summary": "List user projects", "operationId": "projectListUserProjects", "parameters": [ { "type": "boolean", - "description": "include closed issues or not", + "description": "include closed projects or not", "name": "closed", "in": "query" }, From a7aabc5349b2e7c322b0d78da39a3ff196d3925c Mon Sep 17 00:00:00 2001 From: Dinesh Salunke Date: Sun, 22 Sep 2024 09:17:06 +0530 Subject: [PATCH 25/31] refactor: add the missing parameter bodies for project --- routers/api/v1/swagger/options.go | 6 ++++++ templates/swagger/v1_json.tmpl | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index 1de58632d57fa..f717a49094b4e 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -205,4 +205,10 @@ type swaggerParameterBodies struct { // in:body UpdateVariableOption api.UpdateVariableOption + + // in:body + NewProjectPayload api.NewProjectPayload + + // in:body + UpdateProjectPayload api.UpdateProjectPayload } diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 2fb80388ad776..f30c8b70e85e4 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -26745,7 +26745,7 @@ "parameterBodies": { "description": "parameterBodies", "schema": { - "$ref": "#/definitions/UpdateVariableOption" + "$ref": "#/definitions/UpdateProjectPayload" } }, "redirect": { @@ -26844,4 +26844,4 @@ "TOTPHeader": [] } ] -} +} \ No newline at end of file From 7383e7bf324feb07110c331cb54b691de995268b Mon Sep 17 00:00:00 2001 From: Dinesh Salunke Date: Sun, 22 Sep 2024 11:59:05 +0530 Subject: [PATCH 26/31] refactor: rename BoardType to TemplateType --- services/convert/project.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/services/convert/project.go b/services/convert/project.go index f721dc9ca2fcf..f5e5ecf41e946 100644 --- a/services/convert/project.go +++ b/services/convert/project.go @@ -12,13 +12,13 @@ import ( func ToAPIProject(ctx context.Context, project *project_model.Project) (*api.Project, error) { apiProject := &api.Project{ - Title: project.Title, - Description: project.Description, - BoardType: uint8(project.BoardType), - IsClosed: project.IsClosed, - Created: project.CreatedUnix.AsTime(), - Updated: project.UpdatedUnix.AsTime(), - Closed: project.ClosedDateUnix.AsTime(), + Title: project.Title, + Description: project.Description, + TemplateType: uint8(project.TemplateType), + IsClosed: project.IsClosed, + Created: project.CreatedUnix.AsTime(), + Updated: project.UpdatedUnix.AsTime(), + Closed: project.ClosedDateUnix.AsTime(), } _ = project.LoadRepo(ctx) From 016aa727c05302d61197b4583ed66091316df08e Mon Sep 17 00:00:00 2001 From: Dinesh Salunke Date: Sun, 22 Sep 2024 11:59:23 +0530 Subject: [PATCH 27/31] refactor: add creator property to the project --- models/project/project.go | 1 + 1 file changed, 1 insertion(+) diff --git a/models/project/project.go b/models/project/project.go index d6f6db2b5a29d..5c1fafa20505a 100644 --- a/models/project/project.go +++ b/models/project/project.go @@ -91,6 +91,7 @@ type Project struct { RepoID int64 `xorm:"INDEX"` Repo *repo_model.Repository `xorm:"-"` CreatorID int64 `xorm:"NOT NULL"` + Creator *user_model.User `xorm:"-"` IsClosed bool `xorm:"INDEX"` TemplateType TemplateType `xorm:"'board_type'"` // TODO: rename the column to template_type CardType CardType From d067b1d34feed6e857292b3174f46f9a40000005 Mon Sep 17 00:00:00 2001 From: Dinesh Salunke Date: Sun, 22 Sep 2024 11:59:41 +0530 Subject: [PATCH 28/31] refactor: add BoardType to TemplateType property --- modules/structs/project.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/structs/project.go b/modules/structs/project.go index 46e7bebfcd9b8..26f23d6fd6740 100644 --- a/modules/structs/project.go +++ b/modules/structs/project.go @@ -25,11 +25,11 @@ type UpdateProjectPayload struct { // swagger:model type Project struct { - ID int64 `json:"id"` - Title string `json:"title"` - Description string `json:"description"` - BoardType uint8 `json:"board_type"` - IsClosed bool `json:"is_closed"` + ID int64 `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + TemplateType uint8 `json:"board_type"` + IsClosed bool `json:"is_closed"` // swagger:strfmt date-time Created time.Time `json:"created_at"` // swagger:strfmt date-time From eeca89efb8217864683f46bba96df1b6dcb931ec Mon Sep 17 00:00:00 2001 From: Dinesh Salunke Date: Sun, 22 Sep 2024 12:00:14 +0530 Subject: [PATCH 29/31] refactor: handle errors when converting to APIProject --- services/convert/project.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/services/convert/project.go b/services/convert/project.go index f5e5ecf41e946..1cb00696abd15 100644 --- a/services/convert/project.go +++ b/services/convert/project.go @@ -21,7 +21,9 @@ func ToAPIProject(ctx context.Context, project *project_model.Project) (*api.Pro Closed: project.ClosedDateUnix.AsTime(), } - _ = project.LoadRepo(ctx) + if err := project.LoadRepo(ctx); err != nil { + return nil, err + } if project.Repo != nil { apiProject.Repo = &api.RepositoryMeta{ ID: project.RepoID, @@ -31,7 +33,9 @@ func ToAPIProject(ctx context.Context, project *project_model.Project) (*api.Pro } } - _ = project.LoadCreator(ctx) + if err := project.LoadCreator(ctx); err != nil { + return nil, err + } if project.Creator != nil { apiProject.Creator = &api.User{ ID: project.Creator.ID, @@ -40,7 +44,9 @@ func ToAPIProject(ctx context.Context, project *project_model.Project) (*api.Pro } } - _ = project.LoadOwner(ctx) + if err := project.LoadOwner(ctx); err != nil { + return nil, err + } if project.Owner != nil { apiProject.Owner = &api.User{ ID: project.Owner.ID, From 185a594d329719a583668ff2d912a0b772d984da Mon Sep 17 00:00:00 2001 From: Dinesh Salunke Date: Mon, 23 Sep 2024 07:49:48 +0530 Subject: [PATCH 30/31] refactor: update the context --- routers/api/v1/projects/project.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/routers/api/v1/projects/project.go b/routers/api/v1/projects/project.go index 04ed55f22363d..277ca6b58386b 100644 --- a/routers/api/v1/projects/project.go +++ b/routers/api/v1/projects/project.go @@ -8,23 +8,23 @@ import ( "code.gitea.io/gitea/models/db" project_model "code.gitea.io/gitea/models/project" - "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/convert" ) func innerCreateProject(ctx *context.APIContext, projectType project_model.Type) { form := web.GetForm(ctx).(*api.NewProjectPayload) project := &project_model.Project{ - RepoID: 0, - OwnerID: ctx.Doer.ID, - Title: form.Title, - Description: form.Description, - CreatorID: ctx.Doer.ID, - BoardType: project_model.BoardType(form.BoardType), - Type: projectType, + RepoID: 0, + OwnerID: ctx.Doer.ID, + Title: form.Title, + Description: form.Description, + CreatorID: ctx.Doer.ID, + TemplateType: project_model.TemplateType(form.BoardType), + Type: projectType, } if ctx.ContextUser != nil { @@ -158,7 +158,7 @@ func GetProject(ctx *context.APIContext) { // "$ref": "#/responses/forbidden" // "404": // "$ref": "#/responses/notFound" - project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id")) + project, err := project_model.GetProjectByID(ctx, ctx.FormInt64(":id")) if err != nil { if project_model.IsErrProjectNotExist(err) { ctx.NotFound() @@ -202,7 +202,7 @@ func UpdateProject(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" form := web.GetForm(ctx).(*api.UpdateProjectPayload) - project, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64("id")) + project, err := project_model.GetProjectByID(ctx, ctx.FormInt64("id")) if err != nil { if project_model.IsErrProjectNotExist(err) { ctx.NotFound() @@ -249,7 +249,7 @@ func DeleteProject(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - if err := project_model.DeleteProjectByID(ctx, ctx.ParamsInt64(":id")); err != nil { + if err := project_model.DeleteProjectByID(ctx, ctx.FormInt64(":id")); err != nil { ctx.Error(http.StatusInternalServerError, "DeleteProjectByID", err) return } From 30027ac0bddf9b90720abd0956e7d7ecbc8ceef2 Mon Sep 17 00:00:00 2001 From: Dinesh Salunke Date: Mon, 23 Sep 2024 07:50:11 +0530 Subject: [PATCH 31/31] refactor: update the board type enums --- tests/integration/api_project_test.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/integration/api_project_test.go b/tests/integration/api_project_test.go index 15f431683c55c..740610050a4cd 100644 --- a/tests/integration/api_project_test.go +++ b/tests/integration/api_project_test.go @@ -20,7 +20,7 @@ import ( func TestAPICreateUserProject(t *testing.T) { defer tests.PrepareTestEnv(t)() - const title, description, boardType = "project_name", "project_description", uint8(project_model.BoardTypeBasicKanban) + const title, description, boardType = "project_name", "project_description", uint8(project_model.TemplateTypeBasicKanban) token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteIssue, auth_model.AccessTokenScopeWriteUser) @@ -34,13 +34,13 @@ func TestAPICreateUserProject(t *testing.T) { DecodeJSON(t, resp, &apiProject) assert.Equal(t, title, apiProject.Title) assert.Equal(t, description, apiProject.Description) - assert.Equal(t, boardType, apiProject.BoardType) + assert.Equal(t, boardType, apiProject.TemplateType) assert.Equal(t, "user2", apiProject.Creator.UserName) } func TestAPICreateOrgProject(t *testing.T) { defer tests.PrepareTestEnv(t)() - const title, description, boardType = "project_name", "project_description", uint8(project_model.BoardTypeBasicKanban) + const title, description, boardType = "project_name", "project_description", uint8(project_model.TemplateTypeBasicKanban) orgName := "org17" token := getUserToken(t, "user2", auth_model.AccessTokenScopeWriteIssue, auth_model.AccessTokenScopeWriteOrganization) @@ -56,14 +56,14 @@ func TestAPICreateOrgProject(t *testing.T) { DecodeJSON(t, resp, &apiProject) assert.Equal(t, title, apiProject.Title) assert.Equal(t, description, apiProject.Description) - assert.Equal(t, boardType, apiProject.BoardType) + assert.Equal(t, boardType, apiProject.TemplateType) assert.Equal(t, "user2", apiProject.Creator.UserName) assert.Equal(t, "org17", apiProject.Owner.UserName) } func TestAPICreateRepoProject(t *testing.T) { defer tests.PrepareTestEnv(t)() - const title, description, boardType = "project_name", "project_description", uint8(project_model.BoardTypeBasicKanban) + const title, description, boardType = "project_name", "project_description", uint8(project_model.TemplateTypeBasicKanban) ownerName := "user2" repoName := "repo1" @@ -80,7 +80,7 @@ func TestAPICreateRepoProject(t *testing.T) { DecodeJSON(t, resp, &apiProject) assert.Equal(t, title, apiProject.Title) assert.Equal(t, description, apiProject.Description) - assert.Equal(t, boardType, apiProject.BoardType) + assert.Equal(t, boardType, apiProject.TemplateType) assert.Equal(t, "repo1", apiProject.Repo.Name) }