feat: kanban tui
runner nix smoke / nix label and flake smoke (push) Failing after 1m12s

This commit is contained in:
2026-09-27 15:42:51 +00:00
parent 4c9611a2d2
commit 335a74010d
28 changed files with 4636 additions and 100 deletions
@@ -799,6 +799,17 @@ func mustEnableWiki(ctx *context.APIContext) {
}
}
func mustEnableRepoProjects(ctx *context.APIContext) {
if unit.TypeProjects.UnitGlobalDisabled() || !ctx.Repo.Permission.CanRead(unit.TypeProjects) {
ctx.APIErrorNotFound()
return
}
projectsUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeProjects)
if !projectsUnit.ProjectsConfig().IsProjectsAllowed(repo_model.ProjectsModeRepo) {
ctx.APIErrorNotFound()
}
}
// FIXME: for consistency, maybe most mustNotBeArchived checks should be replaced with mustEnableEditor
func mustNotBeArchived(ctx *context.APIContext) {
if ctx.Repo.Repository.IsArchived {
@@ -1554,6 +1565,12 @@ func Routes() *web.Router {
m.Get("/issues/search", repo.SearchIssues)
m.Group("/{username}/{reponame}", func() {
m.Group("/projects", func() {
m.Get("", repo.ListProjects)
m.Get("/{id}/columns", repo.ListProjectColumns)
m.Get("/{id}/columns/{column_id}/issues", repo.ListProjectColumnIssues)
m.Post("/{id}/issues/{issue_id}/move", reqToken(), reqRepoWriter(unit.TypeProjects), mustNotBeArchived, bind(api.MoveProjectIssueOption{}), repo.MoveProjectIssue)
}, reqRepoReader(unit.TypeProjects), mustEnableRepoProjects)
m.Group("/issues", func() {
m.Combo("").Get(repo.ListIssues).
Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueOption{}), reqRepoReader(unit.TypeIssues), repo.CreateIssue)
@@ -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)
}
@@ -48,6 +48,8 @@ type swaggerParameterBodies struct {
// in:body
IssueLabelsOption api.IssueLabelsOption
// in:body
MoveProjectIssueOption api.MoveProjectIssueOption
// in:body
CreateKeyOption api.CreateKeyOption
@@ -0,0 +1,20 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package swagger
import api "gitea.dev/modules/structs"
// ProjectList
// swagger:response ProjectList
type swaggerResponseProjectList struct {
// in:body
Body []api.Project `json:"body"`
}
// ProjectColumnList
// swagger:response ProjectColumnList
type swaggerResponseProjectColumnList struct {
// in:body
Body []api.ProjectColumn `json:"body"`
}