aboutsummaryrefslogtreecommitdiffstats
path: root/routers/web/repo/issue.go
diff options
context:
space:
mode:
Diffstat (limited to 'routers/web/repo/issue.go')
-rw-r--r--routers/web/repo/issue.go430
1 files changed, 241 insertions, 189 deletions
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index f901ebaf63..7bddabd10a 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -150,7 +150,6 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
mentionedID int64
reviewRequestedID int64
reviewedID int64
- forceEmpty bool
)
if ctx.IsSigned {
@@ -191,31 +190,14 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
keyword = ""
}
- var issueIDs []int64
- if len(keyword) > 0 {
- issueIDs, err = issue_indexer.SearchIssuesByKeyword(ctx, []int64{repo.ID}, keyword, ctx.FormString("state"))
- if err != nil {
- if issue_indexer.IsAvailable(ctx) {
- ctx.ServerError("issueIndexer.Search", err)
- return
- }
- ctx.Data["IssueIndexerUnavailable"] = true
- }
- if len(issueIDs) == 0 {
- forceEmpty = true
- }
- }
-
var mileIDs []int64
if milestoneID > 0 || milestoneID == db.NoConditionID { // -1 to get those issues which have no any milestone assigned
mileIDs = []int64{milestoneID}
}
var issueStats *issues_model.IssueStats
- if forceEmpty {
- issueStats = &issues_model.IssueStats{}
- } else {
- issueStats, err = issues_model.GetIssueStats(&issues_model.IssuesOptions{
+ {
+ statsOpts := &issues_model.IssuesOptions{
RepoIDs: []int64{repo.ID},
LabelIDs: labelIDs,
MilestoneIDs: mileIDs,
@@ -226,12 +208,34 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
ReviewRequestedID: reviewRequestedID,
ReviewedID: reviewedID,
IsPull: isPullOption,
- IssueIDs: issueIDs,
- })
- if err != nil {
- ctx.ServerError("GetIssueStats", err)
- return
+ IssueIDs: nil,
+ }
+ if keyword != "" {
+ allIssueIDs, err := issueIDsFromSearch(ctx, keyword, statsOpts)
+ if err != nil {
+ if issue_indexer.IsAvailable(ctx) {
+ ctx.ServerError("issueIDsFromSearch", err)
+ return
+ }
+ ctx.Data["IssueIndexerUnavailable"] = true
+ return
+ }
+ statsOpts.IssueIDs = allIssueIDs
}
+ if keyword != "" && len(statsOpts.IssueIDs) == 0 {
+ // So it did search with the keyword, but no issue found.
+ // Just set issueStats to empty.
+ issueStats = &issues_model.IssueStats{}
+ } else {
+ // So it did search with the keyword, and found some issues. It needs to get issueStats of these issues.
+ // Or the keyword is empty, so it doesn't need issueIDs as filter, just get issueStats with statsOpts.
+ issueStats, err = issues_model.GetIssueStats(statsOpts)
+ if err != nil {
+ ctx.ServerError("GetIssueStats", err)
+ return
+ }
+ }
+
}
isShowClosed := ctx.FormString("state") == "closed"
@@ -253,12 +257,10 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
}
pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5)
- var issues []*issues_model.Issue
- if forceEmpty {
- issues = []*issues_model.Issue{}
- } else {
- issues, err = issues_model.Issues(ctx, &issues_model.IssuesOptions{
- ListOptions: db.ListOptions{
+ var issues issues_model.IssueList
+ {
+ ids, err := issueIDsFromSearch(ctx, keyword, &issues_model.IssuesOptions{
+ Paginator: &db.ListOptions{
Page: pager.Paginater.Current(),
PageSize: setting.UI.IssuePagingNum,
},
@@ -274,16 +276,23 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
IsPull: isPullOption,
LabelIDs: labelIDs,
SortType: sortType,
- IssueIDs: issueIDs,
})
if err != nil {
- ctx.ServerError("Issues", err)
+ if issue_indexer.IsAvailable(ctx) {
+ ctx.ServerError("issueIDsFromSearch", err)
+ return
+ }
+ ctx.Data["IssueIndexerUnavailable"] = true
+ return
+ }
+ issues, err = issues_model.GetIssuesByIDs(ctx, ids, true)
+ if err != nil {
+ ctx.ServerError("GetIssuesByIDs", err)
return
}
}
- issueList := issues_model.IssueList(issues)
- approvalCounts, err := issueList.GetApprovalCounts(ctx)
+ approvalCounts, err := issues.GetApprovalCounts(ctx)
if err != nil {
ctx.ServerError("ApprovalCounts", err)
return
@@ -306,6 +315,11 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
return
}
+ if err := issues.LoadAttributes(ctx); err != nil {
+ ctx.ServerError("issues.LoadAttributes", err)
+ return
+ }
+
ctx.Data["Issues"] = issues
ctx.Data["CommitLastStatus"] = lastStatus
ctx.Data["CommitStatuses"] = commitStatuses
@@ -429,6 +443,14 @@ func issues(ctx *context.Context, milestoneID, projectID int64, isPullOption uti
ctx.Data["Page"] = pager
}
+func issueIDsFromSearch(ctx *context.Context, keyword string, opts *issues_model.IssuesOptions) ([]int64, error) {
+ ids, _, err := issue_indexer.SearchIssues(ctx, issue_indexer.ToSearchOptions(keyword, opts))
+ if err != nil {
+ return nil, fmt.Errorf("SearchIssues: %w", err)
+ }
+ return ids, nil
+}
+
// Issues render issues page
func Issues(ctx *context.Context) {
isPullList := ctx.Params(":type") == "pulls"
@@ -2419,74 +2441,73 @@ func SearchIssues(ctx *context.Context) {
isClosed = util.OptionalBoolFalse
}
- // find repos user can access (for issue search)
- opts := &repo_model.SearchRepoOptions{
- Private: false,
- AllPublic: true,
- TopicOnly: false,
- Collaborate: util.OptionalBoolNone,
- // 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
- OrderBy: db.SearchOrderByAlphabetically,
- Actor: ctx.Doer,
- }
- if ctx.IsSigned {
- opts.Private = true
- opts.AllLimited = true
- }
- if ctx.FormString("owner") != "" {
- owner, err := user_model.GetUserByName(ctx, ctx.FormString("owner"))
- if err != nil {
- if user_model.IsErrUserNotExist(err) {
- ctx.Error(http.StatusBadRequest, "Owner not found", err.Error())
- } else {
- ctx.Error(http.StatusInternalServerError, "GetUserByName", err.Error())
+ var (
+ repoIDs []int64
+ allPublic bool
+ )
+ {
+ // find repos user can access (for issue search)
+ opts := &repo_model.SearchRepoOptions{
+ Private: false,
+ AllPublic: true,
+ TopicOnly: false,
+ Collaborate: util.OptionalBoolNone,
+ // 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
+ OrderBy: db.SearchOrderByAlphabetically,
+ Actor: ctx.Doer,
+ }
+ if ctx.IsSigned {
+ opts.Private = true
+ opts.AllLimited = true
+ }
+ if ctx.FormString("owner") != "" {
+ owner, err := user_model.GetUserByName(ctx, ctx.FormString("owner"))
+ if err != nil {
+ if user_model.IsErrUserNotExist(err) {
+ ctx.Error(http.StatusBadRequest, "Owner not found", err.Error())
+ } else {
+ ctx.Error(http.StatusInternalServerError, "GetUserByName", err.Error())
+ }
+ return
}
- return
+ opts.OwnerID = owner.ID
+ opts.AllLimited = false
+ opts.AllPublic = false
+ opts.Collaborate = util.OptionalBoolFalse
}
- 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
+ if ctx.FormString("team") != "" {
+ if ctx.FormString("owner") == "" {
+ ctx.Error(http.StatusBadRequest, "", "Owner organisation is required for filtering on team")
+ return
+ }
+ team, err := organization.GetTeam(ctx, opts.OwnerID, ctx.FormString("team"))
+ if err != nil {
+ if organization.IsErrTeamNotExist(err) {
+ ctx.Error(http.StatusBadRequest, "Team not found", err.Error())
+ } else {
+ ctx.Error(http.StatusInternalServerError, "GetUserByName", err.Error())
+ }
+ return
+ }
+ opts.TeamID = team.ID
+ }
+
+ if opts.AllPublic {
+ allPublic = true
+ opts.AllPublic = false // set it false to avoid returning too many repos, we could filter by indexer
}
- team, err := organization.GetTeam(ctx, opts.OwnerID, ctx.FormString("team"))
+ repoIDs, _, err = repo_model.SearchRepositoryIDs(opts)
if err != nil {
- if organization.IsErrTeamNotExist(err) {
- ctx.Error(http.StatusBadRequest, "Team not found", err.Error())
- } else {
- ctx.Error(http.StatusInternalServerError, "GetUserByName", err.Error())
- }
+ ctx.Error(http.StatusInternalServerError, "SearchRepositoryIDs", err.Error())
return
}
- opts.TeamID = team.ID
- }
-
- repoCond := repo_model.SearchRepositoryCondition(opts)
- repoIDs, _, err := repo_model.SearchRepositoryIDs(opts)
- if err != nil {
- ctx.Error(http.StatusInternalServerError, "SearchRepositoryIDs", err.Error())
- return
}
- var issues []*issues_model.Issue
- var filteredCount int64
-
keyword := ctx.FormTrim("q")
if strings.IndexByte(keyword, 0) >= 0 {
keyword = ""
}
- var issueIDs []int64
- if len(keyword) > 0 && len(repoIDs) > 0 {
- if issueIDs, err = issue_indexer.SearchIssuesByKeyword(ctx, repoIDs, keyword, ctx.FormString("state")); err != nil {
- ctx.Error(http.StatusInternalServerError, "SearchIssuesByKeyword", err.Error())
- return
- }
- }
var isPull util.OptionalBool
switch ctx.FormString("type") {
@@ -2498,19 +2519,39 @@ func SearchIssues(ctx *context.Context) {
isPull = util.OptionalBoolNone
}
- labels := ctx.FormTrim("labels")
- var includedLabelNames []string
- if len(labels) > 0 {
- includedLabelNames = strings.Split(labels, ",")
+ var includedAnyLabels []int64
+ {
+
+ labels := ctx.FormTrim("labels")
+ var includedLabelNames []string
+ if len(labels) > 0 {
+ includedLabelNames = strings.Split(labels, ",")
+ }
+ includedAnyLabels, err = issues_model.GetLabelIDsByNames(ctx, includedLabelNames)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "GetLabelIDsByNames", err.Error())
+ return
+ }
}
- milestones := ctx.FormTrim("milestones")
- var includedMilestones []string
- if len(milestones) > 0 {
- includedMilestones = strings.Split(milestones, ",")
+ var includedMilestones []int64
+ {
+ milestones := ctx.FormTrim("milestones")
+ var includedMilestoneNames []string
+ if len(milestones) > 0 {
+ includedMilestoneNames = strings.Split(milestones, ",")
+ }
+ includedMilestones, err = issues_model.GetMilestoneIDsByNames(ctx, includedMilestoneNames)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "GetMilestoneIDsByNames", err.Error())
+ return
+ }
}
- projectID := ctx.FormInt64("project")
+ var projectID *int64
+ if v := ctx.FormInt64("project"); v > 0 {
+ projectID = &v
+ }
// this api is also used in UI,
// so the default limit is set to fit UI needs
@@ -2521,64 +2562,64 @@ func SearchIssues(ctx *context.Context) {
limit = setting.API.MaxResponseItems
}
- // 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.
- if len(keyword) == 0 || len(issueIDs) > 0 || len(includedLabelNames) > 0 || len(includedMilestones) > 0 {
- issuesOpt := &issues_model.IssuesOptions{
- ListOptions: db.ListOptions{
- Page: ctx.FormInt("page"),
- PageSize: limit,
- },
- RepoCond: repoCond,
- IsClosed: isClosed,
- IssueIDs: issueIDs,
- IncludedLabelNames: includedLabelNames,
- IncludeMilestones: includedMilestones,
- ProjectID: projectID,
- SortType: "priorityrepo",
- PriorityRepoID: ctx.FormInt64("priority_repo_id"),
- IsPull: isPull,
- UpdatedBeforeUnix: before,
- UpdatedAfterUnix: since,
- }
-
- ctxUserID := int64(0)
- if ctx.IsSigned {
- ctxUserID = ctx.Doer.ID
- }
+ searchOpt := &issue_indexer.SearchOptions{
+ Paginator: &db.ListOptions{
+ Page: ctx.FormInt("page"),
+ PageSize: limit,
+ },
+ Keyword: keyword,
+ RepoIDs: repoIDs,
+ AllPublic: allPublic,
+ IsPull: isPull,
+ IsClosed: isClosed,
+ IncludedAnyLabelIDs: includedAnyLabels,
+ MilestoneIDs: includedMilestones,
+ ProjectID: projectID,
+ SortBy: issue_indexer.SortByCreatedDesc,
+ }
- // Filter for: Created by User, Assigned to User, Mentioning User, Review of User Requested
+ if since != 0 {
+ searchOpt.UpdatedAfterUnix = &since
+ }
+ if before != 0 {
+ searchOpt.UpdatedBeforeUnix = &before
+ }
+
+ if ctx.IsSigned {
+ ctxUserID := ctx.Doer.ID
if ctx.FormBool("created") {
- issuesOpt.PosterID = ctxUserID
+ searchOpt.PosterID = &ctxUserID
}
if ctx.FormBool("assigned") {
- issuesOpt.AssigneeID = ctxUserID
+ searchOpt.AssigneeID = &ctxUserID
}
if ctx.FormBool("mentioned") {
- issuesOpt.MentionedID = ctxUserID
+ searchOpt.MentionID = &ctxUserID
}
if ctx.FormBool("review_requested") {
- issuesOpt.ReviewRequestedID = ctxUserID
+ searchOpt.ReviewRequestedID = &ctxUserID
}
if ctx.FormBool("reviewed") {
- issuesOpt.ReviewedID = ctxUserID
+ searchOpt.ReviewedID = &ctxUserID
}
+ }
- if issues, err = issues_model.Issues(ctx, issuesOpt); err != nil {
- ctx.Error(http.StatusInternalServerError, "Issues", err.Error())
- return
- }
+ // FIXME: It's unsupported to sort by priority repo when searching by indexer,
+ // it's indeed an regression, but I think it is worth to support filtering by indexer first.
+ _ = ctx.FormInt64("priority_repo_id")
- issuesOpt.ListOptions = db.ListOptions{
- Page: -1,
- }
- if filteredCount, err = issues_model.CountIssues(ctx, issuesOpt); err != nil {
- ctx.Error(http.StatusInternalServerError, "CountIssues", err.Error())
- return
- }
+ ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "SearchIssues", err.Error())
+ return
+ }
+ issues, err := issues_model.GetIssuesByIDs(ctx, ids, true)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "FindIssuesByIDs", err.Error())
+ return
}
- ctx.SetTotalCountHeader(filteredCount)
+ ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, convert.ToIssueList(ctx, issues))
}
@@ -2620,23 +2661,12 @@ func ListIssues(ctx *context.Context) {
isClosed = util.OptionalBoolFalse
}
- var issues []*issues_model.Issue
- var filteredCount int64
-
keyword := ctx.FormTrim("q")
if strings.IndexByte(keyword, 0) >= 0 {
keyword = ""
}
- var issueIDs []int64
- var labelIDs []int64
- if len(keyword) > 0 {
- issueIDs, err = issue_indexer.SearchIssuesByKeyword(ctx, []int64{ctx.Repo.Repository.ID}, keyword, ctx.FormString("state"))
- if err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
- return
- }
- }
+ var labelIDs []int64
if splitted := strings.Split(ctx.FormString("labels"), ","); len(splitted) > 0 {
labelIDs, err = issues_model.GetLabelIDsInRepoByNames(ctx.Repo.Repository.ID, splitted)
if err != nil {
@@ -2675,11 +2705,9 @@ func ListIssues(ctx *context.Context) {
}
}
- projectID := ctx.FormInt64("project")
-
- listOptions := db.ListOptions{
- Page: ctx.FormInt("page"),
- PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
+ var projectID *int64
+ if v := ctx.FormInt64("project"); v > 0 {
+ projectID = &v
}
var isPull util.OptionalBool
@@ -2706,40 +2734,64 @@ func ListIssues(ctx *context.Context) {
return
}
- // 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.
- if len(keyword) == 0 || len(issueIDs) > 0 || len(labelIDs) > 0 {
- issuesOpt := &issues_model.IssuesOptions{
- ListOptions: listOptions,
- RepoIDs: []int64{ctx.Repo.Repository.ID},
- IsClosed: isClosed,
- IssueIDs: issueIDs,
- LabelIDs: labelIDs,
- MilestoneIDs: mileIDs,
- ProjectID: projectID,
- IsPull: isPull,
- UpdatedBeforeUnix: before,
- UpdatedAfterUnix: since,
- PosterID: createdByID,
- AssigneeID: assignedByID,
- MentionedID: mentionedByID,
+ searchOpt := &issue_indexer.SearchOptions{
+ Paginator: &db.ListOptions{
+ Page: ctx.FormInt("page"),
+ PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
+ },
+ Keyword: keyword,
+ RepoIDs: []int64{ctx.Repo.Repository.ID},
+ IsPull: isPull,
+ IsClosed: isClosed,
+ ProjectBoardID: projectID,
+ SortBy: issue_indexer.SortByCreatedDesc,
+ }
+ if since != 0 {
+ searchOpt.UpdatedAfterUnix = &since
+ }
+ if before != 0 {
+ searchOpt.UpdatedBeforeUnix = &before
+ }
+ if len(labelIDs) == 1 && labelIDs[0] == 0 {
+ searchOpt.NoLabelOnly = true
+ } else {
+ for _, labelID := range labelIDs {
+ if labelID > 0 {
+ searchOpt.IncludedLabelIDs = append(searchOpt.IncludedLabelIDs, labelID)
+ } else {
+ searchOpt.ExcludedLabelIDs = append(searchOpt.ExcludedLabelIDs, -labelID)
+ }
}
+ }
- if issues, err = issues_model.Issues(ctx, issuesOpt); err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
- return
- }
+ if len(mileIDs) == 1 && mileIDs[0] == db.NoConditionID {
+ searchOpt.MilestoneIDs = []int64{0}
+ } else {
+ searchOpt.MilestoneIDs = mileIDs
+ }
- issuesOpt.ListOptions = db.ListOptions{
- Page: -1,
- }
- if filteredCount, err = issues_model.CountIssues(ctx, issuesOpt); err != nil {
- ctx.Error(http.StatusInternalServerError, err.Error())
- return
- }
+ if createdByID > 0 {
+ searchOpt.PosterID = &createdByID
+ }
+ if assignedByID > 0 {
+ searchOpt.AssigneeID = &assignedByID
+ }
+ if mentionedByID > 0 {
+ searchOpt.MentionID = &mentionedByID
+ }
+
+ ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "SearchIssues", err.Error())
+ return
+ }
+ issues, err := issues_model.GetIssuesByIDs(ctx, ids, true)
+ if err != nil {
+ ctx.Error(http.StatusInternalServerError, "FindIssuesByIDs", err.Error())
+ return
}
- ctx.SetTotalCountHeader(filteredCount)
+ ctx.SetTotalCountHeader(total)
ctx.JSON(http.StatusOK, convert.ToIssueList(ctx, issues))
}