code.gitea.io/gitea@v1.21.7/routers/api/v1/repo/issue_dependency.go (about)

     1  // Copyright 2016 The Gogs Authors. All rights reserved.
     2  // Copyright 2023 The Gitea Authors. All rights reserved.
     3  // SPDX-License-Identifier: MIT
     4  
     5  package repo
     6  
     7  import (
     8  	"net/http"
     9  
    10  	"code.gitea.io/gitea/models/db"
    11  	issues_model "code.gitea.io/gitea/models/issues"
    12  	access_model "code.gitea.io/gitea/models/perm/access"
    13  	repo_model "code.gitea.io/gitea/models/repo"
    14  	"code.gitea.io/gitea/modules/context"
    15  	"code.gitea.io/gitea/modules/setting"
    16  	api "code.gitea.io/gitea/modules/structs"
    17  	"code.gitea.io/gitea/modules/web"
    18  	"code.gitea.io/gitea/services/convert"
    19  )
    20  
    21  // GetIssueDependencies list an issue's dependencies
    22  func GetIssueDependencies(ctx *context.APIContext) {
    23  	// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/dependencies issue issueListIssueDependencies
    24  	// ---
    25  	// summary: List an issue's dependencies, i.e all issues that block this issue.
    26  	// produces:
    27  	// - application/json
    28  	// parameters:
    29  	// - name: owner
    30  	//   in: path
    31  	//   description: owner of the repo
    32  	//   type: string
    33  	//   required: true
    34  	// - name: repo
    35  	//   in: path
    36  	//   description: name of the repo
    37  	//   type: string
    38  	//   required: true
    39  	// - name: index
    40  	//   in: path
    41  	//   description: index of the issue
    42  	//   type: string
    43  	//   required: true
    44  	// - name: page
    45  	//   in: query
    46  	//   description: page number of results to return (1-based)
    47  	//   type: integer
    48  	// - name: limit
    49  	//   in: query
    50  	//   description: page size of results
    51  	//   type: integer
    52  	// responses:
    53  	//   "200":
    54  	//     "$ref": "#/responses/IssueList"
    55  	//   "404":
    56  	//     "$ref": "#/responses/notFound"
    57  
    58  	// If this issue's repository does not enable dependencies then there can be no dependencies by default
    59  	if !ctx.Repo.Repository.IsDependenciesEnabled(ctx) {
    60  		ctx.NotFound()
    61  		return
    62  	}
    63  
    64  	issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
    65  	if err != nil {
    66  		if issues_model.IsErrIssueNotExist(err) {
    67  			ctx.NotFound("IsErrIssueNotExist", err)
    68  		} else {
    69  			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
    70  		}
    71  		return
    72  	}
    73  
    74  	// 1. We must be able to read this issue
    75  	if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
    76  		ctx.NotFound()
    77  		return
    78  	}
    79  
    80  	page := ctx.FormInt("page")
    81  	if page <= 1 {
    82  		page = 1
    83  	}
    84  	limit := ctx.FormInt("limit")
    85  	if limit == 0 {
    86  		limit = setting.API.DefaultPagingNum
    87  	} else if limit > setting.API.MaxResponseItems {
    88  		limit = setting.API.MaxResponseItems
    89  	}
    90  
    91  	canWrite := ctx.Repo.Permission.CanWriteIssuesOrPulls(issue.IsPull)
    92  
    93  	blockerIssues := make([]*issues_model.Issue, 0, limit)
    94  
    95  	// 2. Get the issues this issue depends on, i.e. the `<#b>`: `<issue> <- <#b>`
    96  	blockersInfo, err := issue.BlockedByDependencies(ctx, db.ListOptions{
    97  		Page:     page,
    98  		PageSize: limit,
    99  	})
   100  	if err != nil {
   101  		ctx.Error(http.StatusInternalServerError, "BlockedByDependencies", err)
   102  		return
   103  	}
   104  
   105  	repoPerms := make(map[int64]access_model.Permission)
   106  	repoPerms[ctx.Repo.Repository.ID] = ctx.Repo.Permission
   107  	for _, blocker := range blockersInfo {
   108  		// Get the permissions for this repository
   109  		// If the repo ID exists in the map, return the exist permissions
   110  		// else get the permission and add it to the map
   111  		var perm access_model.Permission
   112  		existPerm, ok := repoPerms[blocker.RepoID]
   113  		if ok {
   114  			perm = existPerm
   115  		} else {
   116  			var err error
   117  			perm, err = access_model.GetUserRepoPermission(ctx, &blocker.Repository, ctx.Doer)
   118  			if err != nil {
   119  				ctx.ServerError("GetUserRepoPermission", err)
   120  				return
   121  			}
   122  			repoPerms[blocker.RepoID] = perm
   123  		}
   124  
   125  		// check permission
   126  		if !perm.CanReadIssuesOrPulls(blocker.Issue.IsPull) {
   127  			if !canWrite {
   128  				hiddenBlocker := &issues_model.DependencyInfo{
   129  					Issue: issues_model.Issue{
   130  						Title: "HIDDEN",
   131  					},
   132  				}
   133  				blocker = hiddenBlocker
   134  			} else {
   135  				confidentialBlocker := &issues_model.DependencyInfo{
   136  					Issue: issues_model.Issue{
   137  						RepoID:   blocker.Issue.RepoID,
   138  						Index:    blocker.Index,
   139  						Title:    blocker.Title,
   140  						IsClosed: blocker.IsClosed,
   141  						IsPull:   blocker.IsPull,
   142  					},
   143  					Repository: repo_model.Repository{
   144  						ID:        blocker.Issue.Repo.ID,
   145  						Name:      blocker.Issue.Repo.Name,
   146  						OwnerName: blocker.Issue.Repo.OwnerName,
   147  					},
   148  				}
   149  				confidentialBlocker.Issue.Repo = &confidentialBlocker.Repository
   150  				blocker = confidentialBlocker
   151  			}
   152  		}
   153  		blockerIssues = append(blockerIssues, &blocker.Issue)
   154  	}
   155  
   156  	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, blockerIssues))
   157  }
   158  
   159  // CreateIssueDependency create a new issue dependencies
   160  func CreateIssueDependency(ctx *context.APIContext) {
   161  	// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/dependencies issue issueCreateIssueDependencies
   162  	// ---
   163  	// summary: Make the issue in the url depend on the issue in the form.
   164  	// produces:
   165  	// - application/json
   166  	// parameters:
   167  	// - name: owner
   168  	//   in: path
   169  	//   description: owner of the repo
   170  	//   type: string
   171  	//   required: true
   172  	// - name: repo
   173  	//   in: path
   174  	//   description: name of the repo
   175  	//   type: string
   176  	//   required: true
   177  	// - name: index
   178  	//   in: path
   179  	//   description: index of the issue
   180  	//   type: string
   181  	//   required: true
   182  	// - name: body
   183  	//   in: body
   184  	//   schema:
   185  	//     "$ref": "#/definitions/IssueMeta"
   186  	// responses:
   187  	//   "201":
   188  	//     "$ref": "#/responses/Issue"
   189  	//   "404":
   190  	//     description: the issue does not exist
   191  
   192  	// We want to make <:index> depend on <Form>, i.e. <:index> is the target
   193  	target := getParamsIssue(ctx)
   194  	if ctx.Written() {
   195  		return
   196  	}
   197  
   198  	// and <Form> represents the dependency
   199  	form := web.GetForm(ctx).(*api.IssueMeta)
   200  	dependency := getFormIssue(ctx, form)
   201  	if ctx.Written() {
   202  		return
   203  	}
   204  
   205  	dependencyPerm := getPermissionForRepo(ctx, target.Repo)
   206  	if ctx.Written() {
   207  		return
   208  	}
   209  
   210  	createIssueDependency(ctx, target, dependency, ctx.Repo.Permission, *dependencyPerm)
   211  	if ctx.Written() {
   212  		return
   213  	}
   214  
   215  	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, target))
   216  }
   217  
   218  // RemoveIssueDependency remove an issue dependency
   219  func RemoveIssueDependency(ctx *context.APIContext) {
   220  	// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/dependencies issue issueRemoveIssueDependencies
   221  	// ---
   222  	// summary: Remove an issue dependency
   223  	// produces:
   224  	// - application/json
   225  	// parameters:
   226  	// - name: owner
   227  	//   in: path
   228  	//   description: owner of the repo
   229  	//   type: string
   230  	//   required: true
   231  	// - name: repo
   232  	//   in: path
   233  	//   description: name of the repo
   234  	//   type: string
   235  	//   required: true
   236  	// - name: index
   237  	//   in: path
   238  	//   description: index of the issue
   239  	//   type: string
   240  	//   required: true
   241  	// - name: body
   242  	//   in: body
   243  	//   schema:
   244  	//     "$ref": "#/definitions/IssueMeta"
   245  	// responses:
   246  	//   "200":
   247  	//     "$ref": "#/responses/Issue"
   248  	//   "404":
   249  	//     "$ref": "#/responses/notFound"
   250  
   251  	// We want to make <:index> depend on <Form>, i.e. <:index> is the target
   252  	target := getParamsIssue(ctx)
   253  	if ctx.Written() {
   254  		return
   255  	}
   256  
   257  	// and <Form> represents the dependency
   258  	form := web.GetForm(ctx).(*api.IssueMeta)
   259  	dependency := getFormIssue(ctx, form)
   260  	if ctx.Written() {
   261  		return
   262  	}
   263  
   264  	dependencyPerm := getPermissionForRepo(ctx, target.Repo)
   265  	if ctx.Written() {
   266  		return
   267  	}
   268  
   269  	removeIssueDependency(ctx, target, dependency, ctx.Repo.Permission, *dependencyPerm)
   270  	if ctx.Written() {
   271  		return
   272  	}
   273  
   274  	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, target))
   275  }
   276  
   277  // GetIssueBlocks list issues that are blocked by this issue
   278  func GetIssueBlocks(ctx *context.APIContext) {
   279  	// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/blocks issue issueListBlocks
   280  	// ---
   281  	// summary: List issues that are blocked by this issue
   282  	// produces:
   283  	// - application/json
   284  	// parameters:
   285  	// - name: owner
   286  	//   in: path
   287  	//   description: owner of the repo
   288  	//   type: string
   289  	//   required: true
   290  	// - name: repo
   291  	//   in: path
   292  	//   description: name of the repo
   293  	//   type: string
   294  	//   required: true
   295  	// - name: index
   296  	//   in: path
   297  	//   description: index of the issue
   298  	//   type: string
   299  	//   required: true
   300  	// - name: page
   301  	//   in: query
   302  	//   description: page number of results to return (1-based)
   303  	//   type: integer
   304  	// - name: limit
   305  	//   in: query
   306  	//   description: page size of results
   307  	//   type: integer
   308  	// responses:
   309  	//   "200":
   310  	//     "$ref": "#/responses/IssueList"
   311  	//   "404":
   312  	//     "$ref": "#/responses/notFound"
   313  
   314  	// We need to list the issues that DEPEND on this issue not the other way round
   315  	// Therefore whether dependencies are enabled or not in this repository is potentially irrelevant.
   316  
   317  	issue := getParamsIssue(ctx)
   318  	if ctx.Written() {
   319  		return
   320  	}
   321  
   322  	if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
   323  		ctx.NotFound()
   324  		return
   325  	}
   326  
   327  	page := ctx.FormInt("page")
   328  	if page <= 1 {
   329  		page = 1
   330  	}
   331  	limit := ctx.FormInt("limit")
   332  	if limit <= 1 {
   333  		limit = setting.API.DefaultPagingNum
   334  	}
   335  
   336  	skip := (page - 1) * limit
   337  	max := page * limit
   338  
   339  	deps, err := issue.BlockingDependencies(ctx)
   340  	if err != nil {
   341  		ctx.Error(http.StatusInternalServerError, "BlockingDependencies", err)
   342  		return
   343  	}
   344  
   345  	var issues []*issues_model.Issue
   346  
   347  	repoPerms := make(map[int64]access_model.Permission)
   348  	repoPerms[ctx.Repo.Repository.ID] = ctx.Repo.Permission
   349  
   350  	for i, depMeta := range deps {
   351  		if i < skip || i >= max {
   352  			continue
   353  		}
   354  
   355  		// Get the permissions for this repository
   356  		// If the repo ID exists in the map, return the exist permissions
   357  		// else get the permission and add it to the map
   358  		var perm access_model.Permission
   359  		existPerm, ok := repoPerms[depMeta.RepoID]
   360  		if ok {
   361  			perm = existPerm
   362  		} else {
   363  			var err error
   364  			perm, err = access_model.GetUserRepoPermission(ctx, &depMeta.Repository, ctx.Doer)
   365  			if err != nil {
   366  				ctx.ServerError("GetUserRepoPermission", err)
   367  				return
   368  			}
   369  			repoPerms[depMeta.RepoID] = perm
   370  		}
   371  
   372  		if !perm.CanReadIssuesOrPulls(depMeta.Issue.IsPull) {
   373  			continue
   374  		}
   375  
   376  		depMeta.Issue.Repo = &depMeta.Repository
   377  		issues = append(issues, &depMeta.Issue)
   378  	}
   379  
   380  	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, issues))
   381  }
   382  
   383  // CreateIssueBlocking block the issue given in the body by the issue in path
   384  func CreateIssueBlocking(ctx *context.APIContext) {
   385  	// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/blocks issue issueCreateIssueBlocking
   386  	// ---
   387  	// summary: Block the issue given in the body by the issue in path
   388  	// produces:
   389  	// - application/json
   390  	// parameters:
   391  	// - name: owner
   392  	//   in: path
   393  	//   description: owner of the repo
   394  	//   type: string
   395  	//   required: true
   396  	// - name: repo
   397  	//   in: path
   398  	//   description: name of the repo
   399  	//   type: string
   400  	//   required: true
   401  	// - name: index
   402  	//   in: path
   403  	//   description: index of the issue
   404  	//   type: string
   405  	//   required: true
   406  	// - name: body
   407  	//   in: body
   408  	//   schema:
   409  	//     "$ref": "#/definitions/IssueMeta"
   410  	// responses:
   411  	//   "201":
   412  	//     "$ref": "#/responses/Issue"
   413  	//   "404":
   414  	//     description: the issue does not exist
   415  
   416  	dependency := getParamsIssue(ctx)
   417  	if ctx.Written() {
   418  		return
   419  	}
   420  
   421  	form := web.GetForm(ctx).(*api.IssueMeta)
   422  	target := getFormIssue(ctx, form)
   423  	if ctx.Written() {
   424  		return
   425  	}
   426  
   427  	targetPerm := getPermissionForRepo(ctx, target.Repo)
   428  	if ctx.Written() {
   429  		return
   430  	}
   431  
   432  	createIssueDependency(ctx, target, dependency, *targetPerm, ctx.Repo.Permission)
   433  	if ctx.Written() {
   434  		return
   435  	}
   436  
   437  	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, dependency))
   438  }
   439  
   440  // RemoveIssueBlocking unblock the issue given in the body by the issue in path
   441  func RemoveIssueBlocking(ctx *context.APIContext) {
   442  	// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/blocks issue issueRemoveIssueBlocking
   443  	// ---
   444  	// summary: Unblock the issue given in the body by the issue in path
   445  	// produces:
   446  	// - application/json
   447  	// parameters:
   448  	// - name: owner
   449  	//   in: path
   450  	//   description: owner of the repo
   451  	//   type: string
   452  	//   required: true
   453  	// - name: repo
   454  	//   in: path
   455  	//   description: name of the repo
   456  	//   type: string
   457  	//   required: true
   458  	// - name: index
   459  	//   in: path
   460  	//   description: index of the issue
   461  	//   type: string
   462  	//   required: true
   463  	// - name: body
   464  	//   in: body
   465  	//   schema:
   466  	//     "$ref": "#/definitions/IssueMeta"
   467  	// responses:
   468  	//   "200":
   469  	//     "$ref": "#/responses/Issue"
   470  	//   "404":
   471  	//     "$ref": "#/responses/notFound"
   472  
   473  	dependency := getParamsIssue(ctx)
   474  	if ctx.Written() {
   475  		return
   476  	}
   477  
   478  	form := web.GetForm(ctx).(*api.IssueMeta)
   479  	target := getFormIssue(ctx, form)
   480  	if ctx.Written() {
   481  		return
   482  	}
   483  
   484  	targetPerm := getPermissionForRepo(ctx, target.Repo)
   485  	if ctx.Written() {
   486  		return
   487  	}
   488  
   489  	removeIssueDependency(ctx, target, dependency, *targetPerm, ctx.Repo.Permission)
   490  	if ctx.Written() {
   491  		return
   492  	}
   493  
   494  	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, dependency))
   495  }
   496  
   497  func getParamsIssue(ctx *context.APIContext) *issues_model.Issue {
   498  	issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
   499  	if err != nil {
   500  		if issues_model.IsErrIssueNotExist(err) {
   501  			ctx.NotFound("IsErrIssueNotExist", err)
   502  		} else {
   503  			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
   504  		}
   505  		return nil
   506  	}
   507  	issue.Repo = ctx.Repo.Repository
   508  	return issue
   509  }
   510  
   511  func getFormIssue(ctx *context.APIContext, form *api.IssueMeta) *issues_model.Issue {
   512  	var repo *repo_model.Repository
   513  	if form.Owner != ctx.Repo.Repository.OwnerName || form.Name != ctx.Repo.Repository.Name {
   514  		if !setting.Service.AllowCrossRepositoryDependencies {
   515  			ctx.JSON(http.StatusBadRequest, "CrossRepositoryDependencies not enabled")
   516  			return nil
   517  		}
   518  		var err error
   519  		repo, err = repo_model.GetRepositoryByOwnerAndName(ctx, form.Owner, form.Name)
   520  		if err != nil {
   521  			if repo_model.IsErrRepoNotExist(err) {
   522  				ctx.NotFound("IsErrRepoNotExist", err)
   523  			} else {
   524  				ctx.Error(http.StatusInternalServerError, "GetRepositoryByOwnerAndName", err)
   525  			}
   526  			return nil
   527  		}
   528  	} else {
   529  		repo = ctx.Repo.Repository
   530  	}
   531  
   532  	issue, err := issues_model.GetIssueByIndex(ctx, repo.ID, form.Index)
   533  	if err != nil {
   534  		if issues_model.IsErrIssueNotExist(err) {
   535  			ctx.NotFound("IsErrIssueNotExist", err)
   536  		} else {
   537  			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
   538  		}
   539  		return nil
   540  	}
   541  	issue.Repo = repo
   542  	return issue
   543  }
   544  
   545  func getPermissionForRepo(ctx *context.APIContext, repo *repo_model.Repository) *access_model.Permission {
   546  	if repo.ID == ctx.Repo.Repository.ID {
   547  		return &ctx.Repo.Permission
   548  	}
   549  
   550  	perm, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
   551  	if err != nil {
   552  		ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
   553  		return nil
   554  	}
   555  
   556  	return &perm
   557  }
   558  
   559  func createIssueDependency(ctx *context.APIContext, target, dependency *issues_model.Issue, targetPerm, dependencyPerm access_model.Permission) {
   560  	if target.Repo.IsArchived || !target.Repo.IsDependenciesEnabled(ctx) {
   561  		// The target's repository doesn't have dependencies enabled
   562  		ctx.NotFound()
   563  		return
   564  	}
   565  
   566  	if !targetPerm.CanWriteIssuesOrPulls(target.IsPull) {
   567  		// We can't write to the target
   568  		ctx.NotFound()
   569  		return
   570  	}
   571  
   572  	if !dependencyPerm.CanReadIssuesOrPulls(dependency.IsPull) {
   573  		// We can't read the dependency
   574  		ctx.NotFound()
   575  		return
   576  	}
   577  
   578  	err := issues_model.CreateIssueDependency(ctx.Doer, target, dependency)
   579  	if err != nil {
   580  		ctx.Error(http.StatusInternalServerError, "CreateIssueDependency", err)
   581  		return
   582  	}
   583  }
   584  
   585  func removeIssueDependency(ctx *context.APIContext, target, dependency *issues_model.Issue, targetPerm, dependencyPerm access_model.Permission) {
   586  	if target.Repo.IsArchived || !target.Repo.IsDependenciesEnabled(ctx) {
   587  		// The target's repository doesn't have dependencies enabled
   588  		ctx.NotFound()
   589  		return
   590  	}
   591  
   592  	if !targetPerm.CanWriteIssuesOrPulls(target.IsPull) {
   593  		// We can't write to the target
   594  		ctx.NotFound()
   595  		return
   596  	}
   597  
   598  	if !dependencyPerm.CanReadIssuesOrPulls(dependency.IsPull) {
   599  		// We can't read the dependency
   600  		ctx.NotFound()
   601  		return
   602  	}
   603  
   604  	err := issues_model.RemoveIssueDependency(ctx.Doer, target, dependency, issues_model.DependencyTypeBlockedBy)
   605  	if err != nil {
   606  		ctx.Error(http.StatusInternalServerError, "CreateIssueDependency", err)
   607  		return
   608  	}
   609  }