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
					
				
			
		
					 12 changed files with 1074 additions and 34 deletions
				
			
		| 
						 | 
				
			
			@ -134,7 +134,7 @@ func CreateIssueDependency(user *user_model.User, issue, dep *Issue) error {
 | 
			
		|||
	}
 | 
			
		||||
	defer committer.Close()
 | 
			
		||||
 | 
			
		||||
	// Check if it aleready exists
 | 
			
		||||
	// Check if it already exists
 | 
			
		||||
	exists, err := issueDepExists(ctx, issue.ID, dep.ID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -189,7 +189,7 @@ func (issue *Issue) IsOverdue() bool {
 | 
			
		|||
 | 
			
		||||
// LoadRepo loads issue's repository
 | 
			
		||||
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)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("getRepositoryByID [%d]: %w", issue.RepoID, err)
 | 
			
		||||
| 
						 | 
				
			
			@ -223,7 +223,7 @@ func (issue *Issue) GetPullRequest() (pr *PullRequest, err error) {
 | 
			
		|||
 | 
			
		||||
// LoadLabels loads labels
 | 
			
		||||
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)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			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
 | 
			
		||||
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)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			issue.PosterID = -1
 | 
			
		||||
| 
						 | 
				
			
			@ -252,7 +252,7 @@ func (issue *Issue) LoadPoster(ctx context.Context) (err error) {
 | 
			
		|||
// LoadPullRequest loads pull request info
 | 
			
		||||
func (issue *Issue) LoadPullRequest(ctx context.Context) (err error) {
 | 
			
		||||
	if issue.IsPull {
 | 
			
		||||
		if issue.PullRequest == nil {
 | 
			
		||||
		if issue.PullRequest == nil && issue.ID != 0 {
 | 
			
		||||
			issue.PullRequest, err = GetPullRequestByIssueID(ctx, issue.ID)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				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)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		issue.PullRequest.Issue = issue
 | 
			
		||||
		if issue.PullRequest != nil {
 | 
			
		||||
			issue.PullRequest.Issue = issue
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -2128,15 +2130,18 @@ func (issue *Issue) GetParticipantIDsByIssue(ctx context.Context) ([]int64, erro
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
// BlockedByDependencies finds all Dependencies an issue is blocked by
 | 
			
		||||
func (issue *Issue) BlockedByDependencies(ctx context.Context) (issueDeps []*DependencyInfo, err error) {
 | 
			
		||||
	err = db.GetEngine(ctx).
 | 
			
		||||
func (issue *Issue) BlockedByDependencies(ctx context.Context, opts db.ListOptions) (issueDeps []*DependencyInfo, err error) {
 | 
			
		||||
	sess := db.GetEngine(ctx).
 | 
			
		||||
		Table("issue").
 | 
			
		||||
		Join("INNER", "repository", "repository.id = issue.repo_id").
 | 
			
		||||
		Join("INNER", "issue_dependency", "issue_dependency.dependency_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
 | 
			
		||||
		OrderBy("CASE WHEN issue.repo_id = ? THEN 0 ELSE issue.repo_id END, issue.created_unix DESC", issue.RepoID).
 | 
			
		||||
		Find(&issueDeps)
 | 
			
		||||
		OrderBy("CASE WHEN issue.repo_id = ? THEN 0 ELSE issue.repo_id END, issue.created_unix DESC", issue.RepoID)
 | 
			
		||||
	if opts.Page != 0 {
 | 
			
		||||
		sess = db.SetSessionPagination(sess, &opts)
 | 
			
		||||
	}
 | 
			
		||||
	err = sess.Find(&issueDeps)
 | 
			
		||||
 | 
			
		||||
	for _, depInfo := range issueDeps {
 | 
			
		||||
		depInfo.Issue.Repo = &depInfo.Repository
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -211,3 +211,11 @@ func (it IssueTemplate) Type() IssueTemplateType {
 | 
			
		|||
	}
 | 
			
		||||
	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.issue_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.cancel = Cancel
 | 
			
		||||
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).
 | 
			
		||||
								Delete(reqToken(auth_model.AccessTokenScopeRepo), mustNotBeArchived, repo.DeleteIssueAttachment)
 | 
			
		||||
						}, 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)
 | 
			
		||||
				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
 | 
			
		||||
	// in:body
 | 
			
		||||
	EditIssueCommentOption api.EditIssueCommentOption
 | 
			
		||||
	// in:body
 | 
			
		||||
	IssueMeta api.IssueMeta
 | 
			
		||||
 | 
			
		||||
	// in:body
 | 
			
		||||
	IssueLabelsOption api.IssueLabelsOption
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1812,17 +1812,27 @@ func ViewIssue(ctx *context.Context) {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	// Get Dependencies
 | 
			
		||||
	ctx.Data["BlockedByDependencies"], err = issue.BlockedByDependencies(ctx)
 | 
			
		||||
	blockedBy, err := issue.BlockedByDependencies(ctx, db.ListOptions{})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.ServerError("BlockedByDependencies", err)
 | 
			
		||||
		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 {
 | 
			
		||||
		ctx.ServerError("BlockingDependencies", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.Data["BlockingDependencies"], ctx.Data["BlockingByDependenciesNotPermitted"] = checkBlockedByIssues(ctx, blocking)
 | 
			
		||||
	if ctx.Written() {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx.Data["Participants"] = participants
 | 
			
		||||
	ctx.Data["NumParticipants"] = len(participants)
 | 
			
		||||
	ctx.Data["Issue"] = issue
 | 
			
		||||
| 
						 | 
				
			
			@ -1851,6 +1861,48 @@ func ViewIssue(ctx *context.Context) {
 | 
			
		|||
	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.
 | 
			
		||||
func GetActionIssue(ctx *context.Context) *issues_model.Issue {
 | 
			
		||||
	issue, err := issues_model.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,6 +7,7 @@ import (
 | 
			
		|||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	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/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
 | 
			
		||||
	if issue.RepoID != dep.RepoID && !setting.Service.AllowCrossRepositoryDependencies {
 | 
			
		||||
		ctx.Flash.Error(ctx.Tr("repo.issues.dependency.add_error_dep_not_same_repo"))
 | 
			
		||||
		return
 | 
			
		||||
	if issue.RepoID != dep.RepoID {
 | 
			
		||||
		if !setting.Service.AllowCrossRepositoryDependencies {
 | 
			
		||||
			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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -32,21 +32,15 @@ func ToAPIIssue(ctx context.Context, issue *issues_model.Issue) *api.Issue {
 | 
			
		|||
	if err := issue.LoadRepo(ctx); err != nil {
 | 
			
		||||
		return &api.Issue{}
 | 
			
		||||
	}
 | 
			
		||||
	if err := issue.Repo.LoadOwner(ctx); err != nil {
 | 
			
		||||
		return &api.Issue{}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	apiIssue := &api.Issue{
 | 
			
		||||
		ID:          issue.ID,
 | 
			
		||||
		URL:         issue.APIURL(),
 | 
			
		||||
		HTMLURL:     issue.HTMLURL(),
 | 
			
		||||
		Index:       issue.Index,
 | 
			
		||||
		Poster:      ToUser(ctx, issue.Poster, nil),
 | 
			
		||||
		Title:       issue.Title,
 | 
			
		||||
		Body:        issue.Content,
 | 
			
		||||
		Attachments: ToAttachments(issue.Attachments),
 | 
			
		||||
		Ref:         issue.Ref,
 | 
			
		||||
		Labels:      ToLabelList(issue.Labels, issue.Repo, issue.Repo.Owner),
 | 
			
		||||
		State:       issue.State(),
 | 
			
		||||
		IsLocked:    issue.IsLocked,
 | 
			
		||||
		Comments:    issue.NumComments,
 | 
			
		||||
| 
						 | 
				
			
			@ -54,11 +48,19 @@ func ToAPIIssue(ctx context.Context, issue *issues_model.Issue) *api.Issue {
 | 
			
		|||
		Updated:     issue.UpdatedUnix.AsTime(),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	apiIssue.Repo = &api.RepositoryMeta{
 | 
			
		||||
		ID:       issue.Repo.ID,
 | 
			
		||||
		Name:     issue.Repo.Name,
 | 
			
		||||
		Owner:    issue.Repo.OwnerName,
 | 
			
		||||
		FullName: issue.Repo.FullName(),
 | 
			
		||||
	if issue.Repo != nil {
 | 
			
		||||
		if err := issue.Repo.LoadOwner(ctx); err != nil {
 | 
			
		||||
			return &api.Issue{}
 | 
			
		||||
		}
 | 
			
		||||
		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 {
 | 
			
		||||
| 
						 | 
				
			
			@ -85,11 +87,13 @@ func ToAPIIssue(ctx context.Context, issue *issues_model.Issue) *api.Issue {
 | 
			
		|||
		if err := issue.LoadPullRequest(ctx); err != nil {
 | 
			
		||||
			return &api.Issue{}
 | 
			
		||||
		}
 | 
			
		||||
		apiIssue.PullRequest = &api.PullRequestMeta{
 | 
			
		||||
			HasMerged: issue.PullRequest.HasMerged,
 | 
			
		||||
		}
 | 
			
		||||
		if issue.PullRequest.HasMerged {
 | 
			
		||||
			apiIssue.PullRequest.Merged = issue.PullRequest.MergedUnix.AsTimePtr()
 | 
			
		||||
		if issue.PullRequest != nil {
 | 
			
		||||
			apiIssue.PullRequest = &api.PullRequestMeta{
 | 
			
		||||
				HasMerged: issue.PullRequest.HasMerged,
 | 
			
		||||
			}
 | 
			
		||||
			if issue.PullRequest.HasMerged {
 | 
			
		||||
				apiIssue.PullRequest.Merged = issue.PullRequest.MergedUnix.AsTimePtr()
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if issue.DeadlineUnix != 0 {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -420,7 +420,7 @@
 | 
			
		|||
			<div class="ui divider"></div>
 | 
			
		||||
 | 
			
		||||
			<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>
 | 
			
		||||
					<br>
 | 
			
		||||
					<p>
 | 
			
		||||
| 
						 | 
				
			
			@ -432,7 +432,7 @@
 | 
			
		|||
					</p>
 | 
			
		||||
				{{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}}">
 | 
			
		||||
						<strong>{{.locale.Tr "repo.issues.dependency.blocks_short"}}</strong>
 | 
			
		||||
					</span>
 | 
			
		||||
| 
						 | 
				
			
			@ -456,10 +456,15 @@
 | 
			
		|||
								</div>
 | 
			
		||||
							</div>
 | 
			
		||||
						{{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>
 | 
			
		||||
				{{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}}">
 | 
			
		||||
						<strong>{{.locale.Tr "repo.issues.dependency.blocked_by_short"}}</strong>
 | 
			
		||||
					</span>
 | 
			
		||||
| 
						 | 
				
			
			@ -483,6 +488,34 @@
 | 
			
		|||
								</div>
 | 
			
		||||
							</div>
 | 
			
		||||
						{{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>
 | 
			
		||||
				{{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": {
 | 
			
		||||
      "get": {
 | 
			
		||||
        "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": {
 | 
			
		||||
      "get": {
 | 
			
		||||
        "produces": [
 | 
			
		||||
| 
						 | 
				
			
			@ -17932,6 +18222,26 @@
 | 
			
		|||
      },
 | 
			
		||||
      "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": {
 | 
			
		||||
      "description": "IssueTemplate represents an issue template for a repository",
 | 
			
		||||
      "type": "object",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue