code.gitea.io/gitea@v1.21.7/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  	"fmt"
     9  	"net/http"
    10  	"strconv"
    11  	"strings"
    12  	"time"
    13  
    14  	"code.gitea.io/gitea/models/db"
    15  	issues_model "code.gitea.io/gitea/models/issues"
    16  	"code.gitea.io/gitea/models/organization"
    17  	access_model "code.gitea.io/gitea/models/perm/access"
    18  	repo_model "code.gitea.io/gitea/models/repo"
    19  	"code.gitea.io/gitea/models/unit"
    20  	user_model "code.gitea.io/gitea/models/user"
    21  	"code.gitea.io/gitea/modules/context"
    22  	issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
    23  	"code.gitea.io/gitea/modules/setting"
    24  	api "code.gitea.io/gitea/modules/structs"
    25  	"code.gitea.io/gitea/modules/timeutil"
    26  	"code.gitea.io/gitea/modules/util"
    27  	"code.gitea.io/gitea/modules/web"
    28  	"code.gitea.io/gitea/routers/api/v1/utils"
    29  	"code.gitea.io/gitea/services/convert"
    30  	issue_service "code.gitea.io/gitea/services/issue"
    31  	notify_service "code.gitea.io/gitea/services/notify"
    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 util.OptionalBool
   126  	switch ctx.FormString("state") {
   127  	case "closed":
   128  		isClosed = util.OptionalBoolTrue
   129  	case "all":
   130  		isClosed = util.OptionalBoolNone
   131  	default:
   132  		isClosed = util.OptionalBoolFalse
   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: util.OptionalBoolNone,
   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 = true
   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 = util.OptionalBoolFalse
   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(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 util.OptionalBool
   208  	switch ctx.FormString("type") {
   209  	case "pulls":
   210  		isPull = util.OptionalBoolTrue
   211  	case "issues":
   212  		isPull = util.OptionalBoolFalse
   213  	default:
   214  		isPull = util.OptionalBoolNone
   215  	}
   216  
   217  	var includedAnyLabels []int64
   218  	{
   219  
   220  		labels := ctx.FormTrim("labels")
   221  		var includedLabelNames []string
   222  		if len(labels) > 0 {
   223  			includedLabelNames = strings.Split(labels, ",")
   224  		}
   225  		includedAnyLabels, err = issues_model.GetLabelIDsByNames(ctx, includedLabelNames)
   226  		if err != nil {
   227  			ctx.Error(http.StatusInternalServerError, "GetLabelIDsByNames", err)
   228  			return
   229  		}
   230  	}
   231  
   232  	var includedMilestones []int64
   233  	{
   234  		milestones := ctx.FormTrim("milestones")
   235  		var includedMilestoneNames []string
   236  		if len(milestones) > 0 {
   237  			includedMilestoneNames = strings.Split(milestones, ",")
   238  		}
   239  		includedMilestones, err = issues_model.GetMilestoneIDsByNames(ctx, includedMilestoneNames)
   240  		if err != nil {
   241  			ctx.Error(http.StatusInternalServerError, "GetMilestoneIDsByNames", err)
   242  			return
   243  		}
   244  	}
   245  
   246  	// this api is also used in UI,
   247  	// so the default limit is set to fit UI needs
   248  	limit := ctx.FormInt("limit")
   249  	if limit == 0 {
   250  		limit = setting.UI.IssuePagingNum
   251  	} else if limit > setting.API.MaxResponseItems {
   252  		limit = setting.API.MaxResponseItems
   253  	}
   254  
   255  	searchOpt := &issue_indexer.SearchOptions{
   256  		Paginator: &db.ListOptions{
   257  			PageSize: limit,
   258  			Page:     ctx.FormInt("page"),
   259  		},
   260  		Keyword:             keyword,
   261  		RepoIDs:             repoIDs,
   262  		AllPublic:           allPublic,
   263  		IsPull:              isPull,
   264  		IsClosed:            isClosed,
   265  		IncludedAnyLabelIDs: includedAnyLabels,
   266  		MilestoneIDs:        includedMilestones,
   267  		SortBy:              issue_indexer.SortByCreatedDesc,
   268  	}
   269  
   270  	if since != 0 {
   271  		searchOpt.UpdatedAfterUnix = &since
   272  	}
   273  	if before != 0 {
   274  		searchOpt.UpdatedBeforeUnix = &before
   275  	}
   276  
   277  	if ctx.IsSigned {
   278  		ctxUserID := ctx.Doer.ID
   279  		if ctx.FormBool("created") {
   280  			searchOpt.PosterID = &ctxUserID
   281  		}
   282  		if ctx.FormBool("assigned") {
   283  			searchOpt.AssigneeID = &ctxUserID
   284  		}
   285  		if ctx.FormBool("mentioned") {
   286  			searchOpt.MentionID = &ctxUserID
   287  		}
   288  		if ctx.FormBool("review_requested") {
   289  			searchOpt.ReviewRequestedID = &ctxUserID
   290  		}
   291  		if ctx.FormBool("reviewed") {
   292  			searchOpt.ReviewedID = &ctxUserID
   293  		}
   294  	}
   295  
   296  	// FIXME: It's unsupported to sort by priority repo when searching by indexer,
   297  	//        it's indeed an regression, but I think it is worth to support filtering by indexer first.
   298  	_ = ctx.FormInt64("priority_repo_id")
   299  
   300  	ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
   301  	if err != nil {
   302  		ctx.Error(http.StatusInternalServerError, "SearchIssues", err)
   303  		return
   304  	}
   305  	issues, err := issues_model.GetIssuesByIDs(ctx, ids, true)
   306  	if err != nil {
   307  		ctx.Error(http.StatusInternalServerError, "FindIssuesByIDs", err)
   308  		return
   309  	}
   310  
   311  	ctx.SetLinkHeader(int(total), limit)
   312  	ctx.SetTotalCountHeader(total)
   313  	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, issues))
   314  }
   315  
   316  // ListIssues list the issues of a repository
   317  func ListIssues(ctx *context.APIContext) {
   318  	// swagger:operation GET /repos/{owner}/{repo}/issues issue issueListIssues
   319  	// ---
   320  	// summary: List a repository's issues
   321  	// produces:
   322  	// - application/json
   323  	// parameters:
   324  	// - name: owner
   325  	//   in: path
   326  	//   description: owner of the repo
   327  	//   type: string
   328  	//   required: true
   329  	// - name: repo
   330  	//   in: path
   331  	//   description: name of the repo
   332  	//   type: string
   333  	//   required: true
   334  	// - name: state
   335  	//   in: query
   336  	//   description: whether issue is open or closed
   337  	//   type: string
   338  	//   enum: [closed, open, all]
   339  	// - name: labels
   340  	//   in: query
   341  	//   description: comma separated list of labels. Fetch only issues that have any of this labels. Non existent labels are discarded
   342  	//   type: string
   343  	// - name: q
   344  	//   in: query
   345  	//   description: search string
   346  	//   type: string
   347  	// - name: type
   348  	//   in: query
   349  	//   description: filter by type (issues / pulls) if set
   350  	//   type: string
   351  	//   enum: [issues, pulls]
   352  	// - name: milestones
   353  	//   in: query
   354  	//   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
   355  	//   type: string
   356  	// - name: since
   357  	//   in: query
   358  	//   description: Only show items updated after the given time. This is a timestamp in RFC 3339 format
   359  	//   type: string
   360  	//   format: date-time
   361  	//   required: false
   362  	// - name: before
   363  	//   in: query
   364  	//   description: Only show items updated before the given time. This is a timestamp in RFC 3339 format
   365  	//   type: string
   366  	//   format: date-time
   367  	//   required: false
   368  	// - name: created_by
   369  	//   in: query
   370  	//   description: Only show items which were created by the the given user
   371  	//   type: string
   372  	// - name: assigned_by
   373  	//   in: query
   374  	//   description: Only show items for which the given user is assigned
   375  	//   type: string
   376  	// - name: mentioned_by
   377  	//   in: query
   378  	//   description: Only show items in which the given user was mentioned
   379  	//   type: string
   380  	// - name: page
   381  	//   in: query
   382  	//   description: page number of results to return (1-based)
   383  	//   type: integer
   384  	// - name: limit
   385  	//   in: query
   386  	//   description: page size of results
   387  	//   type: integer
   388  	// responses:
   389  	//   "200":
   390  	//     "$ref": "#/responses/IssueList"
   391  	//   "404":
   392  	//     "$ref": "#/responses/notFound"
   393  	before, since, err := context.GetQueryBeforeSince(ctx.Base)
   394  	if err != nil {
   395  		ctx.Error(http.StatusUnprocessableEntity, "GetQueryBeforeSince", err)
   396  		return
   397  	}
   398  
   399  	var isClosed util.OptionalBool
   400  	switch ctx.FormString("state") {
   401  	case "closed":
   402  		isClosed = util.OptionalBoolTrue
   403  	case "all":
   404  		isClosed = util.OptionalBoolNone
   405  	default:
   406  		isClosed = util.OptionalBoolFalse
   407  	}
   408  
   409  	keyword := ctx.FormTrim("q")
   410  	if strings.IndexByte(keyword, 0) >= 0 {
   411  		keyword = ""
   412  	}
   413  
   414  	var labelIDs []int64
   415  	if splitted := strings.Split(ctx.FormString("labels"), ","); len(splitted) > 0 {
   416  		labelIDs, err = issues_model.GetLabelIDsInRepoByNames(ctx, ctx.Repo.Repository.ID, splitted)
   417  		if err != nil {
   418  			ctx.Error(http.StatusInternalServerError, "GetLabelIDsInRepoByNames", err)
   419  			return
   420  		}
   421  	}
   422  
   423  	var mileIDs []int64
   424  	if part := strings.Split(ctx.FormString("milestones"), ","); len(part) > 0 {
   425  		for i := range part {
   426  			// uses names and fall back to ids
   427  			// non existent milestones are discarded
   428  			mile, err := issues_model.GetMilestoneByRepoIDANDName(ctx, ctx.Repo.Repository.ID, part[i])
   429  			if err == nil {
   430  				mileIDs = append(mileIDs, mile.ID)
   431  				continue
   432  			}
   433  			if !issues_model.IsErrMilestoneNotExist(err) {
   434  				ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoIDANDName", err)
   435  				return
   436  			}
   437  			id, err := strconv.ParseInt(part[i], 10, 64)
   438  			if err != nil {
   439  				continue
   440  			}
   441  			mile, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, id)
   442  			if err == nil {
   443  				mileIDs = append(mileIDs, mile.ID)
   444  				continue
   445  			}
   446  			if issues_model.IsErrMilestoneNotExist(err) {
   447  				continue
   448  			}
   449  			ctx.Error(http.StatusInternalServerError, "GetMilestoneByRepoID", err)
   450  		}
   451  	}
   452  
   453  	listOptions := utils.GetListOptions(ctx)
   454  
   455  	var isPull util.OptionalBool
   456  	switch ctx.FormString("type") {
   457  	case "pulls":
   458  		isPull = util.OptionalBoolTrue
   459  	case "issues":
   460  		isPull = util.OptionalBoolFalse
   461  	default:
   462  		isPull = util.OptionalBoolNone
   463  	}
   464  
   465  	if isPull != util.OptionalBoolNone && !ctx.Repo.CanReadIssuesOrPulls(isPull.IsTrue()) {
   466  		ctx.NotFound()
   467  		return
   468  	}
   469  
   470  	if isPull == util.OptionalBoolNone {
   471  		canReadIssues := ctx.Repo.CanRead(unit.TypeIssues)
   472  		canReadPulls := ctx.Repo.CanRead(unit.TypePullRequests)
   473  		if !canReadIssues && !canReadPulls {
   474  			ctx.NotFound()
   475  			return
   476  		} else if !canReadIssues {
   477  			isPull = util.OptionalBoolTrue
   478  		} else if !canReadPulls {
   479  			isPull = util.OptionalBoolFalse
   480  		}
   481  	}
   482  
   483  	// FIXME: we should be more efficient here
   484  	createdByID := getUserIDForFilter(ctx, "created_by")
   485  	if ctx.Written() {
   486  		return
   487  	}
   488  	assignedByID := getUserIDForFilter(ctx, "assigned_by")
   489  	if ctx.Written() {
   490  		return
   491  	}
   492  	mentionedByID := getUserIDForFilter(ctx, "mentioned_by")
   493  	if ctx.Written() {
   494  		return
   495  	}
   496  
   497  	searchOpt := &issue_indexer.SearchOptions{
   498  		Paginator: &listOptions,
   499  		Keyword:   keyword,
   500  		RepoIDs:   []int64{ctx.Repo.Repository.ID},
   501  		IsPull:    isPull,
   502  		IsClosed:  isClosed,
   503  		SortBy:    issue_indexer.SortByCreatedDesc,
   504  	}
   505  	if since != 0 {
   506  		searchOpt.UpdatedAfterUnix = &since
   507  	}
   508  	if before != 0 {
   509  		searchOpt.UpdatedBeforeUnix = &before
   510  	}
   511  	if len(labelIDs) == 1 && labelIDs[0] == 0 {
   512  		searchOpt.NoLabelOnly = true
   513  	} else {
   514  		for _, labelID := range labelIDs {
   515  			if labelID > 0 {
   516  				searchOpt.IncludedLabelIDs = append(searchOpt.IncludedLabelIDs, labelID)
   517  			} else {
   518  				searchOpt.ExcludedLabelIDs = append(searchOpt.ExcludedLabelIDs, -labelID)
   519  			}
   520  		}
   521  	}
   522  
   523  	if len(mileIDs) == 1 && mileIDs[0] == db.NoConditionID {
   524  		searchOpt.MilestoneIDs = []int64{0}
   525  	} else {
   526  		searchOpt.MilestoneIDs = mileIDs
   527  	}
   528  
   529  	if createdByID > 0 {
   530  		searchOpt.PosterID = &createdByID
   531  	}
   532  	if assignedByID > 0 {
   533  		searchOpt.AssigneeID = &assignedByID
   534  	}
   535  	if mentionedByID > 0 {
   536  		searchOpt.MentionID = &mentionedByID
   537  	}
   538  
   539  	ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
   540  	if err != nil {
   541  		ctx.Error(http.StatusInternalServerError, "SearchIssues", err)
   542  		return
   543  	}
   544  	issues, err := issues_model.GetIssuesByIDs(ctx, ids, true)
   545  	if err != nil {
   546  		ctx.Error(http.StatusInternalServerError, "FindIssuesByIDs", err)
   547  		return
   548  	}
   549  
   550  	ctx.SetLinkHeader(int(total), listOptions.PageSize)
   551  	ctx.SetTotalCountHeader(total)
   552  	ctx.JSON(http.StatusOK, convert.ToAPIIssueList(ctx, issues))
   553  }
   554  
   555  func getUserIDForFilter(ctx *context.APIContext, queryName string) int64 {
   556  	userName := ctx.FormString(queryName)
   557  	if len(userName) == 0 {
   558  		return 0
   559  	}
   560  
   561  	user, err := user_model.GetUserByName(ctx, userName)
   562  	if user_model.IsErrUserNotExist(err) {
   563  		ctx.NotFound(err)
   564  		return 0
   565  	}
   566  
   567  	if err != nil {
   568  		ctx.InternalServerError(err)
   569  		return 0
   570  	}
   571  
   572  	return user.ID
   573  }
   574  
   575  // GetIssue get an issue of a repository
   576  func GetIssue(ctx *context.APIContext) {
   577  	// swagger:operation GET /repos/{owner}/{repo}/issues/{index} issue issueGetIssue
   578  	// ---
   579  	// summary: Get an issue
   580  	// produces:
   581  	// - application/json
   582  	// parameters:
   583  	// - name: owner
   584  	//   in: path
   585  	//   description: owner of the repo
   586  	//   type: string
   587  	//   required: true
   588  	// - name: repo
   589  	//   in: path
   590  	//   description: name of the repo
   591  	//   type: string
   592  	//   required: true
   593  	// - name: index
   594  	//   in: path
   595  	//   description: index of the issue to get
   596  	//   type: integer
   597  	//   format: int64
   598  	//   required: true
   599  	// responses:
   600  	//   "200":
   601  	//     "$ref": "#/responses/Issue"
   602  	//   "404":
   603  	//     "$ref": "#/responses/notFound"
   604  
   605  	issue, err := issues_model.GetIssueWithAttrsByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
   606  	if err != nil {
   607  		if issues_model.IsErrIssueNotExist(err) {
   608  			ctx.NotFound()
   609  		} else {
   610  			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
   611  		}
   612  		return
   613  	}
   614  	if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) {
   615  		ctx.NotFound()
   616  		return
   617  	}
   618  	ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, issue))
   619  }
   620  
   621  // CreateIssue create an issue of a repository
   622  func CreateIssue(ctx *context.APIContext) {
   623  	// swagger:operation POST /repos/{owner}/{repo}/issues issue issueCreateIssue
   624  	// ---
   625  	// summary: Create an issue. If using deadline only the date will be taken into account, and time of day ignored.
   626  	// consumes:
   627  	// - application/json
   628  	// produces:
   629  	// - application/json
   630  	// parameters:
   631  	// - name: owner
   632  	//   in: path
   633  	//   description: owner of the repo
   634  	//   type: string
   635  	//   required: true
   636  	// - name: repo
   637  	//   in: path
   638  	//   description: name of the repo
   639  	//   type: string
   640  	//   required: true
   641  	// - name: body
   642  	//   in: body
   643  	//   schema:
   644  	//     "$ref": "#/definitions/CreateIssueOption"
   645  	// responses:
   646  	//   "201":
   647  	//     "$ref": "#/responses/Issue"
   648  	//   "403":
   649  	//     "$ref": "#/responses/forbidden"
   650  	//   "404":
   651  	//     "$ref": "#/responses/notFound"
   652  	//   "412":
   653  	//     "$ref": "#/responses/error"
   654  	//   "422":
   655  	//     "$ref": "#/responses/validationError"
   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); err != nil {
   711  		if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
   712  			ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
   713  			return
   714  		}
   715  		ctx.Error(http.StatusInternalServerError, "NewIssue", err)
   716  		return
   717  	}
   718  
   719  	if form.Closed {
   720  		if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", true); err != nil {
   721  			if issues_model.IsErrDependenciesLeft(err) {
   722  				ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies")
   723  				return
   724  			}
   725  			ctx.Error(http.StatusInternalServerError, "ChangeStatus", err)
   726  			return
   727  		}
   728  	}
   729  
   730  	// Refetch from database to assign some automatic values
   731  	issue, err = issues_model.GetIssueByID(ctx, issue.ID)
   732  	if err != nil {
   733  		ctx.Error(http.StatusInternalServerError, "GetIssueByID", err)
   734  		return
   735  	}
   736  	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, issue))
   737  }
   738  
   739  // EditIssue modify an issue of a repository
   740  func EditIssue(ctx *context.APIContext) {
   741  	// swagger:operation PATCH /repos/{owner}/{repo}/issues/{index} issue issueEditIssue
   742  	// ---
   743  	// summary: Edit an issue. If using deadline only the date will be taken into account, and time of day ignored.
   744  	// consumes:
   745  	// - application/json
   746  	// produces:
   747  	// - application/json
   748  	// parameters:
   749  	// - name: owner
   750  	//   in: path
   751  	//   description: owner of the repo
   752  	//   type: string
   753  	//   required: true
   754  	// - name: repo
   755  	//   in: path
   756  	//   description: name of the repo
   757  	//   type: string
   758  	//   required: true
   759  	// - name: index
   760  	//   in: path
   761  	//   description: index of the issue to edit
   762  	//   type: integer
   763  	//   format: int64
   764  	//   required: true
   765  	// - name: body
   766  	//   in: body
   767  	//   schema:
   768  	//     "$ref": "#/definitions/EditIssueOption"
   769  	// responses:
   770  	//   "201":
   771  	//     "$ref": "#/responses/Issue"
   772  	//   "403":
   773  	//     "$ref": "#/responses/forbidden"
   774  	//   "404":
   775  	//     "$ref": "#/responses/notFound"
   776  	//   "412":
   777  	//     "$ref": "#/responses/error"
   778  
   779  	form := web.GetForm(ctx).(*api.EditIssueOption)
   780  	issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
   781  	if err != nil {
   782  		if issues_model.IsErrIssueNotExist(err) {
   783  			ctx.NotFound()
   784  		} else {
   785  			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
   786  		}
   787  		return
   788  	}
   789  	issue.Repo = ctx.Repo.Repository
   790  	canWrite := ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
   791  
   792  	err = issue.LoadAttributes(ctx)
   793  	if err != nil {
   794  		ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
   795  		return
   796  	}
   797  
   798  	if !issue.IsPoster(ctx.Doer.ID) && !canWrite {
   799  		ctx.Status(http.StatusForbidden)
   800  		return
   801  	}
   802  
   803  	oldTitle := issue.Title
   804  	if len(form.Title) > 0 {
   805  		issue.Title = form.Title
   806  	}
   807  	if form.Body != nil {
   808  		issue.Content = *form.Body
   809  	}
   810  	if form.Ref != nil {
   811  		err = issue_service.ChangeIssueRef(ctx, issue, ctx.Doer, *form.Ref)
   812  		if err != nil {
   813  			ctx.Error(http.StatusInternalServerError, "UpdateRef", err)
   814  			return
   815  		}
   816  	}
   817  
   818  	// Update or remove the deadline, only if set and allowed
   819  	if (form.Deadline != nil || form.RemoveDeadline != nil) && canWrite {
   820  		var deadlineUnix timeutil.TimeStamp
   821  
   822  		if (form.RemoveDeadline == nil || !*form.RemoveDeadline) && !form.Deadline.IsZero() {
   823  			deadline := time.Date(form.Deadline.Year(), form.Deadline.Month(), form.Deadline.Day(),
   824  				23, 59, 59, 0, form.Deadline.Location())
   825  			deadlineUnix = timeutil.TimeStamp(deadline.Unix())
   826  		}
   827  
   828  		if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil {
   829  			ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err)
   830  			return
   831  		}
   832  		issue.DeadlineUnix = deadlineUnix
   833  	}
   834  
   835  	// Add/delete assignees
   836  
   837  	// Deleting is done the GitHub way (quote from their api documentation):
   838  	// https://developer.github.com/v3/issues/#edit-an-issue
   839  	// "assignees" (array): Logins for Users to assign to this issue.
   840  	// Pass one or more user logins to replace the set of assignees on this Issue.
   841  	// Send an empty array ([]) to clear all assignees from the Issue.
   842  
   843  	if canWrite && (form.Assignees != nil || form.Assignee != nil) {
   844  		oneAssignee := ""
   845  		if form.Assignee != nil {
   846  			oneAssignee = *form.Assignee
   847  		}
   848  
   849  		err = issue_service.UpdateAssignees(ctx, issue, oneAssignee, form.Assignees, ctx.Doer)
   850  		if err != nil {
   851  			ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err)
   852  			return
   853  		}
   854  	}
   855  
   856  	if canWrite && form.Milestone != nil &&
   857  		issue.MilestoneID != *form.Milestone {
   858  		oldMilestoneID := issue.MilestoneID
   859  		issue.MilestoneID = *form.Milestone
   860  		if err = issue_service.ChangeMilestoneAssign(issue, ctx.Doer, oldMilestoneID); err != nil {
   861  			ctx.Error(http.StatusInternalServerError, "ChangeMilestoneAssign", err)
   862  			return
   863  		}
   864  	}
   865  	if form.State != nil {
   866  		if issue.IsPull {
   867  			if pr, err := issue.GetPullRequest(); err != nil {
   868  				ctx.Error(http.StatusInternalServerError, "GetPullRequest", err)
   869  				return
   870  			} else if pr.HasMerged {
   871  				ctx.Error(http.StatusPreconditionFailed, "MergedPRState", "cannot change state of this pull request, it was already merged")
   872  				return
   873  			}
   874  		}
   875  		issue.IsClosed = api.StateClosed == api.StateType(*form.State)
   876  	}
   877  	statusChangeComment, titleChanged, err := issues_model.UpdateIssueByAPI(ctx, issue, ctx.Doer)
   878  	if err != nil {
   879  		if issues_model.IsErrDependenciesLeft(err) {
   880  			ctx.Error(http.StatusPreconditionFailed, "DependenciesLeft", "cannot close this issue because it still has open dependencies")
   881  			return
   882  		}
   883  		ctx.Error(http.StatusInternalServerError, "UpdateIssueByAPI", err)
   884  		return
   885  	}
   886  
   887  	if titleChanged {
   888  		notify_service.IssueChangeTitle(ctx, ctx.Doer, issue, oldTitle)
   889  	}
   890  
   891  	if statusChangeComment != nil {
   892  		notify_service.IssueChangeStatus(ctx, ctx.Doer, "", issue, statusChangeComment, issue.IsClosed)
   893  	}
   894  
   895  	// Refetch from database to assign some automatic values
   896  	issue, err = issues_model.GetIssueByID(ctx, issue.ID)
   897  	if err != nil {
   898  		ctx.InternalServerError(err)
   899  		return
   900  	}
   901  	if err = issue.LoadMilestone(ctx); err != nil {
   902  		ctx.InternalServerError(err)
   903  		return
   904  	}
   905  	ctx.JSON(http.StatusCreated, convert.ToAPIIssue(ctx, issue))
   906  }
   907  
   908  func DeleteIssue(ctx *context.APIContext) {
   909  	// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index} issue issueDelete
   910  	// ---
   911  	// summary: Delete an issue
   912  	// parameters:
   913  	// - name: owner
   914  	//   in: path
   915  	//   description: owner of the repo
   916  	//   type: string
   917  	//   required: true
   918  	// - name: repo
   919  	//   in: path
   920  	//   description: name of the repo
   921  	//   type: string
   922  	//   required: true
   923  	// - name: index
   924  	//   in: path
   925  	//   description: index of issue to delete
   926  	//   type: integer
   927  	//   format: int64
   928  	//   required: true
   929  	// responses:
   930  	//   "204":
   931  	//     "$ref": "#/responses/empty"
   932  	//   "403":
   933  	//     "$ref": "#/responses/forbidden"
   934  	//   "404":
   935  	//     "$ref": "#/responses/notFound"
   936  	issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
   937  	if err != nil {
   938  		if issues_model.IsErrIssueNotExist(err) {
   939  			ctx.NotFound(err)
   940  		} else {
   941  			ctx.Error(http.StatusInternalServerError, "GetIssueByID", err)
   942  		}
   943  		return
   944  	}
   945  
   946  	if err = issue_service.DeleteIssue(ctx, ctx.Doer, ctx.Repo.GitRepo, issue); err != nil {
   947  		ctx.Error(http.StatusInternalServerError, "DeleteIssueByID", err)
   948  		return
   949  	}
   950  
   951  	ctx.Status(http.StatusNoContent)
   952  }
   953  
   954  // UpdateIssueDeadline updates an issue deadline
   955  func UpdateIssueDeadline(ctx *context.APIContext) {
   956  	// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/deadline issue issueEditIssueDeadline
   957  	// ---
   958  	// 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.
   959  	// consumes:
   960  	// - application/json
   961  	// produces:
   962  	// - application/json
   963  	// parameters:
   964  	// - name: owner
   965  	//   in: path
   966  	//   description: owner of the repo
   967  	//   type: string
   968  	//   required: true
   969  	// - name: repo
   970  	//   in: path
   971  	//   description: name of the repo
   972  	//   type: string
   973  	//   required: true
   974  	// - name: index
   975  	//   in: path
   976  	//   description: index of the issue to create or update a deadline on
   977  	//   type: integer
   978  	//   format: int64
   979  	//   required: true
   980  	// - name: body
   981  	//   in: body
   982  	//   schema:
   983  	//     "$ref": "#/definitions/EditDeadlineOption"
   984  	// responses:
   985  	//   "201":
   986  	//     "$ref": "#/responses/IssueDeadline"
   987  	//   "403":
   988  	//     "$ref": "#/responses/forbidden"
   989  	//   "404":
   990  	//     "$ref": "#/responses/notFound"
   991  	form := web.GetForm(ctx).(*api.EditDeadlineOption)
   992  	issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
   993  	if err != nil {
   994  		if issues_model.IsErrIssueNotExist(err) {
   995  			ctx.NotFound()
   996  		} else {
   997  			ctx.Error(http.StatusInternalServerError, "GetIssueByIndex", err)
   998  		}
   999  		return
  1000  	}
  1001  
  1002  	if !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
  1003  		ctx.Error(http.StatusForbidden, "", "Not repo writer")
  1004  		return
  1005  	}
  1006  
  1007  	var deadlineUnix timeutil.TimeStamp
  1008  	var deadline time.Time
  1009  	if form.Deadline != nil && !form.Deadline.IsZero() {
  1010  		deadline = time.Date(form.Deadline.Year(), form.Deadline.Month(), form.Deadline.Day(),
  1011  			23, 59, 59, 0, time.Local)
  1012  		deadlineUnix = timeutil.TimeStamp(deadline.Unix())
  1013  	}
  1014  
  1015  	if err := issues_model.UpdateIssueDeadline(ctx, issue, deadlineUnix, ctx.Doer); err != nil {
  1016  		ctx.Error(http.StatusInternalServerError, "UpdateIssueDeadline", err)
  1017  		return
  1018  	}
  1019  
  1020  	ctx.JSON(http.StatusCreated, api.IssueDeadline{Deadline: &deadline})
  1021  }