feat(api): add project APIs - #38691
Conversation
This adds a complete REST API implementation for managing repository
project boards, including projects, columns, and adding issues to columns.
API Endpoints:
- GET /repos/{owner}/{repo}/projects - List projects
- POST /repos/{owner}/{repo}/projects - Create project
- GET /repos/{owner}/{repo}/projects/{id} - Get project
- PATCH /repos/{owner}/{repo}/projects/{id} - Update project
- DELETE /repos/{owner}/{repo}/projects/{id} - Delete project
- GET /repos/{owner}/{repo}/projects/{id}/columns - List columns
- POST /repos/{owner}/{repo}/projects/{id}/columns - Create column
- PATCH /repos/{owner}/{repo}/projects/columns/{id} - Update column
- DELETE /repos/{owner}/{repo}/projects/columns/{id} - Delete column
- POST /repos/{owner}/{repo}/projects/columns/{id}/issues - Add issue
Features:
- Full Swagger/OpenAPI documentation
- Proper permission checks
- Pagination support for list endpoints
- State filtering (open/closed/all)
- Comprehensive error handling
- Token-based authentication with scope validation
- Archive repository protection
New Files:
- modules/structs/project.go: API data structures
- routers/api/v1/repo/project.go: API handlers
- routers/api/v1/swagger/project.go: Swagger responses
- services/convert/project.go: Model converters
- tests/integration/api_repo_project_test.go: Integration tests
Modified Files:
- models/project/issue.go: Added AddOrUpdateIssueToColumn function
- routers/api/v1/api.go: Registered project API routes
- routers/api/v1/swagger/options.go: Added project option types
- templates/swagger/v1_json.tmpl: Regenerated swagger spec
fix(api): remove duplicated permission checks in project handlers
Route middleware reqRepoReader(unit.TypeProjects) wraps the entire
/projects route group, and reqRepoWriter(unit.TypeProjects) is applied
to each mutating route individually in api.go. These middleware run
before any handler fires and already gate access correctly.
The inline CanRead/CanWrite checks at the top of all 10 handlers were
therefore unreachable dead code — removed from ListProjects, GetProject,
CreateProject, EditProject, DeleteProject, ListProjectColumns,
CreateProjectColumn, EditProjectColumn, DeleteProjectColumn, and
AddIssueToProjectColumn.
The now-unused "code.gitea.io/gitea/models/unit" import is also removed.
Addresses review feedback on: go-gitea#36008
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
fix(api): replace AddOrUpdateIssueToColumn with IssueAssignOrRemoveProject
The custom AddOrUpdateIssueToColumn function introduced by this PR was
missing three things that the existing IssueAssignOrRemoveProject provides:
1. db.WithTx transaction wrapper — raw DB updates without a transaction
can leave the database in a partial state on error.
2. CreateComment(CommentTypeProject) — assigning an issue to a project
column via the UI creates a comment on the issue timeline. The API
doing the same action silently was an inconsistency.
3. CanBeAccessedByOwnerRepo ownership check — IssueAssignOrRemoveProject
validates that the issue is accessible within the repo/org context
before mutating state.
AddOrUpdateIssueToColumn is removed entirely. AddIssueToProjectColumn
now delegates to issues_model.IssueAssignOrRemoveProject, which already
has the issue object loaded earlier in the handler.
Addresses review feedback on: go-gitea#36008
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
fix(api): remove unnecessary pagination from ListProjectColumns
Project columns are few in number by design (typically 3-8 per board).
The previous implementation fetched all columns from the DB then sliced
the result in memory — adding complexity and a misleading Link header
without any practical benefit.
ListProjectColumns now returns all columns directly. The page/limit
query parameters and associated swagger docs are removed.
Addresses review feedback on: go-gitea#36008
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
fix(api): regenerate swagger spec after removing ListProjectColumns pagination
Removes the page and limit parameters from the generated swagger spec
for the ListProjectColumns endpoint, matching the handler change that
dropped in-memory pagination.
Co-authored-by: Claude <noreply@anthropic.com>
test(api): remove pagination assertion from TestAPIListProjectColumns
ListProjectColumns no longer supports pagination — it returns all columns
directly. Remove the page/limit test case that expected 2 of 3 columns.
Co-authored-by: Claude <noreply@anthropic.com>
fix(api): implement proper pagination for ListProjectColumns
Per contribution guidelines, list endpoints must support page/limit
query params and set X-Total-Count header.
- Add CountColumns and GetColumnsPaginated to project model (DB-level,
not in-memory slicing)
- ListProjectColumns uses utils.GetListOptions, calls paginated model
functions, and sets X-Total-Count via ctx.SetTotalCountHeader
- Restore page/limit swagger doc params on the endpoint
- Regenerate swagger spec
- Integration test covers: full list with X-Total-Count, page 1 of 2,
page 2 of 2, and 404 for non-existent project
Co-authored-by: Claude <noreply@anthropic.com>
Three issues raised by @lunny in review of go-gitea#36008 are addressed: 1. Duplicate permission checks removed The /projects route group is already wrapped with reqRepoReader and reqRepoWriter in api.go. The inline CanRead/CanWrite checks at the top of all 10 handlers were unreachable dead code. 2. AddOrUpdateIssueToColumn replaced with IssueAssignOrRemoveProject The custom function introduced in go-gitea#36008 was missing a db.WithTx transaction wrapper, the CommentTypeProject audit comment written by the UI, and the CanBeAccessedByOwnerRepo cross-repo ownership guard. AddIssueToProjectColumn now delegates to the existing IssueAssignOrRemoveProject which provides all three. 3. ListProjectColumns pagination implemented correctly Added CountColumns and GetColumnsPaginated (using db.SetSessionPagination) to the project model. The handler uses utils.GetListOptions and sets X-Total-Count via ctx.SetTotalCountHeader per API contribution guidelines. Integration tests cover full list, page 1, page 2, and 404. Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: silverwind <me@silverwind.io>
Signed-off-by: silverwind <me@silverwind.io>
Signed-off-by: silverwind <me@silverwind.io>
Signed-off-by: silverwind <me@silverwind.io>
…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>
- Use ParseIssueFilterStateIsClosed for ListProjects state parsing - Add SortTypeProjectColumnSorting const, replace magic string - Use GetIssueByRepoID and dedupe Add/Remove issue handlers - Migrate Column.Sorting from int8 to int (drops 127-column limit, allows the API to expose a normal int without truncation) - Introduce project_service.UpdateProject with optional.Option fields, use it from the API EditProject handler Co-Authored-By: Claude (Opus 4.7) <noreply@anthropic.com>
- Drop lazy LoadRepo/LoadOwner in convert.ToProject; rely on caller preloading. ListProjects sets Repo from ctx.Repo.Repository on each project; CreateProject does the same on the new project. Avoids N+1 queries for repo-scoped list endpoints. - Strip redundant API struct field comments that just restate the field name; keep the ones that document enum values. - Pre-allocate GetColumnsByIDs result slice with len(columnsIDs). - Fix CountProjectColumns doc comment (was "CountColumns"). Co-Authored-By: Claude (Opus 4.7) <noreply@anthropic.com>
- EditProject: wrap field updates and ChangeProjectStatus in db.WithTx so a status-change failure doesn't leave a partially applied PATCH. - Validate EditProjectOption.State against open/closed; 422 on other values instead of silently treating them as open. - Align missing-issue status to 404 (the URL targets a missing resource); update existing test that was asserting the old 422. - RemoveIssueFromProjectColumn: verify the project_issue row matches the URL column before clearing the issue's project assignment, since IssueAssignOrRemoveProject(projectID=0) detaches the issue from any project regardless of column. Returns 404 if the issue isn't in this column. New test covers the cross-column case. Co-Authored-By: Claude (Opus 4.7) <noreply@anthropic.com>
Tested against the CI image versions (postgres:14, bitnamilegacy/mysql:8.0, mcr.microsoft.com/mssql/server:2019-latest) plus SQLite. Both the original implementation (per-dialect SQL) and a naive `base.ModifyColumn` with `DefaultIsEmpty: false` were tried. Findings: - DefaultIsEmpty: false fails on MSSQL with "Incorrect syntax near the keyword 'DEFAULT'" because MSSQL's ALTER COLUMN does not accept inline DEFAULT (it lives in a separate constraint object). - DefaultIsEmpty: true succeeds on MSSQL (existing default constraint unaffected) and Postgres (DEFAULT constraint is independent of TYPE) but drops the DEFAULT on MySQL because MODIFY COLUMN rewrites all column attributes. Settled on the minimal cross-DB form: base.ModifyColumn with DefaultIsEmpty: true to widen the type, then a MySQL-only follow-up `ALTER ... SET DEFAULT 0` to restore the default that MODIFY COLUMN drops. The new test seeds rows at the int8 boundary (0 and 127), runs the migration, asserts the column type widened, the rows preserved, and that inserting a value > 127 succeeds afterward. Co-Authored-By: Claude (Opus 4.7) <noreply@anthropic.com>
- Use setting.Database.Type.IsSQLite3() / IsMySQL() for dialect checks to match the rest of models/migrations/. - Trim the migration's comment to the load-bearing why (MSSQL rejects inline DEFAULT, MySQL drops it on MODIFY COLUMN), drop the discovery narrative. - Trim test comments and tighten the type-name assertion list to the values dialects actually emit (verified empirically against the CI image versions: MySQL/MSSQL report "INT", Postgres reports "INTEGER"; "INT4" never surfaces, removed). Re-tested against postgres:14, bitnamilegacy/mysql:8.0, mssql:2019-latest, and SQLite — all pass. Co-Authored-By: Claude (Opus 4.7) <noreply@anthropic.com>
Co-Authored-By: Claude (Opus 4.7) <noreply@anthropic.com>
Co-Authored-By: Claude (Opus 4.7) <noreply@anthropic.com>
- Rename response timestamps to created_at / updated_at / closed_at
- Replace is_closed bool with state ("open" / "closed") via api.StateType
- Switch template_type / card_type / type to string enums with input validation
- Embed creator User object on Project and ProjectColumn (batched lookup)
- Add absolute html_url; drop relative url
- Add POST /repos/.../projects/{id}/issues/{issue_id}/move with optional sorting
- Validate column hex color and reject writes to closed projects
- Document issue-only project scope in swagger
- Push project-issue existence check into project_model.IsIssueInColumn
- Add project_service.ErrIssueNotInProject sentinel for the move endpoint
Co-Authored-By: Claude (Opus 4.7) <noreply@anthropic.com>
Per review feedback, the 127-column cap is intentional (maxProjectColumns is 20), so the DB schema is left as-is and no migration is needed. Reverts the Column.Sorting widening to match. Co-Authored-By: Claude (Opus 4.7) <noreply@anthropic.com>
…ture and column move logic
Main renamed the module from code.gitea.io/gitea to gitea.dev, so every import added by this branch needed rewriting. Conflicts in column_test.go, project_test.go, convert/project.go and swagger/options.go were resolved by keeping both sides; the two swagger templates were regenerated from annotations rather than merged, since the branch's copies predate 553 commits of unrelated API changes. Assisted-by: Claude Code:Opus 5
Gitea models projects against a repository, an organization or an individual user, and the board behaves identically for all three. One handler set serves every scope, with the owner derived from the route's assignment, so the scopes cannot drift apart as endpoints are added. The web board handlers gained the same treatment: the column and card handlers were duplicated between repo and org, differing only in how they resolved the project, and now share one implementation. Their inline permission checks were redundant with the route middleware, which is what lunny flagged repeatedly on the earlier attempts. Assisted-by: Claude Code:Opus 5
The spec is a make file target, so a plain "make generate-swagger" left v1_json.tmpl untouched when its timestamp looked current and the trimmed endpoint descriptions never reached it. Splitting the Project doc comment across two lines without a blank line also made go-swagger cut the title mid-sentence, so the paragraph break is back. Assisted-by: Claude Code:Opus 5
Inserting addProjectBoardRoutes above the function put it between registerWebRoutes and its own comment. Assisted-by: Claude Code:Opus 5
|
Also fyi the diff stat on the openapi file on GitHub is deceptive at +6754 / −3890. That stat is from GitHub's hardcoded myers diff algorithm. histogram/patience show a more reasonable +2882 / -18, proving nothing is lost in the API. |
Empty-body operations inherit the spec's global produces, the repeated "note cards are not supported" line said nothing per-operation, and /user/projects has no path params so it cannot 404. Seven handler comments sat behind a blank line, which detaches them from the function, and each only restated its swagger summary. Assisted-by: Claude Code:Opus 5
fmt.Errorf("...: %w", ErrUnprocessableContent) put "unprocessable content"
into the JSON message clients read; util.ErrorWrap keeps errors.Is working
without appending the sentinel.
Also declares the 423 archived-repo response the repo-scope mutations can
already return, and moves /user/projects to the userCurrent* operation ids
that /user/keys and /user/gpg_keys use, freeing userListProjects for the
by-username route.
Assisted-by: Claude Code:Opus 5
IssuesOptions.ProjectColumnID was removed in go-gitea#36784 as meaningless once an issue can belong to several projects, and this branch had reintroduced it as ProjectColumnIDs. The column listing now reads its own column's issue IDs and feeds them through the existing IssueIDs option, so models/issues and the issue indexer are untouched again. models/project/column_list.go goes back to the form it was refactored into, and Issue.projectIDs stays unexported: the service reaches the same data through the exported ProjectColumnMap. Also drops a stale copy of the single-issue move in the web handler now that services/projects owns it, answers a 21st column with 422 rather than 500, and replaces the hand-rolled column permutation check with SliceSortedEqual. Assisted-by: Claude Code:Opus 5
lunny
left a comment
There was a problem hiding this comment.
Nice consolidation - the three-scope sharing holds up. I went looking for cross-scope IDOR and did not find any: projects are filtered by owner/repo, columns by project, and issue attachment still goes through CanBeAccessedByOwnerRepo. Multi-write paths all delegate to transactional model/service helpers, the web dedupe introduces no ctx.Data/template/redirect/i18n regressions, and the generated swagger is in sync.
Verified locally on the PR head: go build ./... clean; go vet clean apart from pre-existing unkeyed-field warnings in tests/integration/repo_test.go; unit tests green in models/project, services/projects, services/convert, routers/web/{org,repo,shared/project}; make generate-swagger produces no diff; make swagger-validate and make lint-swagger pass; golangci-lint --enable=dupl,nolintlint,unused reports 0 issues, so both removed //nolint:dupl directives are genuinely unnecessary.
Five behaviour bugs introduced by the diff look worth fixing before merge, plus two places where the API ends up looser than the web tree. Details inline.
Generated by Codet
closed_at was reported for reopened projects, since changeProjectStatus
stamps ClosedDateUnix in both directions and the guard had been dropped.
A column's membership rule now lives in one place, so listing a default
column and removing an issue from it no longer disagree about the pre-1.22
project_board_id=0 rows. UpdateColumn writes what the caller set, which
makes sorting=0 reachable and stops the response contradicting the row.
An empty title is rejected instead of wiping the name, project lists carry
a unique order so pagination cannot repeat or skip rows, and the html_url
shapes come from the model helpers rather than being respelled.
Two places where the API was looser than the web tree: repo boards ignored
the Projects unit mode, and a public-only token could read a private
organization's boards through /users/{username}/projects, or its own
private repositories' issues through an owner board.
Assisted-by: Claude Code:Opus 5
Adds REST APIs for project boards for repo, org and user scopes, using as much shared code as possible for all 3 scopes.
Incorporates feedback from all previous PRs and did multiple dedupe and review passes in Claude including previously undeduped web code which eliminated an additional ~500 lines. Diff is large mostly because of the swagger/openapi. Code diff is like ~1500 lines.