2023-03-28 19:23:25 +02:00
// Copyright 2016 The Gogs Authors. All rights reserved.
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"net/http"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/services/convert"
)
// GetIssueDependencies list an issue's dependencies
func GetIssueDependencies ( ctx * context . APIContext ) {
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/dependencies issue issueListIssueDependencies
// ---
// summary: List an issue's dependencies, i.e all issues that block this 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: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/IssueList"
2023-09-13 04:37:54 +02:00
// "404":
// "$ref": "#/responses/notFound"
2023-03-28 19:23:25 +02:00
// If this issue's repository does not enable dependencies then there can be no dependencies by default
if ! ctx . Repo . Repository . IsDependenciesEnabled ( ctx ) {
ctx . NotFound ( )
return
}
2023-07-22 22:14:27 +08:00
issue , err := issues_model . GetIssueByIndex ( ctx , ctx . Repo . Repository . ID , ctx . ParamsInt64 ( ":index" ) )
2023-03-28 19:23:25 +02:00
if err != nil {
if issues_model . IsErrIssueNotExist ( err ) {
ctx . NotFound ( "IsErrIssueNotExist" , err )
} else {
ctx . Error ( http . StatusInternalServerError , "GetIssueByIndex" , err )
}
return
}
// 1. We must be able to read this issue
if ! ctx . Repo . Permission . CanReadIssuesOrPulls ( issue . IsPull ) {
ctx . NotFound ( )
return
}
page := ctx . FormInt ( "page" )
if page <= 1 {
page = 1
}
limit := ctx . FormInt ( "limit" )
if limit == 0 {
limit = setting . API . DefaultPagingNum
} else if limit > setting . API . MaxResponseItems {
limit = setting . API . MaxResponseItems
}
canWrite := ctx . Repo . Permission . CanWriteIssuesOrPulls ( issue . IsPull )
blockerIssues := make ( [ ] * issues_model . Issue , 0 , limit )
// 2. Get the issues this issue depends on, i.e. the `<#b>`: `<issue> <- <#b>`
blockersInfo , err := issue . BlockedByDependencies ( ctx , db . ListOptions {
Page : page ,
PageSize : limit ,
} )
if err != nil {
ctx . Error ( http . StatusInternalServerError , "BlockedByDependencies" , err )
return
}
2024-01-13 00:49:02 +08:00
repoPerms := make ( map [ int64 ] access_model . Permission )
repoPerms [ ctx . Repo . Repository . ID ] = ctx . Repo . Permission
2023-03-28 19:23:25 +02:00
for _ , blocker := range blockersInfo {
// Get the permissions for this repository
2024-01-13 00:49:02 +08:00
// If the repo ID exists in the map, return the exist permissions
// else get the permission and add it to the map
var perm access_model . Permission
existPerm , ok := repoPerms [ blocker . RepoID ]
if ok {
perm = existPerm
} else {
var err error
perm , err = access_model . GetUserRepoPermission ( ctx , & blocker . Repository , ctx . Doer )
if err != nil {
ctx . ServerError ( "GetUserRepoPermission" , err )
return
2023-03-28 19:23:25 +02:00
}
2024-01-13 00:49:02 +08:00
repoPerms [ blocker . RepoID ] = perm
2023-03-28 19:23:25 +02:00
}
// check permission
if ! perm . CanReadIssuesOrPulls ( blocker . Issue . IsPull ) {
if ! canWrite {
hiddenBlocker := & issues_model . DependencyInfo {
Issue : issues_model . Issue {
Title : "HIDDEN" ,
} ,
}
blocker = hiddenBlocker
} else {
confidentialBlocker := & issues_model . DependencyInfo {
Issue : issues_model . Issue {
RepoID : blocker . Issue . RepoID ,
Index : blocker . Index ,
Title : blocker . Title ,
IsClosed : blocker . IsClosed ,
IsPull : blocker . IsPull ,
} ,
Repository : repo_model . Repository {
ID : blocker . Issue . Repo . ID ,
Name : blocker . Issue . Repo . Name ,
OwnerName : blocker . Issue . Repo . OwnerName ,
} ,
}
confidentialBlocker . Issue . Repo = & confidentialBlocker . Repository
blocker = confidentialBlocker
}
}
blockerIssues = append ( blockerIssues , & blocker . Issue )
}
ctx . JSON ( http . StatusOK , convert . ToAPIIssueList ( ctx , blockerIssues ) )
}
// CreateIssueDependency create a new issue dependencies
func CreateIssueDependency ( ctx * context . APIContext ) {
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/dependencies issue issueCreateIssueDependencies
// ---
// summary: Make the issue in the url depend on the issue in the form.
// 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: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/IssueMeta"
// responses:
// "201":
// "$ref": "#/responses/Issue"
// "404":
// description: the issue does not exist
2023-09-22 01:43:29 +02:00
// "423":
// "$ref": "#/responses/repoArchivedError"
2023-03-28 19:23:25 +02:00
// We want to make <:index> depend on <Form>, i.e. <:index> is the target
target := getParamsIssue ( ctx )
if ctx . Written ( ) {
return
}
// and <Form> represents the dependency
form := web . GetForm ( ctx ) . ( * api . IssueMeta )
dependency := getFormIssue ( ctx , form )
if ctx . Written ( ) {
return
}
dependencyPerm := getPermissionForRepo ( ctx , target . Repo )
if ctx . Written ( ) {
return
}
createIssueDependency ( ctx , target , dependency , ctx . Repo . Permission , * dependencyPerm )
if ctx . Written ( ) {
return
}
ctx . JSON ( http . StatusCreated , convert . ToAPIIssue ( ctx , target ) )
}
// RemoveIssueDependency remove an issue dependency
func RemoveIssueDependency ( ctx * context . APIContext ) {
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/dependencies issue issueRemoveIssueDependencies
// ---
// summary: Remove an issue dependency
// 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: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/IssueMeta"
// responses:
// "200":
// "$ref": "#/responses/Issue"
2023-09-13 04:37:54 +02:00
// "404":
// "$ref": "#/responses/notFound"
2023-09-22 01:43:29 +02:00
// "423":
// "$ref": "#/responses/repoArchivedError"
2023-03-28 19:23:25 +02:00
// We want to make <:index> depend on <Form>, i.e. <:index> is the target
target := getParamsIssue ( ctx )
if ctx . Written ( ) {
return
}
// and <Form> represents the dependency
form := web . GetForm ( ctx ) . ( * api . IssueMeta )
dependency := getFormIssue ( ctx , form )
if ctx . Written ( ) {
return
}
dependencyPerm := getPermissionForRepo ( ctx , target . Repo )
if ctx . Written ( ) {
return
}
removeIssueDependency ( ctx , target , dependency , ctx . Repo . Permission , * dependencyPerm )
if ctx . Written ( ) {
return
}
ctx . JSON ( http . StatusCreated , convert . ToAPIIssue ( ctx , target ) )
}
// GetIssueBlocks list issues that are blocked by this issue
func GetIssueBlocks ( ctx * context . APIContext ) {
// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/blocks issue issueListBlocks
// ---
// summary: List issues that are blocked by this 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: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/IssueList"
2023-09-13 04:37:54 +02:00
// "404":
// "$ref": "#/responses/notFound"
2023-03-28 19:23:25 +02:00
// We need to list the issues that DEPEND on this issue not the other way round
// Therefore whether dependencies are enabled or not in this repository is potentially irrelevant.
issue := getParamsIssue ( ctx )
if ctx . Written ( ) {
return
}
if ! ctx . Repo . Permission . CanReadIssuesOrPulls ( issue . IsPull ) {
ctx . NotFound ( )
return
}
page := ctx . FormInt ( "page" )
if page <= 1 {
page = 1
}
limit := ctx . FormInt ( "limit" )
if limit <= 1 {
limit = setting . API . DefaultPagingNum
}
skip := ( page - 1 ) * limit
max := page * limit
deps , err := issue . BlockingDependencies ( ctx )
if err != nil {
ctx . Error ( http . StatusInternalServerError , "BlockingDependencies" , err )
return
}
var issues [ ] * issues_model . Issue
2024-01-13 00:49:02 +08:00
repoPerms := make ( map [ int64 ] access_model . Permission )
repoPerms [ ctx . Repo . Repository . ID ] = ctx . Repo . Permission
2023-03-28 19:23:25 +02:00
for i , depMeta := range deps {
if i < skip || i >= max {
continue
}
// Get the permissions for this repository
2024-01-13 00:49:02 +08:00
// If the repo ID exists in the map, return the exist permissions
// else get the permission and add it to the map
var perm access_model . Permission
existPerm , ok := repoPerms [ depMeta . RepoID ]
if ok {
perm = existPerm
} else {
var err error
perm , err = access_model . GetUserRepoPermission ( ctx , & depMeta . Repository , ctx . Doer )
if err != nil {
ctx . ServerError ( "GetUserRepoPermission" , err )
return
2023-03-28 19:23:25 +02:00
}
2024-01-13 00:49:02 +08:00
repoPerms [ depMeta . RepoID ] = perm
2023-03-28 19:23:25 +02:00
}
if ! perm . CanReadIssuesOrPulls ( depMeta . Issue . IsPull ) {
continue
}
depMeta . Issue . Repo = & depMeta . Repository
issues = append ( issues , & depMeta . Issue )
}
ctx . JSON ( http . StatusOK , convert . ToAPIIssueList ( ctx , issues ) )
}
// CreateIssueBlocking block the issue given in the body by the issue in path
func CreateIssueBlocking ( ctx * context . APIContext ) {
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/blocks issue issueCreateIssueBlocking
// ---
// summary: Block the issue given in the body by the issue in path
// 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: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/IssueMeta"
// responses:
// "201":
// "$ref": "#/responses/Issue"
// "404":
// description: the issue does not exist
dependency := getParamsIssue ( ctx )
if ctx . Written ( ) {
return
}
form := web . GetForm ( ctx ) . ( * api . IssueMeta )
target := getFormIssue ( ctx , form )
if ctx . Written ( ) {
return
}
targetPerm := getPermissionForRepo ( ctx , target . Repo )
if ctx . Written ( ) {
return
}
createIssueDependency ( ctx , target , dependency , * targetPerm , ctx . Repo . Permission )
if ctx . Written ( ) {
return
}
ctx . JSON ( http . StatusCreated , convert . ToAPIIssue ( ctx , dependency ) )
}
// RemoveIssueBlocking unblock the issue given in the body by the issue in path
func RemoveIssueBlocking ( ctx * context . APIContext ) {
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/blocks issue issueRemoveIssueBlocking
// ---
// summary: Unblock the issue given in the body by the issue in path
// 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: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/IssueMeta"
// responses:
// "200":
// "$ref": "#/responses/Issue"
2023-09-13 04:37:54 +02:00
// "404":
// "$ref": "#/responses/notFound"
2023-03-28 19:23:25 +02:00
dependency := getParamsIssue ( ctx )
if ctx . Written ( ) {
return
}
form := web . GetForm ( ctx ) . ( * api . IssueMeta )
target := getFormIssue ( ctx , form )
if ctx . Written ( ) {
return
}
targetPerm := getPermissionForRepo ( ctx , target . Repo )
if ctx . Written ( ) {
return
}
removeIssueDependency ( ctx , target , dependency , * targetPerm , ctx . Repo . Permission )
if ctx . Written ( ) {
return
}
ctx . JSON ( http . StatusCreated , convert . ToAPIIssue ( ctx , dependency ) )
}
func getParamsIssue ( ctx * context . APIContext ) * issues_model . Issue {
2023-07-22 22:14:27 +08:00
issue , err := issues_model . GetIssueByIndex ( ctx , ctx . Repo . Repository . ID , ctx . ParamsInt64 ( ":index" ) )
2023-03-28 19:23:25 +02:00
if err != nil {
if issues_model . IsErrIssueNotExist ( err ) {
ctx . NotFound ( "IsErrIssueNotExist" , err )
} else {
ctx . Error ( http . StatusInternalServerError , "GetIssueByIndex" , err )
}
return nil
}
issue . Repo = ctx . Repo . Repository
return issue
}
func getFormIssue ( ctx * context . APIContext , form * api . IssueMeta ) * issues_model . Issue {
var repo * repo_model . Repository
if form . Owner != ctx . Repo . Repository . OwnerName || form . Name != ctx . Repo . Repository . Name {
if ! setting . Service . AllowCrossRepositoryDependencies {
ctx . JSON ( http . StatusBadRequest , "CrossRepositoryDependencies not enabled" )
return nil
}
var err error
repo , err = repo_model . GetRepositoryByOwnerAndName ( ctx , form . Owner , form . Name )
if err != nil {
if repo_model . IsErrRepoNotExist ( err ) {
ctx . NotFound ( "IsErrRepoNotExist" , err )
} else {
ctx . Error ( http . StatusInternalServerError , "GetRepositoryByOwnerAndName" , err )
}
return nil
}
} else {
repo = ctx . Repo . Repository
}
2023-07-22 22:14:27 +08:00
issue , err := issues_model . GetIssueByIndex ( ctx , repo . ID , form . Index )
2023-03-28 19:23:25 +02:00
if err != nil {
if issues_model . IsErrIssueNotExist ( err ) {
ctx . NotFound ( "IsErrIssueNotExist" , err )
} else {
ctx . Error ( http . StatusInternalServerError , "GetIssueByIndex" , err )
}
return nil
}
issue . Repo = repo
return issue
}
func getPermissionForRepo ( ctx * context . APIContext , repo * repo_model . Repository ) * access_model . Permission {
if repo . ID == ctx . Repo . Repository . ID {
return & ctx . Repo . Permission
}
perm , err := access_model . GetUserRepoPermission ( ctx , repo , ctx . Doer )
if err != nil {
ctx . Error ( http . StatusInternalServerError , "GetUserRepoPermission" , err )
return nil
}
return & perm
}
func createIssueDependency ( ctx * context . APIContext , target , dependency * issues_model . Issue , targetPerm , dependencyPerm access_model . Permission ) {
if target . Repo . IsArchived || ! target . Repo . IsDependenciesEnabled ( ctx ) {
// The target's repository doesn't have dependencies enabled
ctx . NotFound ( )
return
}
if ! targetPerm . CanWriteIssuesOrPulls ( target . IsPull ) {
// We can't write to the target
ctx . NotFound ( )
return
}
if ! dependencyPerm . CanReadIssuesOrPulls ( dependency . IsPull ) {
// We can't read the dependency
ctx . NotFound ( )
return
}
2023-10-11 06:24:07 +02:00
err := issues_model . CreateIssueDependency ( ctx , ctx . Doer , target , dependency )
2023-03-28 19:23:25 +02:00
if err != nil {
ctx . Error ( http . StatusInternalServerError , "CreateIssueDependency" , err )
return
}
}
func removeIssueDependency ( ctx * context . APIContext , target , dependency * issues_model . Issue , targetPerm , dependencyPerm access_model . Permission ) {
if target . Repo . IsArchived || ! target . Repo . IsDependenciesEnabled ( ctx ) {
// The target's repository doesn't have dependencies enabled
ctx . NotFound ( )
return
}
if ! targetPerm . CanWriteIssuesOrPulls ( target . IsPull ) {
// We can't write to the target
ctx . NotFound ( )
return
}
if ! dependencyPerm . CanReadIssuesOrPulls ( dependency . IsPull ) {
// We can't read the dependency
ctx . NotFound ( )
return
}
2023-10-11 06:24:07 +02:00
err := issues_model . RemoveIssueDependency ( ctx , ctx . Doer , target , dependency , issues_model . DependencyTypeBlockedBy )
2023-03-28 19:23:25 +02:00
if err != nil {
ctx . Error ( http . StatusInternalServerError , "CreateIssueDependency" , err )
return
}
}