2016-03-13 23:20:22 -04:00
// Copyright 2016 The Gogs Authors. All rights reserved.
2018-06-19 17:15:11 +02:00
// Copyright 2018 The Gitea Authors. All rights reserved.
2016-03-13 23:20:22 -04:00
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package repo
import (
"fmt"
2018-07-17 23:23:58 +02:00
"net/http"
2020-04-30 06:15:39 +02:00
"strconv"
2016-03-13 23:20:22 -04:00
"strings"
2019-01-01 14:56:47 -03:00
"time"
2016-03-13 23:20:22 -04:00
2016-11-10 17:24:48 +01:00
"code.gitea.io/gitea/models"
2021-09-24 19:32:56 +08:00
"code.gitea.io/gitea/models/db"
2021-11-10 03:57:58 +08:00
"code.gitea.io/gitea/models/unit"
2021-11-24 17:49:20 +08:00
user_model "code.gitea.io/gitea/models/user"
2016-11-10 17:24:48 +01:00
"code.gitea.io/gitea/modules/context"
2020-02-29 03:49:50 +01:00
"code.gitea.io/gitea/modules/convert"
2019-02-21 08:54:05 +08:00
issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
2020-05-16 22:05:19 +01:00
"code.gitea.io/gitea/modules/notification"
2016-11-10 17:24:48 +01:00
"code.gitea.io/gitea/modules/setting"
2019-08-23 09:40:30 -07:00
api "code.gitea.io/gitea/modules/structs"
2019-08-15 22:46:21 +08:00
"code.gitea.io/gitea/modules/timeutil"
2017-01-24 21:43:02 -05:00
"code.gitea.io/gitea/modules/util"
2021-01-26 23:36:53 +08:00
"code.gitea.io/gitea/modules/web"
2020-01-24 19:00:29 +00:00
"code.gitea.io/gitea/routers/api/v1/utils"
2019-09-30 21:50:44 +08:00
issue_service "code.gitea.io/gitea/services/issue"
2016-03-13 23:20:22 -04:00
)
2019-10-31 00:06:10 -05:00
// SearchIssues searches for issues across the repositories that the user has access to
func SearchIssues ( ctx * context . APIContext ) {
// swagger:operation GET /repos/issues/search issue issueSearchIssues
// ---
// summary: Search for issues across the repositories that the user has access to
// produces:
// - application/json
// parameters:
// - name: state
// in: query
// description: whether issue is open or closed
// type: string
// - name: labels
// in: query
// description: comma separated list of labels. Fetch only issues that have any of this labels. Non existent labels are discarded
// type: string
2021-06-17 08:40:59 +02:00
// - name: milestones
// in: query
// description: comma separated list of milestone names. Fetch only issues that have any of this milestones. Non existent are discarded
// type: string
2019-10-31 00:06:10 -05:00
// - name: q
// in: query
// description: search string
// type: string
// - name: priority_repo_id
// in: query
// description: repository to prioritize in the results
// type: integer
// format: int64
2020-01-19 14:43:38 +08:00
// - name: type
// in: query
// description: filter by type (issues / pulls) if set
// type: string
2020-11-23 21:49:36 +01:00
// - name: since
// in: query
// description: Only show notifications updated after the given time. This is a timestamp in RFC 3339 format
// type: string
// format: date-time
// required: false
// - name: before
// in: query
// description: Only show notifications updated before the given time. This is a timestamp in RFC 3339 format
// type: string
// format: date-time
// required: false
// - name: assigned
// in: query
// description: filter (issues / pulls) assigned to you, default is false
// type: boolean
// - name: created
// in: query
// description: filter (issues / pulls) created by you, default is false
// type: boolean
// - name: mentioned
// in: query
// description: filter (issues / pulls) mentioning you, default is false
// type: boolean
2021-01-17 17:34:19 +01:00
// - name: review_requested
// in: query
// description: filter pulls requesting your review, default is false
// type: boolean
2021-08-13 22:47:25 +02:00
// - name: owner
// in: query
// description: filter by owner
// type: string
// - name: team
// in: query
// description: filter by team (requires organization owner parameter to be provided)
// type: string
2020-01-24 19:00:29 +00:00
// - name: page
// in: query
2020-11-23 21:49:36 +01:00
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
2020-01-24 19:00:29 +00:00
// type: integer
2019-10-31 00:06:10 -05:00
// responses:
// "200":
// "$ref": "#/responses/IssueList"
2019-12-20 18:07:12 +01:00
2020-11-23 21:49:36 +01:00
before , since , err := utils . GetQueryBeforeSince ( ctx )
if err != nil {
ctx . Error ( http . StatusUnprocessableEntity , "GetQueryBeforeSince" , err )
return
}
2019-10-31 00:06:10 -05:00
var isClosed util . OptionalBool
2021-08-11 02:31:13 +02:00
switch ctx . FormString ( "state" ) {
2019-10-31 00:06:10 -05:00
case "closed" :
isClosed = util . OptionalBoolTrue
case "all" :
isClosed = util . OptionalBoolNone
default :
isClosed = util . OptionalBoolFalse
}
// find repos user can access (for issue search)
2020-01-12 07:35:11 +01:00
opts := & models . SearchRepoOptions {
Private : false ,
AllPublic : true ,
TopicOnly : false ,
Collaborate : util . OptionalBoolNone ,
2020-04-01 00:14:46 -04:00
// This needs to be a column that is not nil in fixtures or
// MySQL will return different results when sorting by null in some cases
2021-11-24 17:49:20 +08:00
OrderBy : db . SearchOrderByAlphabetically ,
2020-04-01 00:14:46 -04:00
Actor : ctx . User ,
2020-01-12 07:35:11 +01:00
}
if ctx . IsSigned {
opts . Private = true
opts . AllLimited = true
}
2021-08-13 22:47:25 +02:00
if ctx . FormString ( "owner" ) != "" {
2021-11-24 17:49:20 +08:00
owner , err := user_model . GetUserByName ( ctx . FormString ( "owner" ) )
2021-08-13 22:47:25 +02:00
if err != nil {
2021-11-24 17:49:20 +08:00
if user_model . IsErrUserNotExist ( err ) {
2021-08-13 22:47:25 +02:00
ctx . Error ( http . StatusBadRequest , "Owner not found" , err )
} else {
ctx . Error ( http . StatusInternalServerError , "GetUserByName" , err )
}
return
}
opts . OwnerID = owner . ID
opts . AllLimited = false
opts . AllPublic = false
opts . Collaborate = util . OptionalBoolFalse
}
if ctx . FormString ( "team" ) != "" {
if ctx . FormString ( "owner" ) == "" {
ctx . Error ( http . StatusBadRequest , "" , "Owner organisation is required for filtering on team" )
return
}
team , err := models . GetTeam ( opts . OwnerID , ctx . FormString ( "team" ) )
if err != nil {
if models . IsErrTeamNotExist ( err ) {
ctx . Error ( http . StatusBadRequest , "Team not found" , err )
} else {
ctx . Error ( http . StatusInternalServerError , "GetUserByName" , err )
}
return
}
opts . TeamID = team . ID
}
2020-03-30 13:30:39 +08:00
2021-03-29 18:12:21 +01:00
repoIDs , _ , err := models . SearchRepositoryIDs ( opts )
if err != nil {
ctx . Error ( http . StatusInternalServerError , "SearchRepositoryByName" , err )
return
2019-10-31 00:06:10 -05:00
}
var issues [ ] * models . Issue
2020-09-25 08:30:40 +09:00
var filteredCount int64
2019-10-31 00:06:10 -05:00
2021-08-11 17:08:52 +02:00
keyword := ctx . FormTrim ( "q" )
2019-10-31 00:06:10 -05:00
if strings . IndexByte ( keyword , 0 ) >= 0 {
keyword = ""
}
var issueIDs [ ] int64
if len ( keyword ) > 0 && len ( repoIDs ) > 0 {
2022-01-27 10:30:51 +02:00
if issueIDs , err = issue_indexer . SearchIssuesByKeyword ( ctx , repoIDs , keyword ) ; err != nil {
2020-09-25 08:30:40 +09:00
ctx . Error ( http . StatusInternalServerError , "SearchIssuesByKeyword" , err )
return
}
2019-10-31 00:06:10 -05:00
}
2020-01-19 14:43:38 +08:00
var isPull util . OptionalBool
2021-08-11 02:31:13 +02:00
switch ctx . FormString ( "type" ) {
2020-01-19 14:43:38 +08:00
case "pulls" :
isPull = util . OptionalBoolTrue
case "issues" :
isPull = util . OptionalBoolFalse
default :
isPull = util . OptionalBoolNone
}
2021-08-11 17:08:52 +02:00
labels := ctx . FormTrim ( "labels" )
2020-03-30 13:30:39 +08:00
var includedLabelNames [ ] string
if len ( labels ) > 0 {
includedLabelNames = strings . Split ( labels , "," )
}
2021-08-11 17:08:52 +02:00
milestones := ctx . FormTrim ( "milestones" )
2021-06-17 08:40:59 +02:00
var includedMilestones [ ] string
if len ( milestones ) > 0 {
includedMilestones = strings . Split ( milestones , "," )
}
2020-11-23 21:49:36 +01:00
// this api is also used in UI,
// so the default limit is set to fit UI needs
2021-07-29 09:42:15 +08:00
limit := ctx . FormInt ( "limit" )
2020-11-23 21:49:36 +01:00
if limit == 0 {
limit = setting . UI . IssuePagingNum
} else if limit > setting . API . MaxResponseItems {
limit = setting . API . MaxResponseItems
}
2019-10-31 00:06:10 -05:00
// Only fetch the issues if we either don't have a keyword or the search returned issues
// This would otherwise return all issues if no issues were found by the search.
2021-06-17 08:40:59 +02:00
if len ( keyword ) == 0 || len ( issueIDs ) > 0 || len ( includedLabelNames ) > 0 || len ( includedMilestones ) > 0 {
2020-09-25 08:30:40 +09:00
issuesOpt := & models . IssuesOptions {
2021-09-24 19:32:56 +08:00
ListOptions : db . ListOptions {
2021-07-29 09:42:15 +08:00
Page : ctx . FormInt ( "page" ) ,
2020-11-23 21:49:36 +01:00
PageSize : limit ,
2020-01-24 19:00:29 +00:00
} ,
2020-03-30 13:30:39 +08:00
RepoIDs : repoIDs ,
IsClosed : isClosed ,
IssueIDs : issueIDs ,
IncludedLabelNames : includedLabelNames ,
2021-06-17 08:40:59 +02:00
IncludeMilestones : includedMilestones ,
2020-03-30 13:30:39 +08:00
SortType : "priorityrepo" ,
2021-07-29 09:42:15 +08:00
PriorityRepoID : ctx . FormInt64 ( "priority_repo_id" ) ,
2020-03-30 13:30:39 +08:00
IsPull : isPull ,
2020-11-23 21:49:36 +01:00
UpdatedBeforeUnix : before ,
UpdatedAfterUnix : since ,
}
2022-03-20 21:04:51 +00:00
ctxUserID := int64 ( 0 )
if ctx . IsSigned {
ctxUserID = ctx . User . ID
}
2021-01-17 17:34:19 +01:00
// Filter for: Created by User, Assigned to User, Mentioning User, Review of User Requested
2021-07-29 09:42:15 +08:00
if ctx . FormBool ( "created" ) {
2022-03-20 21:04:51 +00:00
issuesOpt . PosterID = ctxUserID
2020-11-23 21:49:36 +01:00
}
2021-07-29 09:42:15 +08:00
if ctx . FormBool ( "assigned" ) {
2022-03-20 21:04:51 +00:00
issuesOpt . AssigneeID = ctxUserID
2020-11-23 21:49:36 +01:00
}
2021-07-29 09:42:15 +08:00
if ctx . FormBool ( "mentioned" ) {
2022-03-20 21:04:51 +00:00
issuesOpt . MentionedID = ctxUserID
2020-09-25 08:30:40 +09:00
}
2021-07-29 09:42:15 +08:00
if ctx . FormBool ( "review_requested" ) {
2022-03-20 21:04:51 +00:00
issuesOpt . ReviewRequestedID = ctxUserID
2021-01-17 17:34:19 +01:00
}
2019-10-31 00:06:10 -05:00
2020-09-25 08:30:40 +09:00
if issues , err = models . Issues ( issuesOpt ) ; err != nil {
ctx . Error ( http . StatusInternalServerError , "Issues" , err )
return
}
2021-09-24 19:32:56 +08:00
issuesOpt . ListOptions = db . ListOptions {
2020-09-25 08:30:40 +09:00
Page : - 1 ,
}
if filteredCount , err = models . CountIssues ( issuesOpt ) ; err != nil {
ctx . Error ( http . StatusInternalServerError , "CountIssues" , err )
return
}
2019-10-31 00:06:10 -05:00
}
2020-09-25 08:30:40 +09:00
ctx . SetLinkHeader ( int ( filteredCount ) , setting . UI . IssuePagingNum )
2021-08-12 14:43:08 +02:00
ctx . SetTotalCountHeader ( filteredCount )
2020-02-29 03:49:50 +01:00
ctx . JSON ( http . StatusOK , convert . ToAPIIssueList ( issues ) )
2019-10-31 00:06:10 -05:00
}
2016-11-24 15:04:31 +08:00
// ListIssues list the issues of a repository
2016-03-13 23:20:22 -04:00
func ListIssues ( ctx * context . APIContext ) {
2017-11-12 23:02:25 -08:00
// swagger:operation GET /repos/{owner}/{repo}/issues issue issueListIssues
// ---
// summary: List a repository's issues
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: state
// in: query
// description: whether issue is open or closed
// type: string
2020-06-27 22:32:28 +02:00
// enum: [closed, open, all]
2019-02-04 16:20:44 +01:00
// - name: labels
// in: query
// description: comma separated list of labels. Fetch only issues that have any of this labels. Non existent labels are discarded
// type: string
2018-03-07 11:00:56 +01:00
// - name: q
// in: query
// description: search string
// type: string
2020-01-20 20:00:32 +08:00
// - name: type
// in: query
// description: filter by type (issues / pulls) if set
// type: string
2020-06-27 22:32:28 +02:00
// enum: [issues, pulls]
2020-04-30 06:15:39 +02:00
// - name: milestones
// in: query
// 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
// type: string
2021-06-17 00:33:37 +02:00
// - name: since
// in: query
2021-09-18 03:01:50 +02:00
// description: Only show items updated after the given time. This is a timestamp in RFC 3339 format
2021-06-17 00:33:37 +02:00
// type: string
// format: date-time
// required: false
// - name: before
// in: query
2021-09-18 03:01:50 +02:00
// description: Only show items updated before the given time. This is a timestamp in RFC 3339 format
2021-06-17 00:33:37 +02:00
// type: string
// format: date-time
// required: false
// - name: created_by
// in: query
2021-09-18 03:01:50 +02:00
// description: Only show items which were created by the the given user
2021-06-17 00:33:37 +02:00
// type: string
// - name: assigned_by
// in: query
2021-09-18 03:01:50 +02:00
// description: Only show items for which the given user is assigned
2021-06-17 00:33:37 +02:00
// type: string
// - name: mentioned_by
// in: query
2021-09-18 03:01:50 +02:00
// description: Only show items in which the given user was mentioned
2021-06-17 00:33:37 +02:00
// type: string
2020-01-24 19:00:29 +00:00
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
2020-06-09 06:57:38 +02:00
// description: page size of results
2020-01-24 19:00:29 +00:00
// type: integer
2017-11-12 23:02:25 -08:00
// responses:
// "200":
// "$ref": "#/responses/IssueList"
2021-06-17 00:33:37 +02:00
before , since , err := utils . GetQueryBeforeSince ( ctx )
if err != nil {
ctx . Error ( http . StatusUnprocessableEntity , "GetQueryBeforeSince" , err )
return
}
2019-12-20 18:07:12 +01:00
2017-06-25 10:51:07 -04:00
var isClosed util . OptionalBool
2021-08-11 02:31:13 +02:00
switch ctx . FormString ( "state" ) {
2017-06-25 10:51:07 -04:00
case "closed" :
isClosed = util . OptionalBoolTrue
case "all" :
isClosed = util . OptionalBoolNone
default :
isClosed = util . OptionalBoolFalse
2016-10-07 19:17:27 +02:00
}
2018-03-07 11:00:56 +01:00
var issues [ ] * models . Issue
2020-09-25 08:30:40 +09:00
var filteredCount int64
2018-03-07 11:00:56 +01:00
2021-08-11 17:08:52 +02:00
keyword := ctx . FormTrim ( "q" )
2018-03-07 11:00:56 +01:00
if strings . IndexByte ( keyword , 0 ) >= 0 {
keyword = ""
}
var issueIDs [ ] int64
2019-02-04 16:20:44 +01:00
var labelIDs [ ] int64
2018-03-07 11:00:56 +01:00
if len ( keyword ) > 0 {
2022-01-27 10:30:51 +02:00
issueIDs , err = issue_indexer . SearchIssuesByKeyword ( ctx , [ ] int64 { ctx . Repo . Repository . ID } , keyword )
2020-09-25 08:30:40 +09:00
if err != nil {
ctx . Error ( http . StatusInternalServerError , "SearchIssuesByKeyword" , err )
return
}
2018-03-07 11:00:56 +01:00
}
2021-08-11 02:31:13 +02:00
if splitted := strings . Split ( ctx . FormString ( "labels" ) , "," ) ; len ( splitted ) > 0 {
2019-02-04 16:20:44 +01:00
labelIDs , err = models . GetLabelIDsInRepoByNames ( ctx . Repo . Repository . ID , splitted )
if err != nil {
2019-12-20 18:07:12 +01:00
ctx . Error ( http . StatusInternalServerError , "GetLabelIDsInRepoByNames" , err )
2019-02-04 16:20:44 +01:00
return
}
}
2020-04-30 06:15:39 +02:00
var mileIDs [ ] int64
2021-08-11 02:31:13 +02:00
if part := strings . Split ( ctx . FormString ( "milestones" ) , "," ) ; len ( part ) > 0 {
2020-04-30 06:15:39 +02:00
for i := range part {
// uses names and fall back to ids
// non existent milestones are discarded
mile , err := models . GetMilestoneByRepoIDANDName ( ctx . Repo . Repository . ID , part [ i ] )
if err == nil {
mileIDs = append ( mileIDs , mile . ID )
continue
}
if ! models . IsErrMilestoneNotExist ( err ) {
ctx . Error ( http . StatusInternalServerError , "GetMilestoneByRepoIDANDName" , err )
return
}
id , err := strconv . ParseInt ( part [ i ] , 10 , 64 )
if err != nil {
continue
}
mile , err = models . GetMilestoneByRepoID ( ctx . Repo . Repository . ID , id )
if err == nil {
mileIDs = append ( mileIDs , mile . ID )
continue
}
if models . IsErrMilestoneNotExist ( err ) {
continue
}
ctx . Error ( http . StatusInternalServerError , "GetMilestoneByRepoID" , err )
}
}
2020-01-24 19:00:29 +00:00
listOptions := utils . GetListOptions ( ctx )
2020-01-20 20:00:32 +08:00
var isPull util . OptionalBool
2021-08-11 02:31:13 +02:00
switch ctx . FormString ( "type" ) {
2020-01-20 20:00:32 +08:00
case "pulls" :
isPull = util . OptionalBoolTrue
case "issues" :
isPull = util . OptionalBoolFalse
default :
isPull = util . OptionalBoolNone
}
2021-06-17 00:33:37 +02:00
// FIXME: we should be more efficient here
createdByID := getUserIDForFilter ( ctx , "created_by" )
if ctx . Written ( ) {
return
}
assignedByID := getUserIDForFilter ( ctx , "assigned_by" )
if ctx . Written ( ) {
return
}
mentionedByID := getUserIDForFilter ( ctx , "mentioned_by" )
if ctx . Written ( ) {
return
}
2018-03-07 11:00:56 +01:00
// Only fetch the issues if we either don't have a keyword or the search returned issues
// This would otherwise return all issues if no issues were found by the search.
2019-02-04 16:20:44 +01:00
if len ( keyword ) == 0 || len ( issueIDs ) > 0 || len ( labelIDs ) > 0 {
2020-09-25 08:30:40 +09:00
issuesOpt := & models . IssuesOptions {
2021-06-17 00:33:37 +02:00
ListOptions : listOptions ,
RepoIDs : [ ] int64 { ctx . Repo . Repository . ID } ,
IsClosed : isClosed ,
IssueIDs : issueIDs ,
LabelIDs : labelIDs ,
MilestoneIDs : mileIDs ,
IsPull : isPull ,
UpdatedBeforeUnix : before ,
UpdatedAfterUnix : since ,
PosterID : createdByID ,
AssigneeID : assignedByID ,
MentionedID : mentionedByID ,
2020-09-25 08:30:40 +09:00
}
2018-03-07 11:00:56 +01:00
2020-09-25 08:30:40 +09:00
if issues , err = models . Issues ( issuesOpt ) ; err != nil {
ctx . Error ( http . StatusInternalServerError , "Issues" , err )
return
}
2021-09-24 19:32:56 +08:00
issuesOpt . ListOptions = db . ListOptions {
2020-09-25 08:30:40 +09:00
Page : - 1 ,
}
if filteredCount , err = models . CountIssues ( issuesOpt ) ; err != nil {
ctx . Error ( http . StatusInternalServerError , "CountIssues" , err )
return
}
2016-03-13 23:20:22 -04:00
}
2020-09-25 08:30:40 +09:00
ctx . SetLinkHeader ( int ( filteredCount ) , listOptions . PageSize )
2021-08-12 14:43:08 +02:00
ctx . SetTotalCountHeader ( filteredCount )
2020-02-29 03:49:50 +01:00
ctx . JSON ( http . StatusOK , convert . ToAPIIssueList ( issues ) )
2016-03-13 23:20:22 -04:00
}
2021-06-17 00:33:37 +02:00
func getUserIDForFilter ( ctx * context . APIContext , queryName string ) int64 {
2021-08-11 02:31:13 +02:00
userName := ctx . FormString ( queryName )
2021-06-17 00:33:37 +02:00
if len ( userName ) == 0 {
return 0
}
2021-11-24 17:49:20 +08:00
user , err := user_model . GetUserByName ( userName )
if user_model . IsErrUserNotExist ( err ) {
2021-06-17 00:33:37 +02:00
ctx . NotFound ( err )
return 0
}
if err != nil {
ctx . InternalServerError ( err )
return 0
}
return user . ID
}
2016-11-24 15:04:31 +08:00
// GetIssue get an issue of a repository
2016-03-13 23:20:22 -04:00
func GetIssue ( ctx * context . APIContext ) {
2018-01-04 01:31:40 -05:00
// swagger:operation GET /repos/{owner}/{repo}/issues/{index} issue issueGetIssue
2017-11-12 23:02:25 -08:00
// ---
2018-01-04 01:31:40 -05:00
// summary: Get an issue
2017-11-12 23:02:25 -08:00
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
2018-01-04 01:31:40 -05:00
// - name: index
2017-11-12 23:02:25 -08:00
// in: path
2018-01-04 01:31:40 -05:00
// description: index of the issue to get
2017-11-12 23:02:25 -08:00
// type: integer
2018-10-21 04:40:42 +01:00
// format: int64
2017-11-12 23:02:25 -08:00
// required: true
// responses:
// "200":
// "$ref": "#/responses/Issue"
2019-12-20 18:07:12 +01:00
// "404":
// "$ref": "#/responses/notFound"
2019-02-19 11:07:19 -06:00
issue , err := models . GetIssueWithAttrsByIndex ( ctx . Repo . Repository . ID , ctx . ParamsInt64 ( ":index" ) )
2016-03-13 23:20:22 -04:00
if err != nil {
if models . IsErrIssueNotExist ( err ) {
2019-03-18 21:29:43 -05:00
ctx . NotFound ( )
2016-03-13 23:20:22 -04:00
} else {
2019-12-20 18:07:12 +01:00
ctx . Error ( http . StatusInternalServerError , "GetIssueByIndex" , err )
2016-03-13 23:20:22 -04:00
}
return
}
2020-02-29 03:49:50 +01:00
ctx . JSON ( http . StatusOK , convert . ToAPIIssue ( issue ) )
2016-03-13 23:20:22 -04:00
}
2016-11-24 15:04:31 +08:00
// CreateIssue create an issue of a repository
2021-01-26 23:36:53 +08:00
func CreateIssue ( ctx * context . APIContext ) {
2017-11-12 23:02:25 -08:00
// swagger:operation POST /repos/{owner}/{repo}/issues issue issueCreateIssue
// ---
2019-01-01 14:56:47 -03:00
// summary: Create an issue. If using deadline only the date will be taken into account, and time of day ignored.
2017-11-12 23:02:25 -08:00
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateIssueOption"
// responses:
// "201":
// "$ref": "#/responses/Issue"
2019-12-20 18:07:12 +01:00
// "403":
// "$ref": "#/responses/forbidden"
// "412":
// "$ref": "#/responses/error"
// "422":
// "$ref": "#/responses/validationError"
2021-01-26 23:36:53 +08:00
form := web . GetForm ( ctx ) . ( * api . CreateIssueOption )
2019-08-15 22:46:21 +08:00
var deadlineUnix timeutil . TimeStamp
2021-11-10 03:57:58 +08:00
if form . Deadline != nil && ctx . Repo . CanWrite ( unit . TypeIssues ) {
2019-08-15 22:46:21 +08:00
deadlineUnix = timeutil . TimeStamp ( form . Deadline . Unix ( ) )
2018-05-01 21:05:28 +02:00
}
2016-03-13 23:20:22 -04:00
issue := & models . Issue {
2018-05-01 21:05:28 +02:00
RepoID : ctx . Repo . Repository . ID ,
2018-12-13 23:55:43 +08:00
Repo : ctx . Repo . Repository ,
2018-05-01 21:05:28 +02:00
Title : form . Title ,
PosterID : ctx . User . ID ,
Poster : ctx . User ,
Content : form . Body ,
2020-12-15 18:38:10 +00:00
Ref : form . Ref ,
2018-05-01 21:05:28 +02:00
DeadlineUnix : deadlineUnix ,
2016-03-13 23:20:22 -04:00
}
2022-01-20 18:46:10 +01:00
assigneeIDs := make ( [ ] int64 , 0 )
2018-06-19 17:15:11 +02:00
var err error
2021-11-10 03:57:58 +08:00
if ctx . Repo . CanWrite ( unit . TypeIssues ) {
2018-06-19 17:15:11 +02:00
issue . MilestoneID = form . Milestone
assigneeIDs , err = models . MakeIDsFromAPIAssigneesToAdd ( form . Assignee , form . Assignees )
if err != nil {
2021-11-24 17:49:20 +08:00
if user_model . IsErrUserNotExist ( err ) {
2019-12-20 18:07:12 +01:00
ctx . Error ( http . StatusUnprocessableEntity , "" , fmt . Sprintf ( "Assignee does not exist: [name: %s]" , err ) )
2018-06-19 17:15:11 +02:00
} else {
2019-12-20 18:07:12 +01:00
ctx . Error ( http . StatusInternalServerError , "AddAssigneeByName" , err )
2018-06-19 17:15:11 +02:00
}
return
2016-03-13 23:20:22 -04:00
}
2019-10-25 16:46:37 +02:00
// Check if the passed assignees is assignable
for _ , aID := range assigneeIDs {
2021-11-24 17:49:20 +08:00
assignee , err := user_model . GetUserByID ( aID )
2019-10-25 16:46:37 +02:00
if err != nil {
2019-12-20 18:07:12 +01:00
ctx . Error ( http . StatusInternalServerError , "GetUserByID" , err )
2019-10-25 16:46:37 +02:00
return
}
valid , err := models . CanBeAssigned ( assignee , ctx . Repo . Repository , false )
if err != nil {
2019-12-20 18:07:12 +01:00
ctx . Error ( http . StatusInternalServerError , "canBeAssigned" , err )
2019-10-25 16:46:37 +02:00
return
}
if ! valid {
2019-12-20 18:07:12 +01:00
ctx . Error ( http . StatusUnprocessableEntity , "canBeAssigned" , models . ErrUserDoesNotHaveAccessToRepo { UserID : aID , RepoName : ctx . Repo . Repository . Name } )
2019-10-25 16:46:37 +02:00
return
}
}
2018-06-19 17:15:11 +02:00
} else {
// setting labels is not allowed if user is not a writer
form . Labels = make ( [ ] int64 , 0 )
2016-03-13 23:20:22 -04:00
}
2019-10-29 00:45:43 +08:00
if err := issue_service . NewIssue ( ctx . Repo . Repository , issue , form . Labels , nil , assigneeIDs ) ; err != nil {
2018-05-09 18:29:04 +02:00
if models . IsErrUserDoesNotHaveAccessToRepo ( err ) {
2019-12-20 18:07:12 +01:00
ctx . Error ( http . StatusBadRequest , "UserDoesNotHaveAccessToRepo" , err )
2018-05-09 18:29:04 +02:00
return
}
2019-12-20 18:07:12 +01:00
ctx . Error ( http . StatusInternalServerError , "NewIssue" , err )
2016-03-13 23:20:22 -04:00
return
}
2016-05-27 18:23:39 -07:00
if form . Closed {
2019-10-28 13:26:46 +08:00
if err := issue_service . ChangeStatus ( issue , ctx . User , true ) ; err != nil {
2018-07-17 23:23:58 +02:00
if models . IsErrDependenciesLeft ( err ) {
ctx . Error ( http . StatusPreconditionFailed , "DependenciesLeft" , "cannot close this issue because it still has open dependencies" )
return
}
2019-12-20 18:07:12 +01:00
ctx . Error ( http . StatusInternalServerError , "ChangeStatus" , err )
2016-05-27 18:23:39 -07:00
return
}
}
2016-03-13 23:20:22 -04:00
// Refetch from database to assign some automatic values
issue , err = models . GetIssueByID ( issue . ID )
if err != nil {
2019-12-20 18:07:12 +01:00
ctx . Error ( http . StatusInternalServerError , "GetIssueByID" , err )
2016-03-13 23:20:22 -04:00
return
}
2020-02-29 03:49:50 +01:00
ctx . JSON ( http . StatusCreated , convert . ToAPIIssue ( issue ) )
2016-03-13 23:20:22 -04:00
}
2016-11-24 15:04:31 +08:00
// EditIssue modify an issue of a repository
2021-01-26 23:36:53 +08:00
func EditIssue ( ctx * context . APIContext ) {
2018-01-04 01:31:40 -05:00
// swagger:operation PATCH /repos/{owner}/{repo}/issues/{index} issue issueEditIssue
2017-11-12 23:02:25 -08:00
// ---
2019-01-01 14:56:47 -03:00
// summary: Edit an issue. If using deadline only the date will be taken into account, and time of day ignored.
2017-11-12 23:02:25 -08:00
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
2018-01-04 01:31:40 -05:00
// - name: index
2017-11-12 23:02:25 -08:00
// in: path
2018-01-04 01:31:40 -05:00
// description: index of the issue to edit
2017-11-12 23:02:25 -08:00
// type: integer
2018-10-21 04:40:42 +01:00
// format: int64
2017-11-12 23:02:25 -08:00
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditIssueOption"
// responses:
// "201":
// "$ref": "#/responses/Issue"
2019-12-20 18:07:12 +01:00
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
// "412":
// "$ref": "#/responses/error"
2021-01-26 23:36:53 +08:00
form := web . GetForm ( ctx ) . ( * api . EditIssueOption )
2016-03-13 23:20:22 -04:00
issue , err := models . GetIssueByIndex ( ctx . Repo . Repository . ID , ctx . ParamsInt64 ( ":index" ) )
if err != nil {
if models . IsErrIssueNotExist ( err ) {
2019-03-18 21:29:43 -05:00
ctx . NotFound ( )
2016-03-13 23:20:22 -04:00
} else {
2019-12-20 18:07:12 +01:00
ctx . Error ( http . StatusInternalServerError , "GetIssueByIndex" , err )
2016-03-13 23:20:22 -04:00
}
return
}
2018-12-13 23:55:43 +08:00
issue . Repo = ctx . Repo . Repository
2020-01-20 20:00:32 +08:00
canWrite := ctx . Repo . CanWriteIssuesOrPulls ( issue . IsPull )
2016-03-13 23:20:22 -04:00
2019-04-23 12:07:12 -05:00
err = issue . LoadAttributes ( )
if err != nil {
2019-12-20 18:07:12 +01:00
ctx . Error ( http . StatusInternalServerError , "LoadAttributes" , err )
2019-04-23 12:07:12 -05:00
return
}
2020-01-20 20:00:32 +08:00
if ! issue . IsPoster ( ctx . User . ID ) && ! canWrite {
2019-12-20 18:07:12 +01:00
ctx . Status ( http . StatusForbidden )
2016-03-13 23:20:22 -04:00
return
}
2020-05-16 22:05:19 +01:00
oldTitle := issue . Title
2016-03-13 23:20:22 -04:00
if len ( form . Title ) > 0 {
2016-08-14 03:32:24 -07:00
issue . Title = form . Title
2016-03-13 23:20:22 -04:00
}
if form . Body != nil {
issue . Content = * form . Body
}
2020-12-15 18:38:10 +00:00
if form . Ref != nil {
err = issue_service . ChangeIssueRef ( issue , ctx . User , * form . Ref )
if err != nil {
ctx . Error ( http . StatusInternalServerError , "UpdateRef" , err )
return
}
}
2016-03-13 23:20:22 -04:00
2019-11-03 15:46:32 +01:00
// Update or remove the deadline, only if set and allowed
2020-01-20 20:00:32 +08:00
if ( form . Deadline != nil || form . RemoveDeadline != nil ) && canWrite {
2019-11-03 15:46:32 +01:00
var deadlineUnix timeutil . TimeStamp
if ( form . RemoveDeadline == nil || ! * form . RemoveDeadline ) && ! form . Deadline . IsZero ( ) {
deadline := time . Date ( form . Deadline . Year ( ) , form . Deadline . Month ( ) , form . Deadline . Day ( ) ,
23 , 59 , 59 , 0 , form . Deadline . Location ( ) )
deadlineUnix = timeutil . TimeStamp ( deadline . Unix ( ) )
}
2019-10-28 00:35:20 +01:00
if err := models . UpdateIssueDeadline ( issue , deadlineUnix , ctx . User ) ; err != nil {
2019-12-20 18:07:12 +01:00
ctx . Error ( http . StatusInternalServerError , "UpdateIssueDeadline" , err )
2019-10-28 00:35:20 +01:00
return
}
issue . DeadlineUnix = deadlineUnix
2018-05-01 21:05:28 +02:00
}
2018-05-09 18:29:04 +02:00
// Add/delete assignees
2019-03-09 16:15:45 -05:00
// Deleting is done the GitHub way (quote from their api documentation):
2018-05-09 18:29:04 +02:00
// https://developer.github.com/v3/issues/#edit-an-issue
// "assignees" (array): Logins for Users to assign to this issue.
// Pass one or more user logins to replace the set of assignees on this Issue.
// Send an empty array ([]) to clear all assignees from the Issue.
2020-01-20 20:00:32 +08:00
if canWrite && ( form . Assignees != nil || form . Assignee != nil ) {
2018-05-09 18:29:04 +02:00
oneAssignee := ""
if form . Assignee != nil {
oneAssignee = * form . Assignee
2016-03-13 23:20:22 -04:00
}
2019-10-25 16:46:37 +02:00
err = issue_service . UpdateAssignees ( issue , oneAssignee , form . Assignees , ctx . User )
2018-05-09 18:29:04 +02:00
if err != nil {
2019-12-20 18:07:12 +01:00
ctx . Error ( http . StatusInternalServerError , "UpdateAssignees" , err )
2016-03-13 23:20:22 -04:00
return
}
}
2018-05-09 18:29:04 +02:00
2020-01-20 20:00:32 +08:00
if canWrite && form . Milestone != nil &&
2016-03-13 23:20:22 -04:00
issue . MilestoneID != * form . Milestone {
2016-08-15 18:40:32 -07:00
oldMilestoneID := issue . MilestoneID
2016-03-13 23:20:22 -04:00
issue . MilestoneID = * form . Milestone
2019-11-02 11:33:20 +08:00
if err = issue_service . ChangeMilestoneAssign ( issue , ctx . User , oldMilestoneID ) ; err != nil {
2019-12-20 18:07:12 +01:00
ctx . Error ( http . StatusInternalServerError , "ChangeMilestoneAssign" , err )
2016-03-13 23:20:22 -04:00
return
}
}
2016-08-23 18:09:32 +02:00
if form . State != nil {
2021-10-03 05:11:17 +02:00
if issue . IsPull {
if pr , err := issue . GetPullRequest ( ) ; err != nil {
ctx . Error ( http . StatusInternalServerError , "GetPullRequest" , err )
return
} else if pr . HasMerged {
ctx . Error ( http . StatusPreconditionFailed , "MergedPRState" , "cannot change state of this pull request, it was already merged" )
return
}
}
2021-04-09 09:40:34 +02:00
issue . IsClosed = api . StateClosed == api . StateType ( * form . State )
2020-05-16 22:05:19 +01:00
}
statusChangeComment , titleChanged , err := models . UpdateIssueByAPI ( issue , ctx . User )
if err != nil {
if models . IsErrDependenciesLeft ( err ) {
ctx . Error ( http . StatusPreconditionFailed , "DependenciesLeft" , "cannot close this issue because it still has open dependencies" )
2016-08-23 18:09:32 +02:00
return
}
2020-05-16 22:05:19 +01:00
ctx . Error ( http . StatusInternalServerError , "UpdateIssueByAPI" , err )
return
}
if titleChanged {
notification . NotifyIssueChangeTitle ( ctx . User , issue , oldTitle )
}
if statusChangeComment != nil {
notification . NotifyIssueChangeStatus ( ctx . User , issue , statusChangeComment , issue . IsClosed )
2016-08-23 18:09:32 +02:00
}
2016-03-13 23:20:22 -04:00
// Refetch from database to assign some automatic values
issue , err = models . GetIssueByID ( issue . ID )
if err != nil {
2020-01-01 23:51:10 +01:00
ctx . InternalServerError ( err )
return
}
if err = issue . LoadMilestone ( ) ; err != nil {
ctx . InternalServerError ( err )
2016-03-13 23:20:22 -04:00
return
}
2020-02-29 03:49:50 +01:00
ctx . JSON ( http . StatusCreated , convert . ToAPIIssue ( issue ) )
2016-03-13 23:20:22 -04:00
}
2018-07-16 14:43:00 +02:00
2022-03-01 01:20:15 +01:00
func DeleteIssue ( ctx * context . APIContext ) {
// swagger:operation DELETE /repos/{owner}/{repo}/issues/{index} issue issueDelete
// ---
// summary: Delete an issue
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of issue to delete
// type: integer
// format: int64
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
issue , err := models . GetIssueByIndex ( ctx . Repo . Repository . ID , ctx . ParamsInt64 ( ":index" ) )
if err != nil {
if models . IsErrIssueNotExist ( err ) {
ctx . NotFound ( err )
} else {
ctx . Error ( http . StatusInternalServerError , "GetIssueByID" , err )
}
return
}
if err = issue_service . DeleteIssue ( ctx . User , ctx . Repo . GitRepo , issue ) ; err != nil {
ctx . Error ( http . StatusInternalServerError , "DeleteIssueByID" , err )
return
}
ctx . Status ( http . StatusNoContent )
}
2018-07-16 14:43:00 +02:00
// UpdateIssueDeadline updates an issue deadline
2021-01-26 23:36:53 +08:00
func UpdateIssueDeadline ( ctx * context . APIContext ) {
2018-07-16 14:43:00 +02:00
// swagger:operation POST /repos/{owner}/{repo}/issues/{index}/deadline issue issueEditIssueDeadline
// ---
2019-01-01 14:56:47 -03:00
// 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.
2018-07-16 14:43:00 +02:00
// consumes:
// - application/json
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: index
// in: path
// description: index of the issue to create or update a deadline on
// type: integer
2018-10-21 04:40:42 +01:00
// format: int64
2018-07-16 14:43:00 +02:00
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/EditDeadlineOption"
// responses:
// "201":
// "$ref": "#/responses/IssueDeadline"
// "403":
2019-12-20 18:07:12 +01:00
// "$ref": "#/responses/forbidden"
2018-07-16 14:43:00 +02:00
// "404":
2019-12-20 18:07:12 +01:00
// "$ref": "#/responses/notFound"
2021-01-26 23:36:53 +08:00
form := web . GetForm ( ctx ) . ( * api . EditDeadlineOption )
2018-07-16 14:43:00 +02:00
issue , err := models . GetIssueByIndex ( ctx . Repo . Repository . ID , ctx . ParamsInt64 ( ":index" ) )
if err != nil {
if models . IsErrIssueNotExist ( err ) {
2019-03-18 21:29:43 -05:00
ctx . NotFound ( )
2018-07-16 14:43:00 +02:00
} else {
2019-12-20 18:07:12 +01:00
ctx . Error ( http . StatusInternalServerError , "GetIssueByIndex" , err )
2018-07-16 14:43:00 +02:00
}
return
}
2020-01-20 20:00:32 +08:00
if ! ctx . Repo . CanWriteIssuesOrPulls ( issue . IsPull ) {
2019-12-20 18:07:12 +01:00
ctx . Error ( http . StatusForbidden , "" , "Not repo writer" )
2018-07-16 14:43:00 +02:00
return
}
2019-08-15 22:46:21 +08:00
var deadlineUnix timeutil . TimeStamp
2019-01-01 14:56:47 -03:00
var deadline time . Time
2018-07-16 14:43:00 +02:00
if form . Deadline != nil && ! form . Deadline . IsZero ( ) {
2019-01-01 14:56:47 -03:00
deadline = time . Date ( form . Deadline . Year ( ) , form . Deadline . Month ( ) , form . Deadline . Day ( ) ,
2020-06-06 00:51:10 +02:00
23 , 59 , 59 , 0 , time . Local )
2019-08-15 22:46:21 +08:00
deadlineUnix = timeutil . TimeStamp ( deadline . Unix ( ) )
2018-07-16 14:43:00 +02:00
}
if err := models . UpdateIssueDeadline ( issue , deadlineUnix , ctx . User ) ; err != nil {
2019-12-20 18:07:12 +01:00
ctx . Error ( http . StatusInternalServerError , "UpdateIssueDeadline" , err )
2018-07-16 14:43:00 +02:00
return
}
2019-12-20 18:07:12 +01:00
ctx . JSON ( http . StatusCreated , api . IssueDeadline { Deadline : & deadline } )
2018-07-16 14:43:00 +02:00
}