code.gitea.io/gitea@v1.22.3/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/setting"
    15  	api "code.gitea.io/gitea/modules/structs"
    16  	"code.gitea.io/gitea/modules/web"
    17  	"code.gitea.io/gitea/services/context"
    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, ctx.Doer, 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  	//   "423":
   192  	//     "$ref": "#/responses/repoArchivedError"
   193  
   194  	// We want to make <:index> depend on <Form>, i.e. <:index> is the target
   195  	target := getParamsIssue(ctx)
   196  	if ctx.Written() {
   197  		return
   198  	}
   199  
   200  	// and <Form> represents the dependency
   201  	form := web.GetForm(ctx).(*api.IssueMeta)
   202  	dependency := getFormIssue(ctx, form)
   203  	if ctx.Written() {
   204  		return
   205  	}
   206  
   207  	dependencyPerm := getPermissionForRepo(ctx, target.Repo)
   208  	if ctx.Written() {
   209  		return
   210  	}
   211  
   212  	createIssueDependency(ctx, target, dependency, ctx.Repo.Permission, *dependencyPerm)
   213  	if ctx.Written() {
   214  		return
   215  	}
   216  
   217  	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, target))
   218  }
   219  
   220  // RemoveIssueDependency remove an issue dependency
   221  func RemoveIssueDependency(ctx *context.APIContext) {
   222  	// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/dependencies issue issueRemoveIssueDependencies
   223  	// ---
   224  	// summary: Remove an issue dependency
   225  	// produces:
   226  	// - application/json
   227  	// parameters:
   228  	// - name: owner
   229  	//   in: path
   230  	//   description: owner of the repo
   231  	//   type: string
   232  	//   required: true
   233  	// - name: repo
   234  	//   in: path
   235  	//   description: name of the repo
   236  	//   type: string
   237  	//   required: true
   238  	// - name: index
   239  	//   in: path
   240  	//   description: index of the issue
   241  	//   type: string
   242  	//   required: true
   243  	// - name: body
   244  	//   in: body
   245  	//   schema:
   246  	//     "$ref": "#/definitions/IssueMeta"
   247  	// responses:
   248  	//   "200":
   249  	//     "$ref": "#/responses/Issue"
   250  	//   "404":
   251  	//     "$ref": "#/responses/notFound"
   252  	//   "423":
   253  	//     "$ref": "#/responses/repoArchivedError"
   254  
   255  	// We want to make <:index> depend on <Form>, i.e. <:index> is the target
   256  	target := getParamsIssue(ctx)
   257  	if ctx.Written() {
   258  		return
   259  	}
   260  
   261  	// and <Form> represents the dependency
   262  	form := web.GetForm(ctx).(*api.IssueMeta)
   263  	dependency := getFormIssue(ctx, form)
   264  	if ctx.Written() {
   265  		return
   266  	}
   267  
   268  	dependencyPerm := getPermissionForRepo(ctx, target.Repo)
   269  	if ctx.Written() {
   270  		return
   271  	}
   272  
   273  	removeIssueDependency(ctx, target, dependency, ctx.Repo.Permission, *dependencyPerm)
   274  	if ctx.Written() {
   275  		return
   276  	}
   277  
   278  	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, target))
   279  }
   280  
   281  // GetIssueBlocks list issues that are blocked by this issue
   282  func GetIssueBlocks(ctx *context.APIContext) {
   283  	// swagger:operation GET /repos/{owner}/{repo}/issues/{index}/blocks issue issueListBlocks
   284  	// ---
   285  	// summary: List issues that are blocked by this issue
   286  	// produces:
   287  	// - application/json
   288  	// parameters:
   289  	// - name: owner
   290  	//   in: path
   291  	//   description: owner of the repo
   292  	//   type: string
   293  	//   required: true
   294  	// - name: repo
   295  	//   in: path
   296  	//   description: name of the repo
   297  	//   type: string
   298  	//   required: true
   299  	// - name: index
   300  	//   in: path
   301  	//   description: index of the issue
   302  	//   type: string
   303  	//   required: true
   304  	// - name: page
   305  	//   in: query
   306  	//   description: page number of results to return (1-based)
   307  	//   type: integer
   308  	// - name: limit
   309  	//   in: query
   310  	//   description: page size of results
   311  	//   type: integer
   312  	// responses:
   313  	//   "200":
   314  	//     "$ref": "#/responses/IssueList"
   315  	//   "404":
   316  	//     "$ref": "#/responses/notFound"
   317  
   318  	// We need to list the issues that DEPEND on this issue not the other way round
   319  	// Therefore whether dependencies are enabled or not in this repository is potentially irrelevant.
   320  
   321  	issue := getParamsIssue(ctx)
   322  	if ctx.Written() {
   323  		return
   324  	}
   325  
   326  	if !ctx.Repo.Permission.CanReadIssuesOrPulls(issue.IsPull) {
   327  		ctx.NotFound()
   328  		return
   329  	}
   330  
   331  	page := ctx.FormInt("page")
   332  	if page <= 1 {
   333  		page = 1
   334  	}
   335  	limit := ctx.FormInt("limit")
   336  	if limit <= 1 {
   337  		limit = setting.API.DefaultPagingNum
   338  	}
   339  
   340  	skip := (page - 1) * limit
   341  	max := page * limit
   342  
   343  	deps, err := issue.BlockingDependencies(ctx)
   344  	if err != nil {
   345  		ctx.Error(http.StatusInternalServerError, "BlockingDependencies", err)
   346  		return
   347  	}
   348  
   349  	var issues []*issues_model.Issue
   350  
   351  	repoPerms := make(map[int64]access_model.Permission)
   352  	repoPerms[ctx.Repo.Repository.ID] = ctx.Repo.Permission
   353  
   354  	for i, depMeta := range deps {
   355  		if i < skip || i >= max {
   356  			continue
   357  		}
   358  
   359  		// Get the permissions for this repository
   360  		// If the repo ID exists in the map, return the exist permissions
   361  		// else get the permission and add it to the map
   362  		var perm access_model.Permission
   363  		existPerm, ok := repoPerms[depMeta.RepoID]
   364  		if ok {
   365  			perm = existPerm
   366  		} else {
   367  			var err error
   368  			perm, err = access_model.GetUserRepoPermission(ctx, &depMeta.Repository, ctx.Doer)
   369  			if err != nil {
   370  				ctx.ServerError("GetUserRepoPermission", err)
   371  				return
   372  			}
   373  			repoPerms[depMeta.RepoID] = perm
   374  		}
   375  
   376  		if !perm.CanReadIssuesOrPulls(depMeta.Issue.IsPull) {
   377  			continue
   378  		}
   379  
   380  		depMeta.Issue.Repo = &depMeta.Repository
   381  		issues = append(issues, &depMeta.Issue)
   382  	}
   383  
   384  	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, issues))
   385  }
   386  
   387  // CreateIssueBlocking block the issue given in the body by the issue in path
   388  func CreateIssueBlocking(ctx *context.APIContext) {
   389  	// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/blocks issue issueCreateIssueBlocking
   390  	// ---
   391  	// summary: Block the issue given in the body by the issue in path
   392  	// produces:
   393  	// - application/json
   394  	// parameters:
   395  	// - name: owner
   396  	//   in: path
   397  	//   description: owner of the repo
   398  	//   type: string
   399  	//   required: true
   400  	// - name: repo
   401  	//   in: path
   402  	//   description: name of the repo
   403  	//   type: string
   404  	//   required: true
   405  	// - name: index
   406  	//   in: path
   407  	//   description: index of the issue
   408  	//   type: string
   409  	//   required: true
   410  	// - name: body
   411  	//   in: body
   412  	//   schema:
   413  	//     "$ref": "#/definitions/IssueMeta"
   414  	// responses:
   415  	//   "201":
   416  	//     "$ref": "#/responses/Issue"
   417  	//   "404":
   418  	//     description: the issue does not exist
   419  
   420  	dependency := getParamsIssue(ctx)
   421  	if ctx.Written() {
   422  		return
   423  	}
   424  
   425  	form := web.GetForm(ctx).(*api.IssueMeta)
   426  	target := getFormIssue(ctx, form)
   427  	if ctx.Written() {
   428  		return
   429  	}
   430  
   431  	targetPerm := getPermissionForRepo(ctx, target.Repo)
   432  	if ctx.Written() {
   433  		return
   434  	}
   435  
   436  	createIssueDependency(ctx, target, dependency, *targetPerm, ctx.Repo.Permission)
   437  	if ctx.Written() {
   438  		return
   439  	}
   440  
   441  	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, dependency))
   442  }
   443  
   444  // RemoveIssueBlocking unblock the issue given in the body by the issue in path
   445  func RemoveIssueBlocking(ctx *context.APIContext) {
   446  	// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index}/blocks issue issueRemoveIssueBlocking
   447  	// ---
   448  	// summary: Unblock the issue given in the body by the issue in path
   449  	// produces:
   450  	// - application/json
   451  	// parameters:
   452  	// - name: owner
   453  	//   in: path
   454  	//   description: owner of the repo
   455  	//   type: string
   456  	//   required: true
   457  	// - name: repo
   458  	//   in: path
   459  	//   description: name of the repo
   460  	//   type: string
   461  	//   required: true
   462  	// - name: index
   463  	//   in: path
   464  	//   description: index of the issue
   465  	//   type: string
   466  	//   required: true
   467  	// - name: body
   468  	//   in: body
   469  	//   schema:
   470  	//     "$ref": "#/definitions/IssueMeta"
   471  	// responses:
   472  	//   "200":
   473  	//     "$ref": "#/responses/Issue"
   474  	//   "404":
   475  	//     "$ref": "#/responses/notFound"
   476  
   477  	dependency := getParamsIssue(ctx)
   478  	if ctx.Written() {
   479  		return
   480  	}
   481  
   482  	form := web.GetForm(ctx).(*api.IssueMeta)
   483  	target := getFormIssue(ctx, form)
   484  	if ctx.Written() {
   485  		return
   486  	}
   487  
   488  	targetPerm := getPermissionForRepo(ctx, target.Repo)
   489  	if ctx.Written() {
   490  		return
   491  	}
   492  
   493  	removeIssueDependency(ctx, target, dependency, *targetPerm, ctx.Repo.Permission)
   494  	if ctx.Written() {
   495  		return
   496  	}
   497  
   498  	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, dependency))
   499  }
   500  
   501  func getParamsIssue(ctx *context.APIContext) *issues_model.Issue {
   502  	issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
   503  	if err != nil {
   504  		if issues_model.IsErrIssueNotExist(err) {
   505  			ctx.NotFound("IsErrIssueNotExist", err)
   506  		} else {
   507  			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
   508  		}
   509  		return nil
   510  	}
   511  	issue.Repo = ctx.Repo.Repository
   512  	return issue
   513  }
   514  
   515  func getFormIssue(ctx *context.APIContext, form *api.IssueMeta) *issues_model.Issue {
   516  	var repo *repo_model.Repository
   517  	if form.Owner != ctx.Repo.Repository.OwnerName || form.Name != ctx.Repo.Repository.Name {
   518  		if !setting.Service.AllowCrossRepositoryDependencies {
   519  			ctx.JSON(http.StatusBadRequest, "CrossRepositoryDependencies not enabled")
   520  			return nil
   521  		}
   522  		var err error
   523  		repo, err = repo_model.GetRepositoryByOwnerAndName(ctx, form.Owner, form.Name)
   524  		if err != nil {
   525  			if repo_model.IsErrRepoNotExist(err) {
   526  				ctx.NotFound("IsErrRepoNotExist", err)
   527  			} else {
   528  				ctx.Error(http.StatusInternalServerError, "GetRepositoryByOwnerAndName", err)
   529  			}
   530  			return nil
   531  		}
   532  	} else {
   533  		repo = ctx.Repo.Repository
   534  	}
   535  
   536  	issue, err := issues_model.GetIssueByIndex(ctx, repo.ID, form.Index)
   537  	if err != nil {
   538  		if issues_model.IsErrIssueNotExist(err) {
   539  			ctx.NotFound("IsErrIssueNotExist", err)
   540  		} else {
   541  			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
   542  		}
   543  		return nil
   544  	}
   545  	issue.Repo = repo
   546  	return issue
   547  }
   548  
   549  func getPermissionForRepo(ctx *context.APIContext, repo *repo_model.Repository) *access_model.Permission {
   550  	if repo.ID == ctx.Repo.Repository.ID {
   551  		return &ctx.Repo.Permission
   552  	}
   553  
   554  	perm, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
   555  	if err != nil {
   556  		ctx.Error(http.StatusInternalServerError, "GetUserRepoPermission", err)
   557  		return nil
   558  	}
   559  
   560  	return &perm
   561  }
   562  
   563  func createIssueDependency(ctx *context.APIContext, target, dependency *issues_model.Issue, targetPerm, dependencyPerm access_model.Permission) {
   564  	if target.Repo.IsArchived || !target.Repo.IsDependenciesEnabled(ctx) {
   565  		// The target's repository doesn't have dependencies enabled
   566  		ctx.NotFound()
   567  		return
   568  	}
   569  
   570  	if !targetPerm.CanWriteIssuesOrPulls(target.IsPull) {
   571  		// We can't write to the target
   572  		ctx.NotFound()
   573  		return
   574  	}
   575  
   576  	if !dependencyPerm.CanReadIssuesOrPulls(dependency.IsPull) {
   577  		// We can't read the dependency
   578  		ctx.NotFound()
   579  		return
   580  	}
   581  
   582  	err := issues_model.CreateIssueDependency(ctx, ctx.Doer, target, dependency)
   583  	if err != nil {
   584  		ctx.Error(http.StatusInternalServerError, "CreateIssueDependency", err)
   585  		return
   586  	}
   587  }
   588  
   589  func removeIssueDependency(ctx *context.APIContext, target, dependency *issues_model.Issue, targetPerm, dependencyPerm access_model.Permission) {
   590  	if target.Repo.IsArchived || !target.Repo.IsDependenciesEnabled(ctx) {
   591  		// The target's repository doesn't have dependencies enabled
   592  		ctx.NotFound()
   593  		return
   594  	}
   595  
   596  	if !targetPerm.CanWriteIssuesOrPulls(target.IsPull) {
   597  		// We can't write to the target
   598  		ctx.NotFound()
   599  		return
   600  	}
   601  
   602  	if !dependencyPerm.CanReadIssuesOrPulls(dependency.IsPull) {
   603  		// We can't read the dependency
   604  		ctx.NotFound()
   605  		return
   606  	}
   607  
   608  	err := issues_model.RemoveIssueDependency(ctx, ctx.Doer, target, dependency, issues_model.DependencyTypeBlockedBy)
   609  	if err != nil {
   610  		ctx.Error(http.StatusInternalServerError, "CreateIssueDependency", err)
   611  		return
   612  	}
   613  }