mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
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]>
239 lines
6.0 KiB
Go
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))
|
|
}
|