Files
forge/routers/api/v1/repo/issue_assignee.go
zeekayandHanzo Dev aa87442fdd Record upstream authorship centrally in NOTICE instead of in every file header
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the
Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in
NOTICE, alongside LICENSE. The header lint accepts any copyright line above the
SPDX line, so it no longer demands an upstream one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 17:50:35 -07:00

239 lines
6.0 KiB
Go

// Copyright 2026 Hanzo AI, Inc. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
issues_model "github.com/hanzoai/git/models/issues"
access_model "github.com/hanzoai/git/models/perm/access"
repo_model "github.com/hanzoai/git/models/repo"
user_model "github.com/hanzoai/git/models/user"
api "github.com/hanzoai/git/modules/structs"
"github.com/hanzoai/git/modules/web"
"github.com/hanzoai/git/services/context"
"github.com/hanzoai/git/services/convert"
issue_service "github.com/hanzoai/git/services/issue"
)
// AddIssueAssignees add assignees to an issue
func AddIssueAssignees(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/assignees issue issueAddAssignees
// ---
// summary: Add assignees to an issue
// consumes:
// - application/json
// 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: index
// in: path
// description: index of the issue
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// required: true
// schema:
// "$ref": "#/definitions/IssueAssigneesOption"
// responses:
// "201":
// "$ref": "#/responses/Issue"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
opts := web.GetForm(ctx).(*api.IssueAssigneesOption)
updateIssueAssignees(ctx, *opts, true)
}
// DeleteIssueAssignees remove assignees from an issue
func DeleteIssueAssignees(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/assignees issue issueRemoveAssignees
// ---
// summary: Remove assignees from an issue
// consumes:
// - application/json
// 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: index
// in: path
// description: index of the issue
// type: integer
// format: int64
// required: true
// - name: body
// in: body
// required: true
// schema:
// "$ref": "#/definitions/IssueAssigneesOption"
// responses:
// "200":
// "$ref": "#/responses/Issue"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "422":
// "$ref": "#/responses/validationError"
opts := web.GetForm(ctx).(*api.IssueAssigneesOption)
updateIssueAssignees(ctx, *opts, false)
}
// CheckIssueAssignee check if a user can be assigned to an issue
func CheckIssueAssignee(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/assignees/{assignee} issue issueCheckAssignee
// ---
// summary: Check if a user can be assigned to an issue
// 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: index
// in: path
// description: index of the issue
// type: integer
// format: int64
// required: true
// - name: assignee
// in: path
// description: username of the user to check for being an assignee
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "400":
// "$ref": "#/responses/error"
// "404":
// "$ref": "#/responses/notFound"
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
ctx.APIErrorAuto(err)
return
}
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
ctx.APIErrorNotFound()
return
}
if checkAssignableUser(ctx, ctx.PathParam("assignee"), ctx.Repo.Repository) {
ctx.Status(http.StatusNoContent)
}
}
// checkAssignableUser resolves assigneeName and verifies the user can be assigned to issues in repo.
// Returns true only when the user resolves AND is assignable; the caller is responsible for writing the 204.
// On any failure it writes the appropriate API response and returns false.
func checkAssignableUser(ctx *context.APIContext, assigneeName string, repo *repo_model.Repository) bool {
assignee, err := user_model.GetUserByName(ctx, assigneeName)
if err != nil {
ctx.APIErrorAuto(err)
return false
}
canAssign, err := access_model.CanBeAssigned(ctx, assignee, repo)
if err != nil {
ctx.APIErrorAuto(err)
return false
}
if !canAssign {
ctx.APIErrorNotFound()
return false
}
return true
}
func updateIssueAssignees(ctx *context.APIContext, opts api.IssueAssigneesOption, isAdd bool) {
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
if err != nil {
ctx.APIErrorAuto(err)
return
}
if !ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull) {
ctx.Status(http.StatusForbidden)
return
}
if err := issue.LoadAttributes(ctx); err != nil {
ctx.APIErrorInternal(err)
return
}
assigneeIDs, err := user_model.GetUserIDsByNames(ctx, opts.Assignees, false)
if err != nil {
if user_model.IsErrUserNotExist(err) {
ctx.APIError(http.StatusUnprocessableEntity, err.Error())
return
}
ctx.APIErrorAuto(err)
return
}
if isAdd {
err = issue_service.AddAssignees(ctx, issue, ctx.Doer, assigneeIDs)
} else {
err = issue_service.RemoveAssignees(ctx, issue, ctx.Doer, assigneeIDs)
}
if err != nil {
ctx.APIErrorAuto(err)
return
}
issue, err = issues_model.GetIssueByID(ctx, issue.ID)
if err != nil {
ctx.APIErrorInternal(err)
return
}
status := http.StatusOK
if isAdd {
status = http.StatusCreated
}
ctx.JSON(status, convert.ToAPIIssue(ctx, ctx.Doer, issue))
}