Add API to manage issue dependencies (#17935)
Adds API endpoints to manage issue/PR dependencies * `GET /repos/{owner}/{repo}/issues/{index}/blocks` List issues that are blocked by this issue * `POST /repos/{owner}/{repo}/issues/{index}/blocks` Block the issue given in the body by the issue in path * `DELETE /repos/{owner}/{repo}/issues/{index}/blocks` Unblock the issue given in the body by the issue in path * `GET /repos/{owner}/{repo}/issues/{index}/dependencies` List an issue's dependencies * `POST /repos/{owner}/{repo}/issues/{index}/dependencies` Create a new issue dependencies * `DELETE /repos/{owner}/{repo}/issues/{index}/dependencies` Remove an issue dependency Closes https://github.com/go-gitea/gitea/issues/15393 Closes #22115 Co-authored-by: Andrew Thornton <art27@cantab.net>
This commit is contained in:
parent
85e8c837b8
commit
3cab9c6b0c
|
@ -134,7 +134,7 @@ func CreateIssueDependency(user *user_model.User, issue, dep *Issue) error {
|
||||||
}
|
}
|
||||||
defer committer.Close()
|
defer committer.Close()
|
||||||
|
|
||||||
// Check if it aleready exists
|
// Check if it already exists
|
||||||
exists, err := issueDepExists(ctx, issue.ID, dep.ID)
|
exists, err := issueDepExists(ctx, issue.ID, dep.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -189,7 +189,7 @@ func (issue *Issue) IsOverdue() bool {
|
||||||
|
|
||||||
// LoadRepo loads issue's repository
|
// LoadRepo loads issue's repository
|
||||||
func (issue *Issue) LoadRepo(ctx context.Context) (err error) {
|
func (issue *Issue) LoadRepo(ctx context.Context) (err error) {
|
||||||
if issue.Repo == nil {
|
if issue.Repo == nil && issue.RepoID != 0 {
|
||||||
issue.Repo, err = repo_model.GetRepositoryByID(ctx, issue.RepoID)
|
issue.Repo, err = repo_model.GetRepositoryByID(ctx, issue.RepoID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("getRepositoryByID [%d]: %w", issue.RepoID, err)
|
return fmt.Errorf("getRepositoryByID [%d]: %w", issue.RepoID, err)
|
||||||
|
@ -223,7 +223,7 @@ func (issue *Issue) GetPullRequest() (pr *PullRequest, err error) {
|
||||||
|
|
||||||
// LoadLabels loads labels
|
// LoadLabels loads labels
|
||||||
func (issue *Issue) LoadLabels(ctx context.Context) (err error) {
|
func (issue *Issue) LoadLabels(ctx context.Context) (err error) {
|
||||||
if issue.Labels == nil {
|
if issue.Labels == nil && issue.ID != 0 {
|
||||||
issue.Labels, err = GetLabelsByIssueID(ctx, issue.ID)
|
issue.Labels, err = GetLabelsByIssueID(ctx, issue.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("getLabelsByIssueID [%d]: %w", issue.ID, err)
|
return fmt.Errorf("getLabelsByIssueID [%d]: %w", issue.ID, err)
|
||||||
|
@ -234,7 +234,7 @@ func (issue *Issue) LoadLabels(ctx context.Context) (err error) {
|
||||||
|
|
||||||
// LoadPoster loads poster
|
// LoadPoster loads poster
|
||||||
func (issue *Issue) LoadPoster(ctx context.Context) (err error) {
|
func (issue *Issue) LoadPoster(ctx context.Context) (err error) {
|
||||||
if issue.Poster == nil {
|
if issue.Poster == nil && issue.PosterID != 0 {
|
||||||
issue.Poster, err = user_model.GetPossibleUserByID(ctx, issue.PosterID)
|
issue.Poster, err = user_model.GetPossibleUserByID(ctx, issue.PosterID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
issue.PosterID = -1
|
issue.PosterID = -1
|
||||||
|
@ -252,7 +252,7 @@ func (issue *Issue) LoadPoster(ctx context.Context) (err error) {
|
||||||
// LoadPullRequest loads pull request info
|
// LoadPullRequest loads pull request info
|
||||||
func (issue *Issue) LoadPullRequest(ctx context.Context) (err error) {
|
func (issue *Issue) LoadPullRequest(ctx context.Context) (err error) {
|
||||||
if issue.IsPull {
|
if issue.IsPull {
|
||||||
if issue.PullRequest == nil {
|
if issue.PullRequest == nil && issue.ID != 0 {
|
||||||
issue.PullRequest, err = GetPullRequestByIssueID(ctx, issue.ID)
|
issue.PullRequest, err = GetPullRequestByIssueID(ctx, issue.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if IsErrPullRequestNotExist(err) {
|
if IsErrPullRequestNotExist(err) {
|
||||||
|
@ -261,7 +261,9 @@ func (issue *Issue) LoadPullRequest(ctx context.Context) (err error) {
|
||||||
return fmt.Errorf("getPullRequestByIssueID [%d]: %w", issue.ID, err)
|
return fmt.Errorf("getPullRequestByIssueID [%d]: %w", issue.ID, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
issue.PullRequest.Issue = issue
|
if issue.PullRequest != nil {
|
||||||
|
issue.PullRequest.Issue = issue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -2128,15 +2130,18 @@ func (issue *Issue) GetParticipantIDsByIssue(ctx context.Context) ([]int64, erro
|
||||||
}
|
}
|
||||||
|
|
||||||
// BlockedByDependencies finds all Dependencies an issue is blocked by
|
// BlockedByDependencies finds all Dependencies an issue is blocked by
|
||||||
func (issue *Issue) BlockedByDependencies(ctx context.Context) (issueDeps []*DependencyInfo, err error) {
|
func (issue *Issue) BlockedByDependencies(ctx context.Context, opts db.ListOptions) (issueDeps []*DependencyInfo, err error) {
|
||||||
err = db.GetEngine(ctx).
|
sess := db.GetEngine(ctx).
|
||||||
Table("issue").
|
Table("issue").
|
||||||
Join("INNER", "repository", "repository.id = issue.repo_id").
|
Join("INNER", "repository", "repository.id = issue.repo_id").
|
||||||
Join("INNER", "issue_dependency", "issue_dependency.dependency_id = issue.id").
|
Join("INNER", "issue_dependency", "issue_dependency.dependency_id = issue.id").
|
||||||
Where("issue_id = ?", issue.ID).
|
Where("issue_id = ?", issue.ID).
|
||||||
// sort by repo id then created date, with the issues of the same repo at the beginning of the list
|
// sort by repo id then created date, with the issues of the same repo at the beginning of the list
|
||||||
OrderBy("CASE WHEN issue.repo_id = ? THEN 0 ELSE issue.repo_id END, issue.created_unix DESC", issue.RepoID).
|
OrderBy("CASE WHEN issue.repo_id = ? THEN 0 ELSE issue.repo_id END, issue.created_unix DESC", issue.RepoID)
|
||||||
Find(&issueDeps)
|
if opts.Page != 0 {
|
||||||
|
sess = db.SetSessionPagination(sess, &opts)
|
||||||
|
}
|
||||||
|
err = sess.Find(&issueDeps)
|
||||||
|
|
||||||
for _, depInfo := range issueDeps {
|
for _, depInfo := range issueDeps {
|
||||||
depInfo.Issue.Repo = &depInfo.Repository
|
depInfo.Issue.Repo = &depInfo.Repository
|
||||||
|
|
|
@ -211,3 +211,11 @@ func (it IssueTemplate) Type() IssueTemplateType {
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IssueMeta basic issue information
|
||||||
|
// swagger:model
|
||||||
|
type IssueMeta struct {
|
||||||
|
Index int64 `json:"index"`
|
||||||
|
Owner string `json:"owner"`
|
||||||
|
Name string `json:"repo"`
|
||||||
|
}
|
||||||
|
|
|
@ -1489,6 +1489,9 @@ issues.due_date_invalid = "The due date is invalid or out of range. Please use t
|
||||||
issues.dependency.title = Dependencies
|
issues.dependency.title = Dependencies
|
||||||
issues.dependency.issue_no_dependencies = No dependencies set.
|
issues.dependency.issue_no_dependencies = No dependencies set.
|
||||||
issues.dependency.pr_no_dependencies = No dependencies set.
|
issues.dependency.pr_no_dependencies = No dependencies set.
|
||||||
|
issues.dependency.no_permission_1 = "You do not have permission to read %d dependency"
|
||||||
|
issues.dependency.no_permission_n = "You do not have permission to read %d dependencies"
|
||||||
|
issues.dependency.no_permission.can_remove = "You do not have permission to read this dependency but can remove this dependency"
|
||||||
issues.dependency.add = Add dependency…
|
issues.dependency.add = Add dependency…
|
||||||
issues.dependency.cancel = Cancel
|
issues.dependency.cancel = Cancel
|
||||||
issues.dependency.remove = Remove
|
issues.dependency.remove = Remove
|
||||||
|
|
|
@ -1026,6 +1026,14 @@ func Routes(ctx gocontext.Context) *web.Route {
|
||||||
Patch(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueAttachment).
|
Patch(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, bind(api.EditAttachmentOptions{}), repo.EditIssueAttachment).
|
||||||
Delete(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, repo.DeleteIssueAttachment)
|
Delete(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, repo.DeleteIssueAttachment)
|
||||||
}, mustEnableAttachments)
|
}, mustEnableAttachments)
|
||||||
|
m.Combo("/dependencies").
|
||||||
|
Get(repo.GetIssueDependencies).
|
||||||
|
Post(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, bind(api.IssueMeta{}), repo.CreateIssueDependency).
|
||||||
|
Delete(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, bind(api.IssueMeta{}), repo.RemoveIssueDependency)
|
||||||
|
m.Combo("/blocks").
|
||||||
|
Get(repo.GetIssueBlocks).
|
||||||
|
Post(reqToken(auth_model.AccessTokenScopeRepo), bind(api.IssueMeta{}), repo.CreateIssueBlocking).
|
||||||
|
Delete(reqToken(auth_model.AccessTokenScopeRepo), bind(api.IssueMeta{}), repo.RemoveIssueBlocking)
|
||||||
})
|
})
|
||||||
}, mustEnableIssuesOrPulls)
|
}, mustEnableIssuesOrPulls)
|
||||||
m.Group("/labels", func() {
|
m.Group("/labels", func() {
|
||||||
|
|
598
routers/api/v1/repo/issue_dependency.go
Normal file
598
routers/api/v1/repo/issue_dependency.go
Normal file
|
@ -0,0 +1,598 @@
|
||||||
|
// 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"
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastRepoID int64
|
||||||
|
var lastPerm access_model.Permission
|
||||||
|
for _, blocker := range blockersInfo {
|
||||||
|
// Get the permissions for this repository
|
||||||
|
perm := lastPerm
|
||||||
|
if lastRepoID != blocker.Repository.ID {
|
||||||
|
if blocker.Repository.ID == ctx.Repo.Repository.ID {
|
||||||
|
perm = ctx.Repo.Permission
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
perm, err = access_model.GetUserRepoPermission(ctx, &blocker.Repository, ctx.Doer)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetUserRepoPermission", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastRepoID = blocker.Repository.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// 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"
|
||||||
|
|
||||||
|
// 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"
|
||||||
|
|
||||||
|
// 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 lastRepoID int64
|
||||||
|
var lastPerm access_model.Permission
|
||||||
|
|
||||||
|
var issues []*issues_model.Issue
|
||||||
|
for i, depMeta := range deps {
|
||||||
|
if i < skip || i >= max {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the permissions for this repository
|
||||||
|
perm := lastPerm
|
||||||
|
if lastRepoID != depMeta.Repository.ID {
|
||||||
|
if depMeta.Repository.ID == ctx.Repo.Repository.ID {
|
||||||
|
perm = ctx.Repo.Permission
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
perm, err = access_model.GetUserRepoPermission(ctx, &depMeta.Repository, ctx.Doer)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetUserRepoPermission", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastRepoID = depMeta.Repository.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
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 {
|
||||||
|
issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
issue, err := issues_model.GetIssueByIndex(repo.ID, form.Index)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
err := issues_model.CreateIssueDependency(ctx.Doer, target, dependency)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
err := issues_model.RemoveIssueDependency(ctx.Doer, target, dependency, issues_model.DependencyTypeBlockedBy)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "CreateIssueDependency", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
|
@ -41,6 +41,8 @@ type swaggerParameterBodies struct {
|
||||||
CreateIssueCommentOption api.CreateIssueCommentOption
|
CreateIssueCommentOption api.CreateIssueCommentOption
|
||||||
// in:body
|
// in:body
|
||||||
EditIssueCommentOption api.EditIssueCommentOption
|
EditIssueCommentOption api.EditIssueCommentOption
|
||||||
|
// in:body
|
||||||
|
IssueMeta api.IssueMeta
|
||||||
|
|
||||||
// in:body
|
// in:body
|
||||||
IssueLabelsOption api.IssueLabelsOption
|
IssueLabelsOption api.IssueLabelsOption
|
||||||
|
|
|
@ -1812,17 +1812,27 @@ func ViewIssue(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get Dependencies
|
// Get Dependencies
|
||||||
ctx.Data["BlockedByDependencies"], err = issue.BlockedByDependencies(ctx)
|
blockedBy, err := issue.BlockedByDependencies(ctx, db.ListOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("BlockedByDependencies", err)
|
ctx.ServerError("BlockedByDependencies", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Data["BlockingDependencies"], err = issue.BlockingDependencies(ctx)
|
ctx.Data["BlockedByDependencies"], ctx.Data["BlockedByDependenciesNotPermitted"] = checkBlockedByIssues(ctx, blockedBy)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
blocking, err := issue.BlockingDependencies(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("BlockingDependencies", err)
|
ctx.ServerError("BlockingDependencies", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx.Data["BlockingDependencies"], ctx.Data["BlockingByDependenciesNotPermitted"] = checkBlockedByIssues(ctx, blocking)
|
||||||
|
if ctx.Written() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
ctx.Data["Participants"] = participants
|
ctx.Data["Participants"] = participants
|
||||||
ctx.Data["NumParticipants"] = len(participants)
|
ctx.Data["NumParticipants"] = len(participants)
|
||||||
ctx.Data["Issue"] = issue
|
ctx.Data["Issue"] = issue
|
||||||
|
@ -1851,6 +1861,48 @@ func ViewIssue(ctx *context.Context) {
|
||||||
ctx.HTML(http.StatusOK, tplIssueView)
|
ctx.HTML(http.StatusOK, tplIssueView)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkBlockedByIssues(ctx *context.Context, blockers []*issues_model.DependencyInfo) (canRead, notPermitted []*issues_model.DependencyInfo) {
|
||||||
|
var lastRepoID int64
|
||||||
|
var lastPerm access_model.Permission
|
||||||
|
for i, blocker := range blockers {
|
||||||
|
// Get the permissions for this repository
|
||||||
|
perm := lastPerm
|
||||||
|
if lastRepoID != blocker.Repository.ID {
|
||||||
|
if blocker.Repository.ID == ctx.Repo.Repository.ID {
|
||||||
|
perm = ctx.Repo.Permission
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
perm, err = access_model.GetUserRepoPermission(ctx, &blocker.Repository, ctx.Doer)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetUserRepoPermission", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lastRepoID = blocker.Repository.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// check permission
|
||||||
|
if !perm.CanReadIssuesOrPulls(blocker.Issue.IsPull) {
|
||||||
|
blockers[len(notPermitted)], blockers[i] = blocker, blockers[len(notPermitted)]
|
||||||
|
notPermitted = blockers[:len(notPermitted)+1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
blockers = blockers[len(notPermitted):]
|
||||||
|
sortDependencyInfo(blockers)
|
||||||
|
sortDependencyInfo(notPermitted)
|
||||||
|
|
||||||
|
return blockers, notPermitted
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortDependencyInfo(blockers []*issues_model.DependencyInfo) {
|
||||||
|
sort.Slice(blockers, func(i, j int) bool {
|
||||||
|
if blockers[i].RepoID == blockers[j].RepoID {
|
||||||
|
return blockers[i].Issue.CreatedUnix < blockers[j].Issue.CreatedUnix
|
||||||
|
}
|
||||||
|
return blockers[i].RepoID < blockers[j].RepoID
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// GetActionIssue will return the issue which is used in the context.
|
// GetActionIssue will return the issue which is used in the context.
|
||||||
func GetActionIssue(ctx *context.Context) *issues_model.Issue {
|
func GetActionIssue(ctx *context.Context) *issues_model.Issue {
|
||||||
issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
|
issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
issues_model "code.gitea.io/gitea/models/issues"
|
issues_model "code.gitea.io/gitea/models/issues"
|
||||||
|
access_model "code.gitea.io/gitea/models/perm/access"
|
||||||
"code.gitea.io/gitea/modules/context"
|
"code.gitea.io/gitea/modules/context"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
)
|
)
|
||||||
|
@ -44,9 +45,25 @@ func AddDependency(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if both issues are in the same repo if cross repository dependencies is not enabled
|
// Check if both issues are in the same repo if cross repository dependencies is not enabled
|
||||||
if issue.RepoID != dep.RepoID && !setting.Service.AllowCrossRepositoryDependencies {
|
if issue.RepoID != dep.RepoID {
|
||||||
ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_dep_not_same_repo"))
|
if !setting.Service.AllowCrossRepositoryDependencies {
|
||||||
return
|
ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_dep_not_same_repo"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := dep.LoadRepo(ctx); err != nil {
|
||||||
|
ctx.ServerError("loadRepo", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Can ctx.Doer read issues in the dep repo?
|
||||||
|
depRepoPerm, err := access_model.GetUserRepoPermission(ctx, dep.Repo, ctx.Doer)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetUserRepoPermission", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !depRepoPerm.CanReadIssuesOrPulls(dep.IsPull) {
|
||||||
|
// you can't see this dependency
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if issue and dependency is the same
|
// Check if issue and dependency is the same
|
||||||
|
|
|
@ -32,21 +32,15 @@ func ToAPIIssue(ctx context.Context, issue *issues_model.Issue) *api.Issue {
|
||||||
if err := issue.LoadRepo(ctx); err != nil {
|
if err := issue.LoadRepo(ctx); err != nil {
|
||||||
return &api.Issue{}
|
return &api.Issue{}
|
||||||
}
|
}
|
||||||
if err := issue.Repo.LoadOwner(ctx); err != nil {
|
|
||||||
return &api.Issue{}
|
|
||||||
}
|
|
||||||
|
|
||||||
apiIssue := &api.Issue{
|
apiIssue := &api.Issue{
|
||||||
ID: issue.ID,
|
ID: issue.ID,
|
||||||
URL: issue.APIURL(),
|
|
||||||
HTMLURL: issue.HTMLURL(),
|
|
||||||
Index: issue.Index,
|
Index: issue.Index,
|
||||||
Poster: ToUser(ctx, issue.Poster, nil),
|
Poster: ToUser(ctx, issue.Poster, nil),
|
||||||
Title: issue.Title,
|
Title: issue.Title,
|
||||||
Body: issue.Content,
|
Body: issue.Content,
|
||||||
Attachments: ToAttachments(issue.Attachments),
|
Attachments: ToAttachments(issue.Attachments),
|
||||||
Ref: issue.Ref,
|
Ref: issue.Ref,
|
||||||
Labels: ToLabelList(issue.Labels, issue.Repo, issue.Repo.Owner),
|
|
||||||
State: issue.State(),
|
State: issue.State(),
|
||||||
IsLocked: issue.IsLocked,
|
IsLocked: issue.IsLocked,
|
||||||
Comments: issue.NumComments,
|
Comments: issue.NumComments,
|
||||||
|
@ -54,11 +48,19 @@ func ToAPIIssue(ctx context.Context, issue *issues_model.Issue) *api.Issue {
|
||||||
Updated: issue.UpdatedUnix.AsTime(),
|
Updated: issue.UpdatedUnix.AsTime(),
|
||||||
}
|
}
|
||||||
|
|
||||||
apiIssue.Repo = &api.RepositoryMeta{
|
if issue.Repo != nil {
|
||||||
ID: issue.Repo.ID,
|
if err := issue.Repo.LoadOwner(ctx); err != nil {
|
||||||
Name: issue.Repo.Name,
|
return &api.Issue{}
|
||||||
Owner: issue.Repo.OwnerName,
|
}
|
||||||
FullName: issue.Repo.FullName(),
|
apiIssue.URL = issue.APIURL()
|
||||||
|
apiIssue.HTMLURL = issue.HTMLURL()
|
||||||
|
apiIssue.Labels = ToLabelList(issue.Labels, issue.Repo, issue.Repo.Owner)
|
||||||
|
apiIssue.Repo = &api.RepositoryMeta{
|
||||||
|
ID: issue.Repo.ID,
|
||||||
|
Name: issue.Repo.Name,
|
||||||
|
Owner: issue.Repo.OwnerName,
|
||||||
|
FullName: issue.Repo.FullName(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if issue.ClosedUnix != 0 {
|
if issue.ClosedUnix != 0 {
|
||||||
|
@ -85,11 +87,13 @@ func ToAPIIssue(ctx context.Context, issue *issues_model.Issue) *api.Issue {
|
||||||
if err := issue.LoadPullRequest(ctx); err != nil {
|
if err := issue.LoadPullRequest(ctx); err != nil {
|
||||||
return &api.Issue{}
|
return &api.Issue{}
|
||||||
}
|
}
|
||||||
apiIssue.PullRequest = &api.PullRequestMeta{
|
if issue.PullRequest != nil {
|
||||||
HasMerged: issue.PullRequest.HasMerged,
|
apiIssue.PullRequest = &api.PullRequestMeta{
|
||||||
}
|
HasMerged: issue.PullRequest.HasMerged,
|
||||||
if issue.PullRequest.HasMerged {
|
}
|
||||||
apiIssue.PullRequest.Merged = issue.PullRequest.MergedUnix.AsTimePtr()
|
if issue.PullRequest.HasMerged {
|
||||||
|
apiIssue.PullRequest.Merged = issue.PullRequest.MergedUnix.AsTimePtr()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if issue.DeadlineUnix != 0 {
|
if issue.DeadlineUnix != 0 {
|
||||||
|
|
|
@ -420,7 +420,7 @@
|
||||||
<div class="ui divider"></div>
|
<div class="ui divider"></div>
|
||||||
|
|
||||||
<div class="ui depending">
|
<div class="ui depending">
|
||||||
{{if (and (not .BlockedByDependencies) (not .BlockingDependencies))}}
|
{{if (and (not .BlockedByDependencies) (not .BlockedByDependenciesNotPermitted) (not .BlockingDependencies) (not .BlockingDependenciesNotPermitted))}}
|
||||||
<span class="text"><strong>{{.locale.Tr "repo.issues.dependency.title"}}</strong></span>
|
<span class="text"><strong>{{.locale.Tr "repo.issues.dependency.title"}}</strong></span>
|
||||||
<br>
|
<br>
|
||||||
<p>
|
<p>
|
||||||
|
@ -432,7 +432,7 @@
|
||||||
</p>
|
</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{if .BlockingDependencies}}
|
{{if or .BlockingDependencies .BlockingDependenciesNotPermitted}}
|
||||||
<span class="text" data-tooltip-content="{{if .Issue.IsPull}}{{.locale.Tr "repo.issues.dependency.pr_close_blocks"}}{{else}}{{.locale.Tr "repo.issues.dependency.issue_close_blocks"}}{{end}}">
|
<span class="text" data-tooltip-content="{{if .Issue.IsPull}}{{.locale.Tr "repo.issues.dependency.pr_close_blocks"}}{{else}}{{.locale.Tr "repo.issues.dependency.issue_close_blocks"}}{{end}}">
|
||||||
<strong>{{.locale.Tr "repo.issues.dependency.blocks_short"}}</strong>
|
<strong>{{.locale.Tr "repo.issues.dependency.blocks_short"}}</strong>
|
||||||
</span>
|
</span>
|
||||||
|
@ -456,10 +456,15 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{if .BlockingDependenciesNotPermitted}}
|
||||||
|
<div class="item gt-df gt-ac gt-sb">
|
||||||
|
<span>{{$.locale.TrN (len .BlockingDependenciesNotPermitted) "repo.issues.dependency.no_permission_1" "repo.issues.dependency.no_permission_n" (len .BlockingDependenciesNotPermitted)}}</span>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{if .BlockedByDependencies}}
|
{{if or .BlockedByDependencies .BlockedByDependenciesNotPermitted}}
|
||||||
<span class="text" data-tooltip-content="{{if .Issue.IsPull}}{{.locale.Tr "repo.issues.dependency.pr_closing_blockedby"}}{{else}}{{.locale.Tr "repo.issues.dependency.issue_closing_blockedby"}}{{end}}">
|
<span class="text" data-tooltip-content="{{if .Issue.IsPull}}{{.locale.Tr "repo.issues.dependency.pr_closing_blockedby"}}{{else}}{{.locale.Tr "repo.issues.dependency.issue_closing_blockedby"}}{{end}}">
|
||||||
<strong>{{.locale.Tr "repo.issues.dependency.blocked_by_short"}}</strong>
|
<strong>{{.locale.Tr "repo.issues.dependency.blocked_by_short"}}</strong>
|
||||||
</span>
|
</span>
|
||||||
|
@ -483,6 +488,34 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{if $.CanCreateIssueDependencies}}
|
||||||
|
{{range .BlockedByDependenciesNotPermitted}}
|
||||||
|
<div class="item dependency{{if .Issue.IsClosed}} is-closed{{end}} gt-df gt-ac gt-sb">
|
||||||
|
<div class="item-left gt-df gt-jc gt-fc gt-f1">
|
||||||
|
<div>
|
||||||
|
<span data-tooltip-content="{{$.locale.Tr "repo.issues.dependency.no_permission.can_remove"}}">{{svg "octicon-lock" 16}}</span>
|
||||||
|
<span class="title" data-tooltip-content="#{{.Issue.Index}} {{.Issue.Title | RenderEmoji $.Context}}">
|
||||||
|
#{{.Issue.Index}} {{.Issue.Title | RenderEmoji $.Context}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="text small">
|
||||||
|
{{.Repository.OwnerName}}/{{.Repository.Name}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item-right gt-df gt-ac">
|
||||||
|
{{if and $.CanCreateIssueDependencies (not $.Repository.IsArchived)}}
|
||||||
|
<a class="delete-dependency-button ci muted" data-id="{{.Issue.ID}}" data-type="blocking" data-tooltip-content="{{$.locale.Tr "repo.issues.dependency.remove_info"}}">
|
||||||
|
{{svg "octicon-trash" 16}}
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{else if .BlockedByDependenciesNotPermitted}}
|
||||||
|
<div class="item gt-df gt-ac gt-sb">
|
||||||
|
<span>{{$.locale.TrN (len .BlockedByDependenciesNotPermitted) "repo.issues.dependency.no_permission_1" "repo.issues.dependency.no_permission_n" (len .BlockedByDependenciesNotPermitted)}}</span>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
|
|
@ -6256,6 +6256,151 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/repos/{owner}/{repo}/issues/{index}/blocks": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"issue"
|
||||||
|
],
|
||||||
|
"summary": "List issues that are blocked by this issue",
|
||||||
|
"operationId": "issueListBlocks",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "owner of the repo",
|
||||||
|
"name": "owner",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the repo",
|
||||||
|
"name": "repo",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "index of the issue",
|
||||||
|
"name": "index",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "page number of results to return (1-based)",
|
||||||
|
"name": "page",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "page size of results",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/responses/IssueList"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"issue"
|
||||||
|
],
|
||||||
|
"summary": "Block the issue given in the body by the issue in path",
|
||||||
|
"operationId": "issueCreateIssueBlocking",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "owner of the repo",
|
||||||
|
"name": "owner",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the repo",
|
||||||
|
"name": "repo",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "index of the issue",
|
||||||
|
"name": "index",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/IssueMeta"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"$ref": "#/responses/Issue"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "the issue does not exist"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"issue"
|
||||||
|
],
|
||||||
|
"summary": "Unblock the issue given in the body by the issue in path",
|
||||||
|
"operationId": "issueRemoveIssueBlocking",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "owner of the repo",
|
||||||
|
"name": "owner",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the repo",
|
||||||
|
"name": "repo",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "index of the issue",
|
||||||
|
"name": "index",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/IssueMeta"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/responses/Issue"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/repos/{owner}/{repo}/issues/{index}/comments": {
|
"/repos/{owner}/{repo}/issues/{index}/comments": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
|
@ -6538,6 +6683,151 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/repos/{owner}/{repo}/issues/{index}/dependencies": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"issue"
|
||||||
|
],
|
||||||
|
"summary": "List an issue's dependencies, i.e all issues that block this issue.",
|
||||||
|
"operationId": "issueListIssueDependencies",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "owner of the repo",
|
||||||
|
"name": "owner",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the repo",
|
||||||
|
"name": "repo",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "index of the issue",
|
||||||
|
"name": "index",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "page number of results to return (1-based)",
|
||||||
|
"name": "page",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "page size of results",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/responses/IssueList"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"issue"
|
||||||
|
],
|
||||||
|
"summary": "Make the issue in the url depend on the issue in the form.",
|
||||||
|
"operationId": "issueCreateIssueDependencies",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "owner of the repo",
|
||||||
|
"name": "owner",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the repo",
|
||||||
|
"name": "repo",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "index of the issue",
|
||||||
|
"name": "index",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/IssueMeta"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"$ref": "#/responses/Issue"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "the issue does not exist"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"issue"
|
||||||
|
],
|
||||||
|
"summary": "Remove an issue dependency",
|
||||||
|
"operationId": "issueRemoveIssueDependencies",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "owner of the repo",
|
||||||
|
"name": "owner",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the repo",
|
||||||
|
"name": "repo",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "index of the issue",
|
||||||
|
"name": "index",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/IssueMeta"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/responses/Issue"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/repos/{owner}/{repo}/issues/{index}/labels": {
|
"/repos/{owner}/{repo}/issues/{index}/labels": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
|
@ -17932,6 +18222,26 @@
|
||||||
},
|
},
|
||||||
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||||
},
|
},
|
||||||
|
"IssueMeta": {
|
||||||
|
"description": "IssueMeta basic issue information",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"index": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"x-go-name": "Index"
|
||||||
|
},
|
||||||
|
"owner": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "Owner"
|
||||||
|
},
|
||||||
|
"repo": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "Name"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-go-package": "code.gitea.io/gitea/modules/structs"
|
||||||
|
},
|
||||||
"IssueTemplate": {
|
"IssueTemplate": {
|
||||||
"description": "IssueTemplate represents an issue template for a repository",
|
"description": "IssueTemplate represents an issue template for a repository",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
|
Loading…
Reference in a new issue