code.gitea.io/gitea@v1.22.3/routers/api/v1/repo/issue.go (about)

     1  // Copyright 2016 The Gogs Authors. All rights reserved.
     2  // Copyright 2018 The Gitea Authors. All rights reserved.
     3  // SPDX-License-Identifier: MIT
     4  
     5  package repo
     6  
     7  import (
     8  	"errors"
     9  	"fmt"
    10  	"net/http"
    11  	"strconv"
    12  	"strings"
    13  	"time"
    14  
    15  	"code.gitea.io/gitea/models/db"
    16  	issues_model "code.gitea.io/gitea/models/issues"
    17  	"code.gitea.io/gitea/models/organization"
    18  	access_model "code.gitea.io/gitea/models/perm/access"
    19  	repo_model "code.gitea.io/gitea/models/repo"
    20  	"code.gitea.io/gitea/models/unit"
    21  	user_model "code.gitea.io/gitea/models/user"
    22  	issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
    23  	"code.gitea.io/gitea/modules/optional"
    24  	"code.gitea.io/gitea/modules/setting"
    25  	api "code.gitea.io/gitea/modules/structs"
    26  	"code.gitea.io/gitea/modules/timeutil"
    27  	"code.gitea.io/gitea/modules/web"
    28  	"code.gitea.io/gitea/routers/api/v1/utils"
    29  	"code.gitea.io/gitea/services/context"
    30  	"code.gitea.io/gitea/services/convert"
    31  	issue_service "code.gitea.io/gitea/services/issue"
    32  )
    33  
    34  // SearchIssues searches for issues across the repositories that the user has access to
    35  func SearchIssues(ctx *context.APIContext) {
    36  	// swagger:operation GET /repos/issues/search issue issueSearchIssues
    37  	// ---
    38  	// summary: Search for issues across the repositories that the user has access to
    39  	// produces:
    40  	// - application/json
    41  	// parameters:
    42  	// - name: state
    43  	//   in: query
    44  	//   description: whether issue is open or closed
    45  	//   type: string
    46  	// - name: labels
    47  	//   in: query
    48  	//   description: comma separated list of labels. Fetch only issues that have any of this labels. Non existent labels are discarded
    49  	//   type: string
    50  	// - name: milestones
    51  	//   in: query
    52  	//   description: comma separated list of milestone names. Fetch only issues that have any of this milestones. Non existent are discarded
    53  	//   type: string
    54  	// - name: q
    55  	//   in: query
    56  	//   description: search string
    57  	//   type: string
    58  	// - name: priority_repo_id
    59  	//   in: query
    60  	//   description: repository to prioritize in the results
    61  	//   type: integer
    62  	//   format: int64
    63  	// - name: type
    64  	//   in: query
    65  	//   description: filter by type (issues / pulls) if set
    66  	//   type: string
    67  	// - name: since
    68  	//   in: query
    69  	//   description: Only show notifications updated after the given time. This is a timestamp in RFC 3339 format
    70  	//   type: string
    71  	//   format: date-time
    72  	//   required: false
    73  	// - name: before
    74  	//   in: query
    75  	//   description: Only show notifications updated before the given time. This is a timestamp in RFC 3339 format
    76  	//   type: string
    77  	//   format: date-time
    78  	//   required: false
    79  	// - name: assigned
    80  	//   in: query
    81  	//   description: filter (issues / pulls) assigned to you, default is false
    82  	//   type: boolean
    83  	// - name: created
    84  	//   in: query
    85  	//   description: filter (issues / pulls) created by you, default is false
    86  	//   type: boolean
    87  	// - name: mentioned
    88  	//   in: query
    89  	//   description: filter (issues / pulls) mentioning you, default is false
    90  	//   type: boolean
    91  	// - name: review_requested
    92  	//   in: query
    93  	//   description: filter pulls requesting your review, default is false
    94  	//   type: boolean
    95  	// - name: reviewed
    96  	//   in: query
    97  	//   description: filter pulls reviewed by you, default is false
    98  	//   type: boolean
    99  	// - name: owner
   100  	//   in: query
   101  	//   description: filter by owner
   102  	//   type: string
   103  	// - name: team
   104  	//   in: query
   105  	//   description: filter by team (requires organization owner parameter to be provided)
   106  	//   type: string
   107  	// - name: page
   108  	//   in: query
   109  	//   description: page number of results to return (1-based)
   110  	//   type: integer
   111  	// - name: limit
   112  	//   in: query
   113  	//   description: page size of results
   114  	//   type: integer
   115  	// responses:
   116  	//   "200":
   117  	//     "$ref": "#/responses/IssueList"
   118  
   119  	before, since, err := context.GetQueryBeforeSince(ctx.Base)
   120  	if err != nil {
   121  		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
   122  		return
   123  	}
   124  
   125  	var isClosed optional.Option[bool]
   126  	switch ctx.FormString("state") {
   127  	case "closed":
   128  		isClosed = optional.Some(true)
   129  	case "all":
   130  		isClosed = optional.None[bool]()
   131  	default:
   132  		isClosed = optional.Some(false)
   133  	}
   134  
   135  	var (
   136  		repoIDs   []int64
   137  		allPublic bool
   138  	)
   139  	{
   140  		// find repos user can access (for issue search)
   141  		opts := &repo_model.SearchRepoOptions{
   142  			Private:     false,
   143  			AllPublic:   true,
   144  			TopicOnly:   false,
   145  			Collaborate: optional.None[bool](),
   146  			// This needs to be a column that is not nil in fixtures or
   147  			// MySQL will return different results when sorting by null in some cases
   148  			OrderBy: db.SearchOrderByAlphabetically,
   149  			Actor:   ctx.Doer,
   150  		}
   151  		if ctx.IsSigned {
   152  			opts.Private = !ctx.PublicOnly
   153  			opts.AllLimited = true
   154  		}
   155  		if ctx.FormString("owner") != "" {
   156  			owner, err := user_model.GetUserByName(ctx, ctx.FormString("owner"))
   157  			if err != nil {
   158  				if user_model.IsErrUserNotExist(err) {
   159  					ctx.Error(http.StatusBadRequest, "Owner not found", err)
   160  				} else {
   161  					ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
   162  				}
   163  				return
   164  			}
   165  			opts.OwnerID = owner.ID
   166  			opts.AllLimited = false
   167  			opts.AllPublic = false
   168  			opts.Collaborate = optional.Some(false)
   169  		}
   170  		if ctx.FormString("team") != "" {
   171  			if ctx.FormString("owner") == "" {
   172  				ctx.Error(http.StatusBadRequest, "", "Owner organisation is required for filtering on team")
   173  				return
   174  			}
   175  			team, err := organization.GetTeam(ctx, opts.OwnerID, ctx.FormString("team"))
   176  			if err != nil {
   177  				if organization.IsErrTeamNotExist(err) {
   178  					ctx.Error(http.StatusBadRequest, "Team not found", err)
   179  				} else {
   180  					ctx.Error(http.StatusInternalServerError, "GetUserByName", err)
   181  				}
   182  				return
   183  			}
   184  			opts.TeamID = team.ID
   185  		}
   186  
   187  		if opts.AllPublic {
   188  			allPublic = true
   189  			opts.AllPublic = false // set it false to avoid returning too many repos, we could filter by indexer
   190  		}
   191  		repoIDs, _, err = repo_model.SearchRepositoryIDs(ctx, opts)
   192  		if err != nil {
   193  			ctx.Error(http.StatusInternalServerError, "SearchRepositoryIDs", err)
   194  			return
   195  		}
   196  		if len(repoIDs) == 0 {
   197  			// no repos found, don't let the indexer return all repos
   198  			repoIDs = []int64{0}
   199  		}
   200  	}
   201  
   202  	keyword := ctx.FormTrim("q")
   203  	if strings.IndexByte(keyword, 0) >= 0 {
   204  		keyword = ""
   205  	}
   206  
   207  	var isPull optional.Option[bool]
   208  	switch ctx.FormString("type") {
   209  	case "pulls":
   210  		isPull = optional.Some(true)
   211  	case "issues":
   212  		isPull = optional.Some(false)
   213  	default:
   214  		isPull = optional.None[bool]()
   215  	}
   216  
   217  	var includedAnyLabels []int64
   218  	{
   219  		labels := ctx.FormTrim("labels")
   220  		var includedLabelNames []string
   221  		if len(labels) > 0 {
   222  			includedLabelNames = strings.Split(labels, ",")
   223  		}
   224  		includedAnyLabels, err = issues_model.GetLabelIDsByNames(ctx, includedLabelNames)
   225  		if err != nil {
   226  			ctx.Error(http.StatusInternalServerError, "GetLabelIDsByNames", err)
   227  			return
   228  		}
   229  	}
   230  
   231  	var includedMilestones []int64
   232  	{
   233  		milestones := ctx.FormTrim("milestones")
   234  		var includedMilestoneNames []string
   235  		if len(milestones) > 0 {
   236  			includedMilestoneNames = strings.Split(milestones, ",")
   237  		}
   238  		includedMilestones, err = issues_model.GetMilestoneIDsByNames(ctx, includedMilestoneNames)
   239  		if err != nil {
   240  			ctx.Error(http.StatusInternalServerError, "GetMilestoneIDsByNames", err)
   241  			return
   242  		}
   243  	}
   244  
   245  	// this api is also used in UI,
   246  	// so the default limit is set to fit UI needs
   247  	limit := ctx.FormInt("limit")
   248  	if limit == 0 {
   249  		limit = setting.UI.IssuePagingNum
   250  	} else if limit > setting.API.MaxResponseItems {
   251  		limit = setting.API.MaxResponseItems
   252  	}
   253  
   254  	searchOpt := &issue_indexer.SearchOptions{
   255  		Paginator: &db.ListOptions{
   256  			PageSize: limit,
   257  			Page:     ctx.FormInt("page"),
   258  		},
   259  		Keyword:             keyword,
   260  		RepoIDs:             repoIDs,
   261  		AllPublic:           allPublic,
   262  		IsPull:              isPull,
   263  		IsClosed:            isClosed,
   264  		IncludedAnyLabelIDs: includedAnyLabels,
   265  		MilestoneIDs:        includedMilestones,
   266  		SortBy:              issue_indexer.SortByCreatedDesc,
   267  	}
   268  
   269  	if since != 0 {
   270  		searchOpt.UpdatedAfterUnix = optional.Some(since)
   271  	}
   272  	if before != 0 {
   273  		searchOpt.UpdatedBeforeUnix = optional.Some(before)
   274  	}
   275  
   276  	if ctx.IsSigned {
   277  		ctxUserID := ctx.Doer.ID
   278  		if ctx.FormBool("created") {
   279  			searchOpt.PosterID = optional.Some(ctxUserID)
   280  		}
   281  		if ctx.FormBool("assigned") {
   282  			searchOpt.AssigneeID = optional.Some(ctxUserID)
   283  		}
   284  		if ctx.FormBool("mentioned") {
   285  			searchOpt.MentionID = optional.Some(ctxUserID)
   286  		}
   287  		if ctx.FormBool("review_requested") {
   288  			searchOpt.ReviewRequestedID = optional.Some(ctxUserID)
   289  		}
   290  		if ctx.FormBool("reviewed") {
   291  			searchOpt.ReviewedID = optional.Some(ctxUserID)
   292  		}
   293  	}
   294  
   295  	// FIXME: It's unsupported to sort by priority repo when searching by indexer,
   296  	//        it's indeed an regression, but I think it is worth to support filtering by indexer first.
   297  	_ = ctx.FormInt64("priority_repo_id")
   298  
   299  	ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
   300  	if err != nil {
   301  		ctx.Error(http.StatusInternalServerError, "SearchIssues", err)
   302  		return
   303  	}
   304  	issues, err := issues_model.GetIssuesByIDs(ctx, ids, true)
   305  	if err != nil {
   306  		ctx.Error(http.StatusInternalServerError, "FindIssuesByIDs", err)
   307  		return
   308  	}
   309  
   310  	ctx.SetLinkHeader(int(total), limit)
   311  	ctx.SetTotalCountHeader(total)
   312  	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, issues))
   313  }
   314  
   315  // ListIssues list the issues of a repository
   316  func ListIssues(ctx *context.APIContext) {
   317  	// swagger:operation GET /repos/{owner}/{repo}/issues issue issueListIssues
   318  	// ---
   319  	// summary: List a repository's issues
   320  	// produces:
   321  	// - application/json
   322  	// parameters:
   323  	// - name: owner
   324  	//   in: path
   325  	//   description: owner of the repo
   326  	//   type: string
   327  	//   required: true
   328  	// - name: repo
   329  	//   in: path
   330  	//   description: name of the repo
   331  	//   type: string
   332  	//   required: true
   333  	// - name: state
   334  	//   in: query
   335  	//   description: whether issue is open or closed
   336  	//   type: string
   337  	//   enum: [closed, open, all]
   338  	// - name: labels
   339  	//   in: query
   340  	//   description: comma separated list of labels. Fetch only issues that have any of this labels. Non existent labels are discarded
   341  	//   type: string
   342  	// - name: q
   343  	//   in: query
   344  	//   description: search string
   345  	//   type: string
   346  	// - name: type
   347  	//   in: query
   348  	//   description: filter by type (issues / pulls) if set
   349  	//   type: string
   350  	//   enum: [issues, pulls]
   351  	// - name: milestones
   352  	//   in: query
   353  	//   description: comma separated list of milestone names or ids. It uses names and fall back to ids. Fetch only issues that have any of this milestones. Non existent milestones are discarded
   354  	//   type: string
   355  	// - name: since
   356  	//   in: query
   357  	//   description: Only show items updated after the given time. This is a timestamp in RFC 3339 format
   358  	//   type: string
   359  	//   format: date-time
   360  	//   required: false
   361  	// - name: before
   362  	//   in: query
   363  	//   description: Only show items updated before the given time. This is a timestamp in RFC 3339 format
   364  	//   type: string
   365  	//   format: date-time
   366  	//   required: false
   367  	// - name: created_by
   368  	//   in: query
   369  	//   description: Only show items which were created by the given user
   370  	//   type: string
   371  	// - name: assigned_by
   372  	//   in: query
   373  	//   description: Only show items for which the given user is assigned
   374  	//   type: string
   375  	// - name: mentioned_by
   376  	//   in: query
   377  	//   description: Only show items in which the given user was mentioned
   378  	//   type: string
   379  	// - name: page
   380  	//   in: query
   381  	//   description: page number of results to return (1-based)
   382  	//   type: integer
   383  	// - name: limit
   384  	//   in: query
   385  	//   description: page size of results
   386  	//   type: integer
   387  	// responses:
   388  	//   "200":
   389  	//     "$ref": "#/responses/IssueList"
   390  	//   "404":
   391  	//     "$ref": "#/responses/notFound"
   392  	before, since, err := context.GetQueryBeforeSince(ctx.Base)
   393  	if err != nil {
   394  		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
   395  		return
   396  	}
   397  
   398  	var isClosed optional.Option[bool]
   399  	switch ctx.FormString("state") {
   400  	case "closed":
   401  		isClosed = optional.Some(true)
   402  	case "all":
   403  		isClosed = optional.None[bool]()
   404  	default:
   405  		isClosed = optional.Some(false)
   406  	}
   407  
   408  	keyword := ctx.FormTrim("q")
   409  	if strings.IndexByte(keyword, 0) >= 0 {
   410  		keyword = ""
   411  	}
   412  
   413  	var labelIDs []int64
   414  	if splitted := strings.Split(ctx.FormString("labels"), ","); len(splitted) > 0 {
   415  		labelIDs, err = issues_model.GetLabelIDsInRepoByNames(ctx, ctx.Repo.Repository.ID, splitted)
   416  		if err != nil {
   417  			ctx.Error(http.StatusInternalServerError, "GetLabelIDsInRepoByNames", err)
   418  			return
   419  		}
   420  	}
   421  
   422  	var mileIDs []int64
   423  	if part := strings.Split(ctx.FormString("milestones"), ","); len(part) > 0 {
   424  		for i := range part {
   425  			// uses names and fall back to ids
   426  			// non existent milestones are discarded
   427  			mile, err := issues_model.GetMilestoneByRepoIDANDName(ctx, ctx.Repo.Repository.ID, part[i])
   428  			if err == nil {
   429  				mileIDs = append(mileIDs, mile.ID)
   430  				continue
   431  			}
   432  			if !issues_model.IsErrMilestoneNotExist(err) {
   433  				ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoIDANDName", err)
   434  				return
   435  			}
   436  			id, err := strconv.ParseInt(part[i], 10, 64)
   437  			if err != nil {
   438  				continue
   439  			}
   440  			mile, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, id)
   441  			if err == nil {
   442  				mileIDs = append(mileIDs, mile.ID)
   443  				continue
   444  			}
   445  			if issues_model.IsErrMilestoneNotExist(err) {
   446  				continue
   447  			}
   448  			ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoID", err)
   449  		}
   450  	}
   451  
   452  	listOptions := utils.GetListOptions(ctx)
   453  
   454  	isPull := optional.None[bool]()
   455  	switch ctx.FormString("type") {
   456  	case "pulls":
   457  		isPull = optional.Some(true)
   458  	case "issues":
   459  		isPull = optional.Some(false)
   460  	}
   461  
   462  	if isPull.Has() && !ctx.Repo.CanReadIssuesOrPulls(isPull.Value()) {
   463  		ctx.NotFound()
   464  		return
   465  	}
   466  
   467  	if !isPull.Has() {
   468  		canReadIssues := ctx.Repo.CanRead(unit.TypeIssues)
   469  		canReadPulls := ctx.Repo.CanRead(unit.TypePullRequests)
   470  		if !canReadIssues && !canReadPulls {
   471  			ctx.NotFound()
   472  			return
   473  		} else if !canReadIssues {
   474  			isPull = optional.Some(true)
   475  		} else if !canReadPulls {
   476  			isPull = optional.Some(false)
   477  		}
   478  	}
   479  
   480  	// FIXME: we should be more efficient here
   481  	createdByID := getUserIDForFilter(ctx, "created_by")
   482  	if ctx.Written() {
   483  		return
   484  	}
   485  	assignedByID := getUserIDForFilter(ctx, "assigned_by")
   486  	if ctx.Written() {
   487  		return
   488  	}
   489  	mentionedByID := getUserIDForFilter(ctx, "mentioned_by")
   490  	if ctx.Written() {
   491  		return
   492  	}
   493  
   494  	searchOpt := &issue_indexer.SearchOptions{
   495  		Paginator: &listOptions,
   496  		Keyword:   keyword,
   497  		RepoIDs:   []int64{ctx.Repo.Repository.ID},
   498  		IsPull:    isPull,
   499  		IsClosed:  isClosed,
   500  		SortBy:    issue_indexer.SortByCreatedDesc,
   501  	}
   502  	if since != 0 {
   503  		searchOpt.UpdatedAfterUnix = optional.Some(since)
   504  	}
   505  	if before != 0 {
   506  		searchOpt.UpdatedBeforeUnix = optional.Some(before)
   507  	}
   508  	if len(labelIDs) == 1 && labelIDs[0] == 0 {
   509  		searchOpt.NoLabelOnly = true
   510  	} else {
   511  		for _, labelID := range labelIDs {
   512  			if labelID > 0 {
   513  				searchOpt.IncludedLabelIDs = append(searchOpt.IncludedLabelIDs, labelID)
   514  			} else {
   515  				searchOpt.ExcludedLabelIDs = append(searchOpt.ExcludedLabelIDs, -labelID)
   516  			}
   517  		}
   518  	}
   519  
   520  	if len(mileIDs) == 1 && mileIDs[0] == db.NoConditionID {
   521  		searchOpt.MilestoneIDs = []int64{0}
   522  	} else {
   523  		searchOpt.MilestoneIDs = mileIDs
   524  	}
   525  
   526  	if createdByID > 0 {
   527  		searchOpt.PosterID = optional.Some(createdByID)
   528  	}
   529  	if assignedByID > 0 {
   530  		searchOpt.AssigneeID = optional.Some(assignedByID)
   531  	}
   532  	if mentionedByID > 0 {
   533  		searchOpt.MentionID = optional.Some(mentionedByID)
   534  	}
   535  
   536  	ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
   537  	if err != nil {
   538  		ctx.Error(http.StatusInternalServerError, "SearchIssues", err)
   539  		return
   540  	}
   541  	issues, err := issues_model.GetIssuesByIDs(ctx, ids, true)
   542  	if err != nil {
   543  		ctx.Error(http.StatusInternalServerError, "FindIssuesByIDs", err)
   544  		return
   545  	}
   546  
   547  	ctx.SetLinkHeader(int(total), listOptions.PageSize)
   548  	ctx.SetTotalCountHeader(total)
   549  	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, ctx.Doer, issues))
   550  }
   551  
   552  func getUserIDForFilter(ctx *context.APIContext, queryName string) int64 {
   553  	userName := ctx.FormString(queryName)
   554  	if len(userName) == 0 {
   555  		return 0
   556  	}
   557  
   558  	user, err := user_model.GetUserByName(ctx, userName)
   559  	if user_model.IsErrUserNotExist(err) {
   560  		ctx.NotFound(err)
   561  		return 0
   562  	}
   563  
   564  	if err != nil {
   565  		ctx.InternalServerError(err)
   566  		return 0
   567  	}
   568  
   569  	return user.ID
   570  }
   571  
   572  // GetIssue get an issue of a repository
   573  func GetIssue(ctx *context.APIContext) {
   574  	// swagger:operation GET /repos/{owner}/{repo}/issues/{index} issue issueGetIssue
   575  	// ---
   576  	// summary: Get an issue
   577  	// produces:
   578  	// - application/json
   579  	// parameters:
   580  	// - name: owner
   581  	//   in: path
   582  	//   description: owner of the repo
   583  	//   type: string
   584  	//   required: true
   585  	// - name: repo
   586  	//   in: path
   587  	//   description: name of the repo
   588  	//   type: string
   589  	//   required: true
   590  	// - name: index
   591  	//   in: path
   592  	//   description: index of the issue to get
   593  	//   type: integer
   594  	//   format: int64
   595  	//   required: true
   596  	// responses:
   597  	//   "200":
   598  	//     "$ref": "#/responses/Issue"
   599  	//   "404":
   600  	//     "$ref": "#/responses/notFound"
   601  
   602  	issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
   603  	if err != nil {
   604  		if issues_model.IsErrIssueNotExist(err) {
   605  			ctx.NotFound()
   606  		} else {
   607  			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
   608  		}
   609  		return
   610  	}
   611  	if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) {
   612  		ctx.NotFound()
   613  		return
   614  	}
   615  	ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, ctx.Doer, issue))
   616  }
   617  
   618  // CreateIssue create an issue of a repository
   619  func CreateIssue(ctx *context.APIContext) {
   620  	// swagger:operation POST /repos/{owner}/{repo}/issues issue issueCreateIssue
   621  	// ---
   622  	// summary: Create an issue. If using deadline only the date will be taken into account, and time of day ignored.
   623  	// consumes:
   624  	// - application/json
   625  	// produces:
   626  	// - application/json
   627  	// parameters:
   628  	// - name: owner
   629  	//   in: path
   630  	//   description: owner of the repo
   631  	//   type: string
   632  	//   required: true
   633  	// - name: repo
   634  	//   in: path
   635  	//   description: name of the repo
   636  	//   type: string
   637  	//   required: true
   638  	// - name: body
   639  	//   in: body
   640  	//   schema:
   641  	//     "$ref": "#/definitions/CreateIssueOption"
   642  	// responses:
   643  	//   "201":
   644  	//     "$ref": "#/responses/Issue"
   645  	//   "403":
   646  	//     "$ref": "#/responses/forbidden"
   647  	//   "404":
   648  	//     "$ref": "#/responses/notFound"
   649  	//   "412":
   650  	//     "$ref": "#/responses/error"
   651  	//   "422":
   652  	//     "$ref": "#/responses/validationError"
   653  	//   "423":
   654  	//     "$ref": "#/responses/repoArchivedError"
   655  
   656  	form := web.GetForm(ctx).(*api.CreateIssueOption)
   657  	var deadlineUnix timeutil.TimeStamp
   658  	if form.Deadline != nil && ctx.Repo.CanWrite(unit.TypeIssues) {
   659  		deadlineUnix = timeutil.TimeStamp(form.Deadline.Unix())
   660  	}
   661  
   662  	issue := &issues_model.Issue{
   663  		RepoID:       ctx.Repo.Repository.ID,
   664  		Repo:         ctx.Repo.Repository,
   665  		Title:        form.Title,
   666  		PosterID:     ctx.Doer.ID,
   667  		Poster:       ctx.Doer,
   668  		Content:      form.Body,
   669  		Ref:          form.Ref,
   670  		DeadlineUnix: deadlineUnix,
   671  	}
   672  
   673  	assigneeIDs := make([]int64, 0)
   674  	var err error
   675  	if ctx.Repo.CanWrite(unit.TypeIssues) {
   676  		issue.MilestoneID = form.Milestone
   677  		assigneeIDs, err = issues_model.MakeIDsFromAPIAssigneesToAdd(ctx, form.Assignee, form.Assignees)
   678  		if err != nil {
   679  			if user_model.IsErrUserNotExist(err) {
   680  				ctx.Error(http.StatusUnprocessableEntity, "", fmt.Sprintf("Assignee does not exist: [name: %s]", err))
   681  			} else {
   682  				ctx.Error(http.StatusInternalServerError, "AddAssigneeByName", err)
   683  			}
   684  			return
   685  		}
   686  
   687  		// Check if the passed assignees is assignable
   688  		for _, aID := range assigneeIDs {
   689  			assignee, err := user_model.GetUserByID(ctx, aID)
   690  			if err != nil {
   691  				ctx.Error(http.StatusInternalServerError, "GetUserByID", err)
   692  				return
   693  			}
   694  
   695  			valid, err := access_model.CanBeAssigned(ctx, assignee, ctx.Repo.Repository, false)
   696  			if err != nil {
   697  				ctx.Error(http.StatusInternalServerError, "canBeAssigned", err)
   698  				return
   699  			}
   700  			if !valid {
   701  				ctx.Error(http.StatusUnprocessableEntity, "canBeAssigned", repo_model.ErrUserDoesNotHaveAccessToRepo{UserID: aID, RepoName: ctx.Repo.Repository.Name})
   702  				return
   703  			}
   704  		}
   705  	} else {
   706  		// setting labels is not allowed if user is not a writer
   707  		form.Labels = make([]int64, 0)
   708  	}
   709  
   710  	if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs, 0); err != nil {
   711  		if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
   712  			ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
   713  		} else if errors.Is(err, user_model.ErrBlockedUser) {
   714  			ctx.Error(http.StatusForbidden, "NewIssue", err)
   715  		} else {
   716  			ctx.Error(http.StatusInternalServerError, "NewIssue", err)
   717  		}
   718  		return
   719  	}
   720  
   721  	if form.Closed {
   722  		if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", true); err != nil {
   723  			if issues_model.IsErrDependenciesLeft(err) {
   724  				ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies")
   725  				return
   726  			}
   727  			ctx.Error(http.StatusInternalServerError, "ChangeStatus", err)
   728  			return
   729  		}
   730  	}
   731  
   732  	// Refetch from database to assign some automatic values
   733  	issue, err = issues_model.GetIssueByID(ctx, issue.ID)
   734  	if err != nil {
   735  		ctx.Error(http.StatusInternalServerError, "GetIssueByID", err)
   736  		return
   737  	}
   738  	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, issue))
   739  }
   740  
   741  // EditIssue modify an issue of a repository
   742  func EditIssue(ctx *context.APIContext) {
   743  	// swagger:operation PATCH /repos/{owner}/{repo}/issues/{index} issue issueEditIssue
   744  	// ---
   745  	// summary: Edit an issue. If using deadline only the date will be taken into account, and time of day ignored.
   746  	// consumes:
   747  	// - application/json
   748  	// produces:
   749  	// - application/json
   750  	// parameters:
   751  	// - name: owner
   752  	//   in: path
   753  	//   description: owner of the repo
   754  	//   type: string
   755  	//   required: true
   756  	// - name: repo
   757  	//   in: path
   758  	//   description: name of the repo
   759  	//   type: string
   760  	//   required: true
   761  	// - name: index
   762  	//   in: path
   763  	//   description: index of the issue to edit
   764  	//   type: integer
   765  	//   format: int64
   766  	//   required: true
   767  	// - name: body
   768  	//   in: body
   769  	//   schema:
   770  	//     "$ref": "#/definitions/EditIssueOption"
   771  	// responses:
   772  	//   "201":
   773  	//     "$ref": "#/responses/Issue"
   774  	//   "403":
   775  	//     "$ref": "#/responses/forbidden"
   776  	//   "404":
   777  	//     "$ref": "#/responses/notFound"
   778  	//   "412":
   779  	//     "$ref": "#/responses/error"
   780  
   781  	form := web.GetForm(ctx).(*api.EditIssueOption)
   782  	issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
   783  	if err != nil {
   784  		if issues_model.IsErrIssueNotExist(err) {
   785  			ctx.NotFound()
   786  		} else {
   787  			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
   788  		}
   789  		return
   790  	}
   791  	issue.Repo = ctx.Repo.Repository
   792  	canWrite := ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
   793  
   794  	err = issue.LoadAttributes(ctx)
   795  	if err != nil {
   796  		ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
   797  		return
   798  	}
   799  
   800  	if !issue.IsPoster(ctx.Doer.ID) && !canWrite {
   801  		ctx.Status(http.StatusForbidden)
   802  		return
   803  	}
   804  
   805  	if len(form.Title) > 0 {
   806  		err = issue_service.ChangeTitle(ctx, issue, ctx.Doer, form.Title)
   807  		if err != nil {
   808  			ctx.Error(http.StatusInternalServerError, "ChangeTitle", err)
   809  			return
   810  		}
   811  	}
   812  	if form.Body != nil {
   813  		err = issue_service.ChangeContent(ctx, issue, ctx.Doer, *form.Body)
   814  		if err != nil {
   815  			ctx.Error(http.StatusInternalServerError, "ChangeContent", err)
   816  			return
   817  		}
   818  	}
   819  	if form.Ref != nil {
   820  		err = issue_service.ChangeIssueRef(ctx, issue, ctx.Doer, *form.Ref)
   821  		if err != nil {
   822  			ctx.Error(http.StatusInternalServerError, "UpdateRef", err)
   823  			return
   824  		}
   825  	}
   826  
   827  	// Update or remove the deadline, only if set and allowed
   828  	if (form.Deadline != nil || form.RemoveDeadline != nil) && canWrite {
   829  		var deadlineUnix timeutil.TimeStamp
   830  
   831  		if form.RemoveDeadline == nil || !*form.RemoveDeadline {
   832  			if form.Deadline == nil {
   833  				ctx.Error(http.StatusBadRequest, "", "The due_date cannot be empty")
   834  				return
   835  			}
   836  			if !form.Deadline.IsZero() {
   837  				deadline := time.Date(form.Deadline.Year(), form.Deadline.Month(), form.Deadline.Day(),
   838  					23, 59, 59, 0, form.Deadline.Location())
   839  				deadlineUnix = timeutil.TimeStamp(deadline.Unix())
   840  			}
   841  		}
   842  
   843  		if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil {
   844  			ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err)
   845  			return
   846  		}
   847  		issue.DeadlineUnix = deadlineUnix
   848  	}
   849  
   850  	// Add/delete assignees
   851  
   852  	// Deleting is done the GitHub way (quote from their api documentation):
   853  	// https://developer.github.com/v3/issues/#edit-an-issue
   854  	// "assignees" (array): Logins for Users to assign to this issue.
   855  	// Pass one or more user logins to replace the set of assignees on this Issue.
   856  	// Send an empty array ([]) to clear all assignees from the Issue.
   857  
   858  	if canWrite && (form.Assignees != nil || form.Assignee != nil) {
   859  		oneAssignee := ""
   860  		if form.Assignee != nil {
   861  			oneAssignee = *form.Assignee
   862  		}
   863  
   864  		err = issue_service.UpdateAssignees(ctx, issue, oneAssignee, form.Assignees, ctx.Doer)
   865  		if err != nil {
   866  			if errors.Is(err, user_model.ErrBlockedUser) {
   867  				ctx.Error(http.StatusForbidden, "UpdateAssignees", err)
   868  			} else {
   869  				ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err)
   870  			}
   871  			return
   872  		}
   873  	}
   874  
   875  	if canWrite && form.Milestone != nil &&
   876  		issue.MilestoneID != *form.Milestone {
   877  		oldMilestoneID := issue.MilestoneID
   878  		issue.MilestoneID = *form.Milestone
   879  		if err = issue_service.ChangeMilestoneAssign(ctx, issue, ctx.Doer, oldMilestoneID); err != nil {
   880  			ctx.Error(http.StatusInternalServerError, "ChangeMilestoneAssign", err)
   881  			return
   882  		}
   883  	}
   884  	if form.State != nil {
   885  		if issue.IsPull {
   886  			if err := issue.LoadPullRequest(ctx); err != nil {
   887  				ctx.Error(http.StatusInternalServerError, "GetPullRequest", err)
   888  				return
   889  			}
   890  			if issue.PullRequest.HasMerged {
   891  				ctx.Error(http.StatusPreconditionFailed, "MergedPRState", "cannot change state of this pull request, it was already merged")
   892  				return
   893  			}
   894  		}
   895  
   896  		var isClosed bool
   897  		switch state := api.StateType(*form.State); state {
   898  		case api.StateOpen:
   899  			isClosed = false
   900  		case api.StateClosed:
   901  			isClosed = true
   902  		default:
   903  			ctx.Error(http.StatusPreconditionFailed, "UnknownIssueStateError", fmt.Sprintf("unknown state: %s", state))
   904  			return
   905  		}
   906  
   907  		if issue.IsClosed != isClosed {
   908  			if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", isClosed); err != nil {
   909  				if issues_model.IsErrDependenciesLeft(err) {
   910  					ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies")
   911  					return
   912  				}
   913  				ctx.Error(http.StatusInternalServerError, "ChangeStatus", err)
   914  				return
   915  			}
   916  		}
   917  	}
   918  
   919  	// Refetch from database to assign some automatic values
   920  	issue, err = issues_model.GetIssueByID(ctx, issue.ID)
   921  	if err != nil {
   922  		ctx.InternalServerError(err)
   923  		return
   924  	}
   925  	if err = issue.LoadMilestone(ctx); err != nil {
   926  		ctx.InternalServerError(err)
   927  		return
   928  	}
   929  	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, ctx.Doer, issue))
   930  }
   931  
   932  func DeleteIssue(ctx *context.APIContext) {
   933  	// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index} issue issueDelete
   934  	// ---
   935  	// summary: Delete an issue
   936  	// parameters:
   937  	// - name: owner
   938  	//   in: path
   939  	//   description: owner of the repo
   940  	//   type: string
   941  	//   required: true
   942  	// - name: repo
   943  	//   in: path
   944  	//   description: name of the repo
   945  	//   type: string
   946  	//   required: true
   947  	// - name: index
   948  	//   in: path
   949  	//   description: index of issue to delete
   950  	//   type: integer
   951  	//   format: int64
   952  	//   required: true
   953  	// responses:
   954  	//   "204":
   955  	//     "$ref": "#/responses/empty"
   956  	//   "403":
   957  	//     "$ref": "#/responses/forbidden"
   958  	//   "404":
   959  	//     "$ref": "#/responses/notFound"
   960  	issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
   961  	if err != nil {
   962  		if issues_model.IsErrIssueNotExist(err) {
   963  			ctx.NotFound(err)
   964  		} else {
   965  			ctx.Error(http.StatusInternalServerError, "GetIssueByID", err)
   966  		}
   967  		return
   968  	}
   969  
   970  	if err = issue_service.DeleteIssue(ctx, ctx.Doer, ctx.Repo.GitRepo, issue); err != nil {
   971  		ctx.Error(http.StatusInternalServerError, "DeleteIssueByID", err)
   972  		return
   973  	}
   974  
   975  	ctx.Status(http.StatusNoContent)
   976  }
   977  
   978  // UpdateIssueDeadline updates an issue deadline
   979  func UpdateIssueDeadline(ctx *context.APIContext) {
   980  	// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/deadline issue issueEditIssueDeadline
   981  	// ---
   982  	// summary: Set an issue deadline. If set to null, the deadline is deleted. If using deadline only the date will be taken into account, and time of day ignored.
   983  	// consumes:
   984  	// - application/json
   985  	// produces:
   986  	// - application/json
   987  	// parameters:
   988  	// - name: owner
   989  	//   in: path
   990  	//   description: owner of the repo
   991  	//   type: string
   992  	//   required: true
   993  	// - name: repo
   994  	//   in: path
   995  	//   description: name of the repo
   996  	//   type: string
   997  	//   required: true
   998  	// - name: index
   999  	//   in: path
  1000  	//   description: index of the issue to create or update a deadline on
  1001  	//   type: integer
  1002  	//   format: int64
  1003  	//   required: true
  1004  	// - name: body
  1005  	//   in: body
  1006  	//   schema:
  1007  	//     "$ref": "#/definitions/EditDeadlineOption"
  1008  	// responses:
  1009  	//   "201":
  1010  	//     "$ref": "#/responses/IssueDeadline"
  1011  	//   "403":
  1012  	//     "$ref": "#/responses/forbidden"
  1013  	//   "404":
  1014  	//     "$ref": "#/responses/notFound"
  1015  	form := web.GetForm(ctx).(*api.EditDeadlineOption)
  1016  	issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  1017  	if err != nil {
  1018  		if issues_model.IsErrIssueNotExist(err) {
  1019  			ctx.NotFound()
  1020  		} else {
  1021  			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
  1022  		}
  1023  		return
  1024  	}
  1025  
  1026  	if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
  1027  		ctx.Error(http.StatusForbidden, "", "Not repo writer")
  1028  		return
  1029  	}
  1030  
  1031  	var deadlineUnix timeutil.TimeStamp
  1032  	var deadline time.Time
  1033  	if form.Deadline != nil && !form.Deadline.IsZero() {
  1034  		deadline = time.Date(form.Deadline.Year(), form.Deadline.Month(), form.Deadline.Day(),
  1035  			23, 59, 59, 0, time.Local)
  1036  		deadlineUnix = timeutil.TimeStamp(deadline.Unix())
  1037  	}
  1038  
  1039  	if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil {
  1040  		ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err)
  1041  		return
  1042  	}
  1043  
  1044  	ctx.JSON(http.StatusCreated, api.IssueDeadline{Deadline: &deadline})
  1045  }