|
|
|
@@ -0,0 +1,314 @@
|
|
|
|
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
|
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
|
|
|
|
|
|
package repo
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"errors"
|
|
|
|
|
"net/http"
|
|
|
|
|
|
|
|
|
|
"gitea.dev/models/db"
|
|
|
|
|
issues_model "gitea.dev/models/issues"
|
|
|
|
|
project_model "gitea.dev/models/project"
|
|
|
|
|
"gitea.dev/modules/optional"
|
|
|
|
|
api "gitea.dev/modules/structs"
|
|
|
|
|
"gitea.dev/modules/util"
|
|
|
|
|
"gitea.dev/modules/web"
|
|
|
|
|
"gitea.dev/routers/api/v1/utils"
|
|
|
|
|
"gitea.dev/services/context"
|
|
|
|
|
"gitea.dev/services/convert"
|
|
|
|
|
project_service "gitea.dev/services/projects"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func getRepoProject(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 getProjectColumn(ctx *context.APIContext, projectID int64) *project_model.Column {
|
|
|
|
|
column, err := project_model.GetColumnByIDAndProjectID(ctx, ctx.PathParamInt64("column_id"), projectID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if project_model.IsErrProjectColumnNotExist(err) {
|
|
|
|
|
ctx.APIErrorNotFound()
|
|
|
|
|
} else {
|
|
|
|
|
ctx.APIErrorInternal(err)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return column
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ListProjects list a repository's projects
|
|
|
|
|
func ListProjects(ctx *context.APIContext) {
|
|
|
|
|
// swagger:operation GET /repos/{owner}/{repo}/projects project repoListProjects
|
|
|
|
|
// ---
|
|
|
|
|
// summary: List a repository's projects
|
|
|
|
|
// produces:
|
|
|
|
|
// - application/json
|
|
|
|
|
// parameters:
|
|
|
|
|
// - name: owner
|
|
|
|
|
// in: path
|
|
|
|
|
// required: true
|
|
|
|
|
// type: string
|
|
|
|
|
// - name: repo
|
|
|
|
|
// in: path
|
|
|
|
|
// required: true
|
|
|
|
|
// type: string
|
|
|
|
|
// - name: state
|
|
|
|
|
// in: query
|
|
|
|
|
// type: string
|
|
|
|
|
// enum: [open, closed, all]
|
|
|
|
|
// default: open
|
|
|
|
|
// - name: page
|
|
|
|
|
// in: query
|
|
|
|
|
// type: integer
|
|
|
|
|
// - name: limit
|
|
|
|
|
// in: query
|
|
|
|
|
// type: integer
|
|
|
|
|
// responses:
|
|
|
|
|
// "200":
|
|
|
|
|
// "$ref": "#/responses/ProjectList"
|
|
|
|
|
// "422":
|
|
|
|
|
// "$ref": "#/responses/validationError"
|
|
|
|
|
// "404":
|
|
|
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
|
// "500":
|
|
|
|
|
// "$ref": "#/responses/internalServerError"
|
|
|
|
|
|
|
|
|
|
state := ctx.FormString("state")
|
|
|
|
|
isClosed := optional.Some(false)
|
|
|
|
|
switch state {
|
|
|
|
|
case "", "open":
|
|
|
|
|
case "closed":
|
|
|
|
|
isClosed = optional.Some(true)
|
|
|
|
|
case "all":
|
|
|
|
|
isClosed = optional.None[bool]()
|
|
|
|
|
default:
|
|
|
|
|
ctx.APIError(http.StatusUnprocessableEntity, "state must be one of open, closed, or all")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
listOptions := utils.GetListOptions(ctx)
|
|
|
|
|
projects, count, err := db.FindAndCount[project_model.Project](ctx, project_model.SearchOptions{
|
|
|
|
|
ListOptions: listOptions,
|
|
|
|
|
RepoID: ctx.Repo.Repository.ID,
|
|
|
|
|
IsClosed: isClosed,
|
|
|
|
|
OrderBy: db.SearchOrderByNewest,
|
|
|
|
|
Type: project_model.TypeRepository,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
ctx.APIErrorInternal(err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
ctx.SetLinkHeader(count, listOptions.PageSize)
|
|
|
|
|
ctx.SetTotalCountHeader(count)
|
|
|
|
|
ctx.JSON(http.StatusOK, convert.ToAPIProjectList(projects))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ListProjectColumns list a repository project's columns
|
|
|
|
|
func ListProjectColumns(ctx *context.APIContext) {
|
|
|
|
|
// swagger:operation GET /repos/{owner}/{repo}/projects/{id}/columns project repoListProjectColumns
|
|
|
|
|
// ---
|
|
|
|
|
// summary: List a repository project's columns
|
|
|
|
|
// produces:
|
|
|
|
|
// - application/json
|
|
|
|
|
// parameters:
|
|
|
|
|
// - name: owner
|
|
|
|
|
// in: path
|
|
|
|
|
// required: true
|
|
|
|
|
// type: string
|
|
|
|
|
// - name: repo
|
|
|
|
|
// in: path
|
|
|
|
|
// required: true
|
|
|
|
|
// type: string
|
|
|
|
|
// - name: id
|
|
|
|
|
// in: path
|
|
|
|
|
// required: true
|
|
|
|
|
// type: integer
|
|
|
|
|
// format: int64
|
|
|
|
|
// responses:
|
|
|
|
|
// "200":
|
|
|
|
|
// "$ref": "#/responses/ProjectColumnList"
|
|
|
|
|
// "404":
|
|
|
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
|
// "500":
|
|
|
|
|
// "$ref": "#/responses/internalServerError"
|
|
|
|
|
|
|
|
|
|
project := getRepoProject(ctx)
|
|
|
|
|
if project == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
columns, err := project.GetColumns(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
ctx.APIErrorInternal(err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
ctx.SetTotalCountHeader(int64(len(columns)))
|
|
|
|
|
ctx.JSON(http.StatusOK, convert.ToAPIProjectColumnList(columns))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ListProjectColumnIssues list issues in a repository project column
|
|
|
|
|
func ListProjectColumnIssues(ctx *context.APIContext) {
|
|
|
|
|
// swagger:operation GET /repos/{owner}/{repo}/projects/{id}/columns/{column_id}/issues project repoListProjectColumnIssues
|
|
|
|
|
// ---
|
|
|
|
|
// summary: List issues in a repository project column
|
|
|
|
|
// produces:
|
|
|
|
|
// - application/json
|
|
|
|
|
// parameters:
|
|
|
|
|
// - name: owner
|
|
|
|
|
// in: path
|
|
|
|
|
// required: true
|
|
|
|
|
// type: string
|
|
|
|
|
// - name: repo
|
|
|
|
|
// in: path
|
|
|
|
|
// required: true
|
|
|
|
|
// type: string
|
|
|
|
|
// - name: id
|
|
|
|
|
// in: path
|
|
|
|
|
// required: true
|
|
|
|
|
// type: integer
|
|
|
|
|
// format: int64
|
|
|
|
|
// - name: column_id
|
|
|
|
|
// in: path
|
|
|
|
|
// required: true
|
|
|
|
|
// type: integer
|
|
|
|
|
// format: int64
|
|
|
|
|
// - name: page
|
|
|
|
|
// in: query
|
|
|
|
|
// type: integer
|
|
|
|
|
// - name: limit
|
|
|
|
|
// in: query
|
|
|
|
|
// type: integer
|
|
|
|
|
// responses:
|
|
|
|
|
// "200":
|
|
|
|
|
// "$ref": "#/responses/IssueList"
|
|
|
|
|
// "404":
|
|
|
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
|
// "500":
|
|
|
|
|
// "$ref": "#/responses/internalServerError"
|
|
|
|
|
|
|
|
|
|
project := getRepoProject(ctx)
|
|
|
|
|
if project == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
column := getProjectColumn(ctx, project.ID)
|
|
|
|
|
if column == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
listOptions := utils.GetListOptions(ctx)
|
|
|
|
|
issueOptions := &issues_model.IssuesOptions{
|
|
|
|
|
Paginator: &listOptions,
|
|
|
|
|
RepoIDs: []int64{ctx.Repo.Repository.ID},
|
|
|
|
|
ProjectIDs: []int64{project.ID},
|
|
|
|
|
ProjectColumnID: column.ID,
|
|
|
|
|
ProjectColumnOrUnassigned: column.Default,
|
|
|
|
|
SortType: "project-column-sorting",
|
|
|
|
|
Doer: ctx.Doer,
|
|
|
|
|
}
|
|
|
|
|
total, err := issues_model.CountIssues(ctx, issueOptions)
|
|
|
|
|
if err != nil {
|
|
|
|
|
ctx.APIErrorInternal(err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
issues, err := issues_model.Issues(ctx, issueOptions)
|
|
|
|
|
if err != nil {
|
|
|
|
|
ctx.APIErrorInternal(err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
ctx.SetLinkHeader(total, listOptions.PageSize)
|
|
|
|
|
ctx.SetTotalCountHeader(total)
|
|
|
|
|
ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, issues))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MoveProjectIssue moves an issue to another repository project column
|
|
|
|
|
func MoveProjectIssue(ctx *context.APIContext) {
|
|
|
|
|
// swagger:operation POST /repos/{owner}/{repo}/projects/{id}/issues/{issue_id}/move project repoMoveProjectIssue
|
|
|
|
|
// ---
|
|
|
|
|
// summary: Move an issue to another repository project column
|
|
|
|
|
// consumes:
|
|
|
|
|
// - application/json
|
|
|
|
|
// parameters:
|
|
|
|
|
// - name: owner
|
|
|
|
|
// in: path
|
|
|
|
|
// required: true
|
|
|
|
|
// type: string
|
|
|
|
|
// - name: repo
|
|
|
|
|
// in: path
|
|
|
|
|
// required: true
|
|
|
|
|
// type: string
|
|
|
|
|
// - name: id
|
|
|
|
|
// in: path
|
|
|
|
|
// required: true
|
|
|
|
|
// type: integer
|
|
|
|
|
// format: int64
|
|
|
|
|
// - name: issue_id
|
|
|
|
|
// in: path
|
|
|
|
|
// required: true
|
|
|
|
|
// type: integer
|
|
|
|
|
// format: int64
|
|
|
|
|
// - name: body
|
|
|
|
|
// in: body
|
|
|
|
|
// required: true
|
|
|
|
|
// schema:
|
|
|
|
|
// "$ref": "#/definitions/MoveProjectIssueOption"
|
|
|
|
|
// responses:
|
|
|
|
|
// "204":
|
|
|
|
|
// description: issue moved
|
|
|
|
|
// "403":
|
|
|
|
|
// "$ref": "#/responses/forbidden"
|
|
|
|
|
// "423":
|
|
|
|
|
// "$ref": "#/responses/locked"
|
|
|
|
|
// "404":
|
|
|
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
|
// "422":
|
|
|
|
|
// "$ref": "#/responses/validationError"
|
|
|
|
|
// "500":
|
|
|
|
|
// "$ref": "#/responses/internalServerError"
|
|
|
|
|
|
|
|
|
|
project := getRepoProject(ctx)
|
|
|
|
|
if project == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if project.IsClosed {
|
|
|
|
|
ctx.APIError(http.StatusForbidden, "project is closed")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
form := web.GetForm(ctx).(*api.MoveProjectIssueOption)
|
|
|
|
|
column, err := project_model.GetColumnByIDAndProjectID(ctx, form.ColumnID, project.ID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if project_model.IsErrProjectColumnNotExist(err) {
|
|
|
|
|
ctx.APIErrorNotFound()
|
|
|
|
|
} else {
|
|
|
|
|
ctx.APIErrorInternal(err)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
issueID := ctx.PathParamInt64("issue_id")
|
|
|
|
|
if _, err := issues_model.GetIssueByRepoID(ctx, ctx.Repo.Repository.ID, issueID); err != nil {
|
|
|
|
|
if issues_model.IsErrIssueNotExist(err) {
|
|
|
|
|
ctx.APIErrorNotFound()
|
|
|
|
|
} else {
|
|
|
|
|
ctx.APIErrorInternal(err)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err := project_service.MoveIssueOnProjectColumn(ctx, ctx.Doer, column, issueID, form.Sorting); err != nil {
|
|
|
|
|
if errors.Is(err, util.ErrNotExist) {
|
|
|
|
|
ctx.APIErrorNotFound()
|
|
|
|
|
} else if errors.Is(err, util.ErrInvalidArgument) {
|
|
|
|
|
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
|
|
|
|
|
} else {
|
|
|
|
|
ctx.APIErrorInternal(err)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
ctx.Status(http.StatusNoContent)
|
|
|
|
|
}
|