mirror of https://github.com/go-gitea/gitea.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
793 lines
22 KiB
793 lines
22 KiB
// Copyright 2024 The Gitea Authors. All rights reserved. |
|
// SPDX-License-Identifier: MIT |
|
|
|
package repo |
|
|
|
import ( |
|
"bytes" |
|
"maps" |
|
"net/http" |
|
"slices" |
|
"sort" |
|
"strconv" |
|
"strings" |
|
|
|
"code.gitea.io/gitea/models/db" |
|
git_model "code.gitea.io/gitea/models/git" |
|
issues_model "code.gitea.io/gitea/models/issues" |
|
"code.gitea.io/gitea/models/organization" |
|
repo_model "code.gitea.io/gitea/models/repo" |
|
"code.gitea.io/gitea/models/unit" |
|
user_model "code.gitea.io/gitea/models/user" |
|
issue_indexer "code.gitea.io/gitea/modules/indexer/issues" |
|
db_indexer "code.gitea.io/gitea/modules/indexer/issues/db" |
|
"code.gitea.io/gitea/modules/log" |
|
"code.gitea.io/gitea/modules/optional" |
|
"code.gitea.io/gitea/modules/setting" |
|
"code.gitea.io/gitea/modules/util" |
|
"code.gitea.io/gitea/routers/web/shared/issue" |
|
shared_user "code.gitea.io/gitea/routers/web/shared/user" |
|
"code.gitea.io/gitea/services/context" |
|
"code.gitea.io/gitea/services/convert" |
|
issue_service "code.gitea.io/gitea/services/issue" |
|
pull_service "code.gitea.io/gitea/services/pull" |
|
) |
|
|
|
func retrieveProjectsForIssueList(ctx *context.Context, repo *repo_model.Repository) { |
|
ctx.Data["OpenProjects"], ctx.Data["ClosedProjects"] = retrieveProjectsInternal(ctx, repo) |
|
} |
|
|
|
// SearchIssues searches for issues across the repositories that the user has access to |
|
func SearchIssues(ctx *context.Context) { |
|
before, since, err := context.GetQueryBeforeSince(ctx.Base) |
|
if err != nil { |
|
ctx.HTTPError(http.StatusUnprocessableEntity, err.Error()) |
|
return |
|
} |
|
|
|
var isClosed optional.Option[bool] |
|
switch ctx.FormString("state") { |
|
case "closed": |
|
isClosed = optional.Some(true) |
|
case "all": |
|
isClosed = optional.None[bool]() |
|
default: |
|
isClosed = optional.Some(false) |
|
} |
|
|
|
var ( |
|
repoIDs []int64 |
|
allPublic bool |
|
) |
|
{ |
|
// find repos user can access (for issue search) |
|
opts := &repo_model.SearchRepoOptions{ |
|
Private: false, |
|
AllPublic: true, |
|
TopicOnly: false, |
|
Collaborate: optional.None[bool](), |
|
// 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.HTTPError(http.StatusBadRequest, "Owner not found", err.Error()) |
|
} else { |
|
ctx.HTTPError(http.StatusInternalServerError, "GetUserByName", err.Error()) |
|
} |
|
return |
|
} |
|
opts.OwnerID = owner.ID |
|
opts.AllLimited = false |
|
opts.AllPublic = false |
|
opts.Collaborate = optional.Some(false) |
|
} |
|
if ctx.FormString("team") != "" { |
|
if ctx.FormString("owner") == "" { |
|
ctx.HTTPError(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.HTTPError(http.StatusBadRequest, "Team not found", err.Error()) |
|
} else { |
|
ctx.HTTPError(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 |
|
} |
|
repoIDs, _, err = repo_model.SearchRepositoryIDs(ctx, opts) |
|
if err != nil { |
|
ctx.HTTPError(http.StatusInternalServerError, "SearchRepositoryIDs", err.Error()) |
|
return |
|
} |
|
if len(repoIDs) == 0 { |
|
// no repos found, don't let the indexer return all repos |
|
repoIDs = []int64{0} |
|
} |
|
} |
|
|
|
keyword := ctx.FormTrim("q") |
|
if strings.IndexByte(keyword, 0) >= 0 { |
|
keyword = "" |
|
} |
|
|
|
isPull := optional.None[bool]() |
|
switch ctx.FormString("type") { |
|
case "pulls": |
|
isPull = optional.Some(true) |
|
case "issues": |
|
isPull = optional.Some(false) |
|
} |
|
|
|
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.HTTPError(http.StatusInternalServerError, "GetLabelIDsByNames", err.Error()) |
|
return |
|
} |
|
} |
|
|
|
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.HTTPError(http.StatusInternalServerError, "GetMilestoneIDsByNames", err.Error()) |
|
return |
|
} |
|
} |
|
|
|
projectID := optional.None[int64]() |
|
if v := ctx.FormInt64("project"); v > 0 { |
|
projectID = optional.Some(v) |
|
} |
|
|
|
// this api is also used in UI, |
|
// so the default limit is set to fit UI needs |
|
limit := ctx.FormInt("limit") |
|
if limit == 0 { |
|
limit = setting.UI.IssuePagingNum |
|
} else if limit > setting.API.MaxResponseItems { |
|
limit = setting.API.MaxResponseItems |
|
} |
|
|
|
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, |
|
} |
|
|
|
if since != 0 { |
|
searchOpt.UpdatedAfterUnix = optional.Some(since) |
|
} |
|
if before != 0 { |
|
searchOpt.UpdatedBeforeUnix = optional.Some(before) |
|
} |
|
|
|
if ctx.IsSigned { |
|
ctxUserID := ctx.Doer.ID |
|
if ctx.FormBool("created") { |
|
searchOpt.PosterID = strconv.FormatInt(ctxUserID, 10) |
|
} |
|
if ctx.FormBool("assigned") { |
|
searchOpt.AssigneeID = strconv.FormatInt(ctxUserID, 10) |
|
} |
|
if ctx.FormBool("mentioned") { |
|
searchOpt.MentionID = optional.Some(ctxUserID) |
|
} |
|
if ctx.FormBool("review_requested") { |
|
searchOpt.ReviewRequestedID = optional.Some(ctxUserID) |
|
} |
|
if ctx.FormBool("reviewed") { |
|
searchOpt.ReviewedID = optional.Some(ctxUserID) |
|
} |
|
} |
|
|
|
// 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") |
|
|
|
ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt) |
|
if err != nil { |
|
ctx.HTTPError(http.StatusInternalServerError, "SearchIssues", err.Error()) |
|
return |
|
} |
|
issues, err := issues_model.GetIssuesByIDs(ctx, ids, true) |
|
if err != nil { |
|
ctx.HTTPError(http.StatusInternalServerError, "FindIssuesByIDs", err.Error()) |
|
return |
|
} |
|
|
|
ctx.SetTotalCountHeader(total) |
|
ctx.JSON(http.StatusOK, convert.ToIssueList(ctx, ctx.Doer, issues)) |
|
} |
|
|
|
func getUserIDForFilter(ctx *context.Context, queryName string) int64 { |
|
userName := ctx.FormString(queryName) |
|
if len(userName) == 0 { |
|
return 0 |
|
} |
|
|
|
user, err := user_model.GetUserByName(ctx, userName) |
|
if user_model.IsErrUserNotExist(err) { |
|
ctx.NotFound(err) |
|
return 0 |
|
} |
|
|
|
if err != nil { |
|
ctx.HTTPError(http.StatusInternalServerError, err.Error()) |
|
return 0 |
|
} |
|
|
|
return user.ID |
|
} |
|
|
|
// SearchRepoIssuesJSON lists the issues of a repository |
|
// This function was copied from API (decouple the web and API routes), |
|
// it is only used by frontend to search some dependency or related issues |
|
func SearchRepoIssuesJSON(ctx *context.Context) { |
|
before, since, err := context.GetQueryBeforeSince(ctx.Base) |
|
if err != nil { |
|
ctx.HTTPError(http.StatusUnprocessableEntity, err.Error()) |
|
return |
|
} |
|
|
|
var isClosed optional.Option[bool] |
|
switch ctx.FormString("state") { |
|
case "closed": |
|
isClosed = optional.Some(true) |
|
case "all": |
|
isClosed = optional.None[bool]() |
|
default: |
|
isClosed = optional.Some(false) |
|
} |
|
|
|
keyword := ctx.FormTrim("q") |
|
if strings.IndexByte(keyword, 0) >= 0 { |
|
keyword = "" |
|
} |
|
|
|
var mileIDs []int64 |
|
if part := strings.Split(ctx.FormString("milestones"), ","); len(part) > 0 { |
|
for i := range part { |
|
// uses names and fall back to ids |
|
// non-existent milestones are discarded |
|
mile, err := issues_model.GetMilestoneByRepoIDANDName(ctx, ctx.Repo.Repository.ID, part[i]) |
|
if err == nil { |
|
mileIDs = append(mileIDs, mile.ID) |
|
continue |
|
} |
|
if !issues_model.IsErrMilestoneNotExist(err) { |
|
ctx.HTTPError(http.StatusInternalServerError, err.Error()) |
|
return |
|
} |
|
id, err := strconv.ParseInt(part[i], 10, 64) |
|
if err != nil { |
|
continue |
|
} |
|
mile, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, id) |
|
if err == nil { |
|
mileIDs = append(mileIDs, mile.ID) |
|
continue |
|
} |
|
if issues_model.IsErrMilestoneNotExist(err) { |
|
continue |
|
} |
|
ctx.HTTPError(http.StatusInternalServerError, err.Error()) |
|
} |
|
} |
|
|
|
projectID := optional.None[int64]() |
|
if v := ctx.FormInt64("project"); v > 0 { |
|
projectID = optional.Some(v) |
|
} |
|
|
|
isPull := optional.None[bool]() |
|
switch ctx.FormString("type") { |
|
case "pulls": |
|
isPull = optional.Some(true) |
|
case "issues": |
|
isPull = optional.Some(false) |
|
} |
|
|
|
// 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 |
|
} |
|
|
|
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, |
|
ProjectID: projectID, |
|
SortBy: issue_indexer.SortByCreatedDesc, |
|
} |
|
if since != 0 { |
|
searchOpt.UpdatedAfterUnix = optional.Some(since) |
|
} |
|
if before != 0 { |
|
searchOpt.UpdatedBeforeUnix = optional.Some(before) |
|
} |
|
|
|
// TODO: the "labels" query parameter is never used, so no need to handle it |
|
|
|
if len(mileIDs) == 1 && mileIDs[0] == db.NoConditionID { |
|
searchOpt.MilestoneIDs = []int64{0} |
|
} else { |
|
searchOpt.MilestoneIDs = mileIDs |
|
} |
|
|
|
if createdByID > 0 { |
|
searchOpt.PosterID = strconv.FormatInt(createdByID, 10) |
|
} |
|
if assignedByID > 0 { |
|
searchOpt.AssigneeID = strconv.FormatInt(assignedByID, 10) |
|
} |
|
if mentionedByID > 0 { |
|
searchOpt.MentionID = optional.Some(mentionedByID) |
|
} |
|
|
|
ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt) |
|
if err != nil { |
|
ctx.HTTPError(http.StatusInternalServerError, "SearchIssues", err.Error()) |
|
return |
|
} |
|
issues, err := issues_model.GetIssuesByIDs(ctx, ids, true) |
|
if err != nil { |
|
ctx.HTTPError(http.StatusInternalServerError, "FindIssuesByIDs", err.Error()) |
|
return |
|
} |
|
|
|
ctx.SetTotalCountHeader(total) |
|
ctx.JSON(http.StatusOK, convert.ToIssueList(ctx, ctx.Doer, issues)) |
|
} |
|
|
|
func BatchDeleteIssues(ctx *context.Context) { |
|
issues := getActionIssues(ctx) |
|
if ctx.Written() { |
|
return |
|
} |
|
for _, issue := range issues { |
|
if err := issue_service.DeleteIssue(ctx, ctx.Doer, ctx.Repo.GitRepo, issue); err != nil { |
|
ctx.ServerError("DeleteIssue", err) |
|
return |
|
} |
|
} |
|
ctx.JSONOK() |
|
} |
|
|
|
// UpdateIssueStatus change issue's status |
|
func UpdateIssueStatus(ctx *context.Context) { |
|
issues := getActionIssues(ctx) |
|
if ctx.Written() { |
|
return |
|
} |
|
|
|
action := ctx.FormString("action") |
|
if action != "open" && action != "close" { |
|
log.Warn("Unrecognized action: %s", action) |
|
ctx.JSONOK() |
|
return |
|
} |
|
|
|
if _, err := issues.LoadRepositories(ctx); err != nil { |
|
ctx.ServerError("LoadRepositories", err) |
|
return |
|
} |
|
if err := issues.LoadPullRequests(ctx); err != nil { |
|
ctx.ServerError("LoadPullRequests", err) |
|
return |
|
} |
|
|
|
for _, issue := range issues { |
|
if issue.IsPull && issue.PullRequest.HasMerged { |
|
continue |
|
} |
|
if action == "close" && !issue.IsClosed { |
|
if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil { |
|
if issues_model.IsErrDependenciesLeft(err) { |
|
ctx.JSON(http.StatusPreconditionFailed, map[string]any{ |
|
"error": ctx.Tr("repo.issues.dependency.issue_batch_close_blocked", issue.Index), |
|
}) |
|
return |
|
} |
|
ctx.ServerError("CloseIssue", err) |
|
return |
|
} |
|
} else if action == "open" && issue.IsClosed { |
|
if err := issue_service.ReopenIssue(ctx, issue, ctx.Doer, ""); err != nil { |
|
ctx.ServerError("ReopenIssue", err) |
|
return |
|
} |
|
} |
|
} |
|
ctx.JSONOK() |
|
} |
|
|
|
func prepareIssueFilterExclusiveOrderScopes(ctx *context.Context, allLabels []*issues_model.Label) { |
|
scopeSet := make(map[string]bool) |
|
for _, label := range allLabels { |
|
scope := label.ExclusiveScope() |
|
if len(scope) > 0 && label.ExclusiveOrder > 0 { |
|
scopeSet[scope] = true |
|
} |
|
} |
|
scopes := slices.Collect(maps.Keys(scopeSet)) |
|
sort.Strings(scopes) |
|
ctx.Data["ExclusiveLabelScopes"] = scopes |
|
} |
|
|
|
func renderMilestones(ctx *context.Context) { |
|
// Get milestones |
|
milestones, err := db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{ |
|
RepoID: ctx.Repo.Repository.ID, |
|
}) |
|
if err != nil { |
|
ctx.ServerError("GetAllRepoMilestones", err) |
|
return |
|
} |
|
|
|
openMilestones, closedMilestones := issues_model.MilestoneList{}, issues_model.MilestoneList{} |
|
for _, milestone := range milestones { |
|
if milestone.IsClosed { |
|
closedMilestones = append(closedMilestones, milestone) |
|
} else { |
|
openMilestones = append(openMilestones, milestone) |
|
} |
|
} |
|
ctx.Data["OpenMilestones"] = openMilestones |
|
ctx.Data["ClosedMilestones"] = closedMilestones |
|
} |
|
|
|
func prepareIssueFilterAndList(ctx *context.Context, milestoneID, projectID int64, isPullOption optional.Option[bool]) { |
|
var err error |
|
viewType := ctx.FormString("type") |
|
sortType := ctx.FormString("sort") |
|
types := []string{"all", "your_repositories", "assigned", "created_by", "mentioned", "review_requested", "reviewed_by"} |
|
if !util.SliceContainsString(types, viewType, true) { |
|
viewType = "all" |
|
} |
|
|
|
assigneeID := ctx.FormString("assignee") |
|
posterUsername := ctx.FormString("poster") |
|
posterUserID := shared_user.GetFilterUserIDByName(ctx, posterUsername) |
|
var mentionedID, reviewRequestedID, reviewedID int64 |
|
|
|
if ctx.IsSigned { |
|
switch viewType { |
|
case "created_by": |
|
posterUserID = strconv.FormatInt(ctx.Doer.ID, 10) |
|
case "mentioned": |
|
mentionedID = ctx.Doer.ID |
|
case "assigned": |
|
assigneeID = strconv.FormatInt(ctx.Doer.ID, 10) |
|
case "review_requested": |
|
reviewRequestedID = ctx.Doer.ID |
|
case "reviewed_by": |
|
reviewedID = ctx.Doer.ID |
|
} |
|
} |
|
|
|
repo := ctx.Repo.Repository |
|
keyword := strings.Trim(ctx.FormString("q"), " ") |
|
if bytes.Contains([]byte(keyword), []byte{0x00}) { |
|
keyword = "" |
|
} |
|
|
|
var mileIDs []int64 |
|
if milestoneID > 0 || milestoneID == db.NoConditionID { // -1 to get those issues which have no any milestone assigned |
|
mileIDs = []int64{milestoneID} |
|
} |
|
|
|
preparedLabelFilter := issue.PrepareFilterIssueLabels(ctx, repo.ID, ctx.Repo.Owner) |
|
if ctx.Written() { |
|
return |
|
} |
|
|
|
prepareIssueFilterExclusiveOrderScopes(ctx, preparedLabelFilter.AllLabels) |
|
|
|
var keywordMatchedIssueIDs []int64 |
|
var issueStats *issues_model.IssueStats |
|
statsOpts := &issues_model.IssuesOptions{ |
|
RepoIDs: []int64{repo.ID}, |
|
LabelIDs: preparedLabelFilter.SelectedLabelIDs, |
|
MilestoneIDs: mileIDs, |
|
ProjectID: projectID, |
|
AssigneeID: assigneeID, |
|
MentionedID: mentionedID, |
|
PosterID: posterUserID, |
|
ReviewRequestedID: reviewRequestedID, |
|
ReviewedID: reviewedID, |
|
IsPull: isPullOption, |
|
IssueIDs: nil, |
|
} |
|
if keyword != "" { |
|
keywordMatchedIssueIDs, _, err = issue_indexer.SearchIssues(ctx, issue_indexer.ToSearchOptions(keyword, statsOpts)) |
|
if err != nil { |
|
if issue_indexer.IsAvailable(ctx) { |
|
ctx.ServerError("issueIDsFromSearch", err) |
|
return |
|
} |
|
ctx.Data["IssueIndexerUnavailable"] = true |
|
return |
|
} |
|
if len(keywordMatchedIssueIDs) == 0 { |
|
// It did search with the keyword, but no issue found, just set issueStats to empty, then no need to do query again. |
|
issueStats = &issues_model.IssueStats{} |
|
// set keywordMatchedIssueIDs to empty slice, so we can distinguish it from "nil" |
|
keywordMatchedIssueIDs = []int64{} |
|
} |
|
statsOpts.IssueIDs = keywordMatchedIssueIDs |
|
} |
|
|
|
if issueStats == nil { |
|
// Either 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(ctx, statsOpts) |
|
if err != nil { |
|
ctx.ServerError("GetIssueStats", err) |
|
return |
|
} |
|
} |
|
|
|
var isShowClosed optional.Option[bool] |
|
switch ctx.FormString("state") { |
|
case "closed": |
|
isShowClosed = optional.Some(true) |
|
case "all": |
|
isShowClosed = optional.None[bool]() |
|
default: |
|
isShowClosed = optional.Some(false) |
|
} |
|
// if there are closed issues and no open issues, default to showing all issues |
|
if len(ctx.FormString("state")) == 0 && issueStats.OpenCount == 0 && issueStats.ClosedCount != 0 { |
|
isShowClosed = optional.None[bool]() |
|
} |
|
|
|
if repo.IsTimetrackerEnabled(ctx) { |
|
totalTrackedTime, err := issues_model.GetIssueTotalTrackedTime(ctx, statsOpts, isShowClosed) |
|
if err != nil { |
|
ctx.ServerError("GetIssueTotalTrackedTime", err) |
|
return |
|
} |
|
ctx.Data["TotalTrackedTime"] = totalTrackedTime |
|
} |
|
|
|
// prepare pager |
|
total := int(issueStats.OpenCount + issueStats.ClosedCount) |
|
if isShowClosed.Has() { |
|
total = util.Iif(isShowClosed.Value(), int(issueStats.ClosedCount), int(issueStats.OpenCount)) |
|
} |
|
page := max(ctx.FormInt("page"), 1) |
|
pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5) |
|
|
|
// prepare real issue list: |
|
var issues issues_model.IssueList |
|
if keywordMatchedIssueIDs == nil || len(keywordMatchedIssueIDs) > 0 { |
|
// Either it did search with the keyword, and found some issues, then keywordMatchedIssueIDs is not null, it needs to use db indexer. |
|
// Or the keyword is empty, it also needs to usd db indexer. |
|
// In either case, no need to use keyword anymore |
|
searchResult, err := db_indexer.GetIndexer().FindWithIssueOptions(ctx, &issues_model.IssuesOptions{ |
|
Paginator: &db.ListOptions{ |
|
Page: pager.Paginater.Current(), |
|
PageSize: setting.UI.IssuePagingNum, |
|
}, |
|
RepoIDs: []int64{repo.ID}, |
|
AssigneeID: assigneeID, |
|
PosterID: posterUserID, |
|
MentionedID: mentionedID, |
|
ReviewRequestedID: reviewRequestedID, |
|
ReviewedID: reviewedID, |
|
MilestoneIDs: mileIDs, |
|
ProjectID: projectID, |
|
IsClosed: isShowClosed, |
|
IsPull: isPullOption, |
|
LabelIDs: preparedLabelFilter.SelectedLabelIDs, |
|
SortType: sortType, |
|
IssueIDs: keywordMatchedIssueIDs, |
|
}) |
|
if err != nil { |
|
ctx.ServerError("DBIndexer.Search", err) |
|
return |
|
} |
|
issueIDs := issue_indexer.SearchResultToIDSlice(searchResult) |
|
issues, err = issues_model.GetIssuesByIDs(ctx, issueIDs, true) |
|
if err != nil { |
|
ctx.ServerError("GetIssuesByIDs", err) |
|
return |
|
} |
|
} |
|
|
|
approvalCounts, err := issues.GetApprovalCounts(ctx) |
|
if err != nil { |
|
ctx.ServerError("ApprovalCounts", err) |
|
return |
|
} |
|
|
|
if ctx.IsSigned { |
|
if err := issues.LoadIsRead(ctx, ctx.Doer.ID); err != nil { |
|
ctx.ServerError("LoadIsRead", err) |
|
return |
|
} |
|
} else { |
|
for i := range issues { |
|
issues[i].IsRead = true |
|
} |
|
} |
|
|
|
commitStatuses, lastStatus, err := pull_service.GetIssuesAllCommitStatus(ctx, issues) |
|
if err != nil { |
|
ctx.ServerError("GetIssuesAllCommitStatus", err) |
|
return |
|
} |
|
if !ctx.Repo.CanRead(unit.TypeActions) { |
|
for key := range commitStatuses { |
|
git_model.CommitStatusesHideActionsURL(ctx, commitStatuses[key]) |
|
} |
|
} |
|
|
|
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 |
|
|
|
// Get assignees. |
|
assigneeUsers, err := repo_model.GetRepoAssignees(ctx, repo) |
|
if err != nil { |
|
ctx.ServerError("GetRepoAssignees", err) |
|
return |
|
} |
|
handleMentionableAssigneesAndTeams(ctx, shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)) |
|
if ctx.Written() { |
|
return |
|
} |
|
|
|
ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] = issue_service.GetRefEndNamesAndURLs(issues, ctx.Repo.RepoLink) |
|
|
|
ctx.Data["ApprovalCounts"] = func(issueID int64, typ string) int64 { |
|
counts, ok := approvalCounts[issueID] |
|
if !ok || len(counts) == 0 { |
|
return 0 |
|
} |
|
reviewTyp := issues_model.ReviewTypeApprove |
|
switch typ { |
|
case "reject": |
|
reviewTyp = issues_model.ReviewTypeReject |
|
case "waiting": |
|
reviewTyp = issues_model.ReviewTypeRequest |
|
} |
|
for _, count := range counts { |
|
if count.Type == reviewTyp { |
|
return count.Count |
|
} |
|
} |
|
return 0 |
|
} |
|
|
|
retrieveProjectsForIssueList(ctx, repo) |
|
if ctx.Written() { |
|
return |
|
} |
|
|
|
pinned, err := issues_model.GetPinnedIssues(ctx, repo.ID, isPullOption.Value()) |
|
if err != nil { |
|
ctx.ServerError("GetPinnedIssues", err) |
|
return |
|
} |
|
|
|
showArchivedLabels := ctx.FormBool("archived_labels") |
|
ctx.Data["ShowArchivedLabels"] = showArchivedLabels |
|
ctx.Data["PinnedIssues"] = pinned |
|
ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.IsAdmin() || ctx.Doer.IsAdmin) |
|
ctx.Data["IssueStats"] = issueStats |
|
ctx.Data["OpenCount"] = issueStats.OpenCount |
|
ctx.Data["ClosedCount"] = issueStats.ClosedCount |
|
ctx.Data["SelLabelIDs"] = preparedLabelFilter.SelectedLabelIDs |
|
ctx.Data["ViewType"] = viewType |
|
ctx.Data["SortType"] = sortType |
|
ctx.Data["MilestoneID"] = milestoneID |
|
ctx.Data["ProjectID"] = projectID |
|
ctx.Data["AssigneeID"] = assigneeID |
|
ctx.Data["PosterUsername"] = posterUsername |
|
ctx.Data["Keyword"] = keyword |
|
ctx.Data["IsShowClosed"] = isShowClosed |
|
switch { |
|
case isShowClosed.Value(): |
|
ctx.Data["State"] = "closed" |
|
case !isShowClosed.Has(): |
|
ctx.Data["State"] = "all" |
|
default: |
|
ctx.Data["State"] = "open" |
|
} |
|
pager.AddParamFromRequest(ctx.Req) |
|
ctx.Data["Page"] = pager |
|
} |
|
|
|
// Issues render issues page |
|
func Issues(ctx *context.Context) { |
|
isPullList := ctx.PathParam("type") == "pulls" |
|
if isPullList { |
|
MustAllowPulls(ctx) |
|
if ctx.Written() { |
|
return |
|
} |
|
ctx.Data["Title"] = ctx.Tr("repo.pulls") |
|
ctx.Data["PageIsPullList"] = true |
|
} else { |
|
MustEnableIssues(ctx) |
|
if ctx.Written() { |
|
return |
|
} |
|
ctx.Data["Title"] = ctx.Tr("repo.issues") |
|
ctx.Data["PageIsIssueList"] = true |
|
ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo) |
|
} |
|
|
|
prepareIssueFilterAndList(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), optional.Some(isPullList)) |
|
if ctx.Written() { |
|
return |
|
} |
|
|
|
renderMilestones(ctx) |
|
if ctx.Written() { |
|
return |
|
} |
|
|
|
ctx.Data["CanWriteIssuesOrPulls"] = ctx.Repo.CanWriteIssuesOrPulls(isPullList) |
|
|
|
ctx.HTML(http.StatusOK, tplIssues) |
|
}
|
|
|