mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-30 13:16:18 +01:00
9b59af37e7
Fix #27722 Fix #27357 Fix #25837 1. Fix the typo `BlockingByDependenciesNotPermitted`, which causes the `not permitted message` not to show. The correct one is `Blocking` or `BlockedBy` 2. Rewrite the perm check. The perm check uses a very tricky way to avoid duplicate checks for a slice of issues, which is confusing. In fact, it's also the reason causing the bug. It uses `lastRepoID` and `lastPerm` to avoid duplicate checks, but forgets to assign the `lastPerm` at the end of the code block. So I rewrote this to avoid this trick. ![I U1AT{GNFY3 1HZ`6L{(2L](https://github.com/go-gitea/gitea/assets/70063547/79acd02a-a567-4316-ae0d-11c6461becf1) 3. It also reuses the `blocks` slice, which is even more confusing. So I rewrote this too. ![UARFPXRGGZQFB7J$2`R}5_R](https://github.com/go-gitea/gitea/assets/70063547/f21cff0f-d9ac-4ce4-ae4d-adffc98ecd99)
613 lines
16 KiB
Go
613 lines
16 KiB
Go
// 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"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
// 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, 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
|
|
}
|
|
|
|
repoPerms := make(map[int64]access_model.Permission)
|
|
repoPerms[ctx.Repo.Repository.ID] = ctx.Repo.Permission
|
|
for _, blocker := range blockersInfo {
|
|
// Get the permissions for this repository
|
|
// If the repo ID exists in the map, return the exist permissions
|
|
// else get the permission and add it to the map
|
|
var perm access_model.Permission
|
|
existPerm, ok := repoPerms[blocker.RepoID]
|
|
if ok {
|
|
perm = existPerm
|
|
} else {
|
|
var err error
|
|
perm, err = access_model.GetUserRepoPermission(ctx, &blocker.Repository, ctx.Doer)
|
|
if err != nil {
|
|
ctx.ServerError("GetUserRepoPermission", err)
|
|
return
|
|
}
|
|
repoPerms[blocker.RepoID] = perm
|
|
}
|
|
|
|
// 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
|
|
// "423":
|
|
// "$ref": "#/responses/repoArchivedError"
|
|
|
|
// 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"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
// "423":
|
|
// "$ref": "#/responses/repoArchivedError"
|
|
|
|
// 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"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
// We need to list the issues that DEPEND on this issue not the other way round
|
|
// Therefore whether dependencies are enabled or not in this repository is potentially irrelevant.
|
|
|
|
issue := getParamsIssue(ctx)
|
|
if ctx.Written() {
|
|
return
|
|
}
|
|
|
|
if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
|
|
ctx.NotFound()
|
|
return
|
|
}
|
|
|
|
page := ctx.FormInt("page")
|
|
if page <= 1 {
|
|
page = 1
|
|
}
|
|
limit := ctx.FormInt("limit")
|
|
if limit <= 1 {
|
|
limit = setting.API.DefaultPagingNum
|
|
}
|
|
|
|
skip := (page - 1) * limit
|
|
max := page * limit
|
|
|
|
deps, err := issue.BlockingDependencies(ctx)
|
|
if err != nil {
|
|
ctx.Error(http.StatusInternalServerError, "BlockingDependencies", err)
|
|
return
|
|
}
|
|
|
|
var issues []*issues_model.Issue
|
|
|
|
repoPerms := make(map[int64]access_model.Permission)
|
|
repoPerms[ctx.Repo.Repository.ID] = ctx.Repo.Permission
|
|
|
|
for i, depMeta := range deps {
|
|
if i < skip || i >= max {
|
|
continue
|
|
}
|
|
|
|
// Get the permissions for this repository
|
|
// If the repo ID exists in the map, return the exist permissions
|
|
// else get the permission and add it to the map
|
|
var perm access_model.Permission
|
|
existPerm, ok := repoPerms[depMeta.RepoID]
|
|
if ok {
|
|
perm = existPerm
|
|
} else {
|
|
var err error
|
|
perm, err = access_model.GetUserRepoPermission(ctx, &depMeta.Repository, ctx.Doer)
|
|
if err != nil {
|
|
ctx.ServerError("GetUserRepoPermission", err)
|
|
return
|
|
}
|
|
repoPerms[depMeta.RepoID] = perm
|
|
}
|
|
|
|
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"
|
|
// "404":
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
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, 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(ctx, 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, 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, ctx.Doer, target, dependency, issues_model.DependencyTypeBlockedBy)
|
|
if err != nil {
|
|
ctx.Error(http.StatusInternalServerError, "CreateIssueDependency", err)
|
|
return
|
|
}
|
|
}
|