mirror of
https://github.com/go-gitea/gitea.git
synced 2026-04-04 03:35:05 +02:00
Simplify project board API: use state field, extract helpers, remove dead code
- Replace separate POST /close and /reopen endpoints with a state field on EditProjectOption, matching the milestone and issue API patterns - Extract getRepoProjectByID and getRepoProjectColumn helpers to deduplicate repeated lookup-and-error-handle patterns - Use LoadIssueNumbersForProject (singular) for single-project handlers - Remove unnecessary LoadIssueNumbersForProjects call on CreateProject since a new project always has zero issues - Remove unnecessary WHAT comments - Fix copyright year in routers/api/v1/repo/project.go Co-Authored-By: Claude (claude-opus-4-6) <noreply@anthropic.com>
This commit is contained in:
parent
d39f321583
commit
c0cdc8b118
@ -71,6 +71,8 @@ type EditProjectOption struct {
|
||||
Description *string `json:"description,omitempty"`
|
||||
// Card type: 0=text_only, 1=images_and_text
|
||||
CardType *int `json:"card_type,omitempty"`
|
||||
// State of the project (open or closed)
|
||||
State *string `json:"state,omitempty"`
|
||||
}
|
||||
|
||||
// ProjectColumn represents a project column (board)
|
||||
|
||||
@ -1580,8 +1580,6 @@ func Routes() *web.Router {
|
||||
m.Combo("").Get(repo.GetProject).
|
||||
Patch(reqToken(), reqRepoWriter(unit.TypeProjects), bind(api.EditProjectOption{}), repo.EditProject).
|
||||
Delete(reqToken(), reqRepoWriter(unit.TypeProjects), repo.DeleteProject)
|
||||
m.Post("/close", reqToken(), reqRepoWriter(unit.TypeProjects), repo.CloseProject)
|
||||
m.Post("/reopen", reqToken(), reqRepoWriter(unit.TypeProjects), repo.ReopenProject)
|
||||
m.Combo("/columns").Get(repo.ListProjectColumns).
|
||||
Post(reqToken(), reqRepoWriter(unit.TypeProjects), mustNotBeArchived, bind(api.CreateProjectColumnOption{}), repo.CreateProjectColumn)
|
||||
})
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
@ -18,6 +18,41 @@ import (
|
||||
project_service "code.gitea.io/gitea/services/projects"
|
||||
)
|
||||
|
||||
func getRepoProjectByID(ctx *context.APIContext) *project_model.Project {
|
||||
project, err := project_model.GetProjectForRepoByID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
if project_model.IsErrProjectNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return project
|
||||
}
|
||||
|
||||
func getRepoProjectColumn(ctx *context.APIContext) *project_model.Column {
|
||||
column, err := project_model.GetColumn(ctx, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
if project_model.IsErrProjectColumnNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
_, err = project_model.GetProjectForRepoByID(ctx, ctx.Repo.Repository.ID, column.ProjectID)
|
||||
if err != nil {
|
||||
if project_model.IsErrProjectNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return column
|
||||
}
|
||||
|
||||
// ListProjects lists all projects in a repository
|
||||
func ListProjects(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/projects repository repoListProjects
|
||||
@ -124,17 +159,12 @@ func GetProject(ctx *context.APIContext) {
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
project, err := project_model.GetProjectForRepoByID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
if project_model.IsErrProjectNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
project := getRepoProjectByID(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := project_service.LoadIssueNumbersForProjects(ctx, []*project_model.Project{project}, ctx.Doer); err != nil {
|
||||
if err := project_service.LoadIssueNumbersForProject(ctx, project, ctx.Doer); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
@ -191,11 +221,6 @@ func CreateProject(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := project_service.LoadIssueNumbersForProjects(ctx, []*project_model.Project{p}, ctx.Doer); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusCreated, convert.ToProject(ctx, p))
|
||||
}
|
||||
|
||||
@ -237,13 +262,8 @@ func EditProject(ctx *context.APIContext) {
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
project, err := project_model.GetProjectForRepoByID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
if project_model.IsErrProjectNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
project := getRepoProjectByID(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
@ -263,97 +283,17 @@ func EditProject(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := project_service.LoadIssueNumbersForProjects(ctx, []*project_model.Project{project}, ctx.Doer); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToProject(ctx, project))
|
||||
}
|
||||
|
||||
// CloseProject closes a project
|
||||
func CloseProject(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/projects/{id}/close repository repoCloseProject
|
||||
// ---
|
||||
// summary: Close a project
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the project
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Project"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
changeProjectStatus(ctx, true)
|
||||
}
|
||||
|
||||
// ReopenProject reopens a project
|
||||
func ReopenProject(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/projects/{id}/reopen repository repoReopenProject
|
||||
// ---
|
||||
// summary: Reopen a project
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: id
|
||||
// in: path
|
||||
// description: id of the project
|
||||
// type: integer
|
||||
// format: int64
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/Project"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
changeProjectStatus(ctx, false)
|
||||
}
|
||||
|
||||
func changeProjectStatus(ctx *context.APIContext, isClosed bool) {
|
||||
project, err := project_model.GetProjectForRepoByID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
if project_model.IsErrProjectNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
if form.State != nil {
|
||||
isClosed := *form.State == string(api.StateClosed)
|
||||
if isClosed != project.IsClosed {
|
||||
if err := project_model.ChangeProjectStatus(ctx, project, isClosed); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := project_model.ChangeProjectStatus(ctx, project, isClosed); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := project_service.LoadIssueNumbersForProjects(ctx, []*project_model.Project{project}, ctx.Doer); err != nil {
|
||||
if err := project_service.LoadIssueNumbersForProject(ctx, project, ctx.Doer); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
@ -389,14 +329,8 @@ func DeleteProject(ctx *context.APIContext) {
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
// Verify project exists and belongs to this repository
|
||||
project, err := project_model.GetProjectForRepoByID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
if project_model.IsErrProjectNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
project := getRepoProjectByID(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
@ -446,13 +380,8 @@ func ListProjectColumns(ctx *context.APIContext) {
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
project, err := project_model.GetProjectForRepoByID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
if project_model.IsErrProjectNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
project := getRepoProjectByID(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
@ -512,13 +441,8 @@ func CreateProjectColumn(ctx *context.APIContext) {
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
project, err := project_model.GetProjectForRepoByID(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
if project_model.IsErrProjectNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
project := getRepoProjectByID(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
@ -577,24 +501,8 @@ func EditProjectColumn(ctx *context.APIContext) {
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
column, err := project_model.GetColumn(ctx, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
if project_model.IsErrProjectColumnNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Verify column belongs to this repo's project
|
||||
_, err = project_model.GetProjectForRepoByID(ctx, ctx.Repo.Repository.ID, column.ProjectID)
|
||||
if err != nil {
|
||||
if project_model.IsErrProjectNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
column := getRepoProjectColumn(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
@ -651,24 +559,8 @@ func DeleteProjectColumn(ctx *context.APIContext) {
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
column, err := project_model.GetColumn(ctx, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
if project_model.IsErrProjectColumnNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Verify column belongs to this repo's project
|
||||
_, err = project_model.GetProjectForRepoByID(ctx, ctx.Repo.Repository.ID, column.ProjectID)
|
||||
if err != nil {
|
||||
if project_model.IsErrProjectNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
column := getRepoProjectColumn(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
@ -720,31 +612,13 @@ func AddIssueToProjectColumn(ctx *context.APIContext) {
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
column, err := project_model.GetColumn(ctx, ctx.PathParamInt64("id"))
|
||||
if err != nil {
|
||||
if project_model.IsErrProjectColumnNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
column := getRepoProjectColumn(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
// Verify column belongs to this repo's project
|
||||
_, err = project_model.GetProjectForRepoByID(ctx, ctx.Repo.Repository.ID, column.ProjectID)
|
||||
if err != nil {
|
||||
if project_model.IsErrProjectNotExist(err) {
|
||||
ctx.APIErrorNotFound()
|
||||
} else {
|
||||
ctx.APIErrorInternal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
form := web.GetForm(ctx).(*api.AddIssueToProjectColumnOption)
|
||||
|
||||
// Verify issue exists and belongs to this repository
|
||||
issue, err := issues_model.GetIssueByID(ctx, form.IssueID)
|
||||
if err != nil {
|
||||
if issues_model.IsErrIssueNotExist(err) {
|
||||
@ -760,7 +634,6 @@ func AddIssueToProjectColumn(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
// Assign issue to column, creating an audit comment on the issue timeline
|
||||
if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, ctx.Doer, column.ProjectID, column.ID); err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
|
||||
93
templates/swagger/v1_json.tmpl
generated
93
templates/swagger/v1_json.tmpl
generated
@ -14145,50 +14145,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/projects/{id}/close": {
|
||||
"post": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"repository"
|
||||
],
|
||||
"summary": "Close a project",
|
||||
"operationId": "repoCloseProject",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "owner of the repo",
|
||||
"name": "owner",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of the repo",
|
||||
"name": "repo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "id of the project",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/Project"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/responses/notFound"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/projects/{id}/columns": {
|
||||
"get": {
|
||||
"produces": [
|
||||
@ -14300,50 +14256,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/projects/{id}/reopen": {
|
||||
"post": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"repository"
|
||||
],
|
||||
"summary": "Reopen a project",
|
||||
"operationId": "repoReopenProject",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "owner of the repo",
|
||||
"name": "owner",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "name of the repo",
|
||||
"name": "repo",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "id of the project",
|
||||
"name": "id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/responses/Project"
|
||||
},
|
||||
"404": {
|
||||
"$ref": "#/responses/notFound"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/repos/{owner}/{repo}/pulls": {
|
||||
"get": {
|
||||
"produces": [
|
||||
@ -25596,6 +25508,11 @@
|
||||
"type": "string",
|
||||
"x-go-name": "Description"
|
||||
},
|
||||
"state": {
|
||||
"description": "State of the project (open or closed)",
|
||||
"type": "string",
|
||||
"x-go-name": "State"
|
||||
},
|
||||
"title": {
|
||||
"description": "Project title",
|
||||
"type": "string",
|
||||
|
||||
@ -217,8 +217,11 @@ func TestAPIChangeProjectStatus(t *testing.T) {
|
||||
|
||||
token := getUserToken(t, owner.Name, auth_model.AccessTokenScopeWriteIssue)
|
||||
|
||||
req := NewRequestf(t, "POST", "/api/v1/repos/%s/%s/projects/%d/close", owner.Name, repo.Name, project.ID).
|
||||
AddTokenAuth(token)
|
||||
// Close via PATCH with state=closed
|
||||
closed := "closed"
|
||||
req := NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/%s/%s/projects/%d", owner.Name, repo.Name, project.ID), &api.EditProjectOption{
|
||||
State: &closed,
|
||||
}).AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var updatedProject api.Project
|
||||
@ -226,8 +229,11 @@ func TestAPIChangeProjectStatus(t *testing.T) {
|
||||
assert.True(t, updatedProject.IsClosed)
|
||||
assert.NotNil(t, updatedProject.ClosedDate)
|
||||
|
||||
req = NewRequestf(t, "POST", "/api/v1/repos/%s/%s/projects/%d/reopen", owner.Name, repo.Name, project.ID).
|
||||
AddTokenAuth(token)
|
||||
// Reopen via PATCH with state=open
|
||||
open := "open"
|
||||
req = NewRequestWithJSON(t, "PATCH", fmt.Sprintf("/api/v1/repos/%s/%s/projects/%d", owner.Name, repo.Name, project.ID), &api.EditProjectOption{
|
||||
State: &open,
|
||||
}).AddTokenAuth(token)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
DecodeJSON(t, resp, &updatedProject)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user