mirror of
				https://github.com/go-gitea/gitea
				synced 2025-11-04 05:18:25 +00:00 
			
		
		
		
	Just functions move, no code change. --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
		
			
				
	
	
		
			883 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			883 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Copyright 2024 The Gitea Authors. All rights reserved.
 | 
						|
// SPDX-License-Identifier: MIT
 | 
						|
 | 
						|
package repo
 | 
						|
 | 
						|
import (
 | 
						|
	"bytes"
 | 
						|
	"fmt"
 | 
						|
	"net/http"
 | 
						|
	"net/url"
 | 
						|
	"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"
 | 
						|
	"code.gitea.io/gitea/modules/base"
 | 
						|
	issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
 | 
						|
	"code.gitea.io/gitea/modules/log"
 | 
						|
	"code.gitea.io/gitea/modules/optional"
 | 
						|
	"code.gitea.io/gitea/modules/setting"
 | 
						|
	"code.gitea.io/gitea/modules/util"
 | 
						|
	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 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
 | 
						|
}
 | 
						|
 | 
						|
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.Error(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.Error(http.StatusBadRequest, "Owner not found", err.Error())
 | 
						|
				} else {
 | 
						|
					ctx.Error(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.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
 | 
						|
		}
 | 
						|
		repoIDs, _, err = repo_model.SearchRepositoryIDs(ctx, opts)
 | 
						|
		if err != nil {
 | 
						|
			ctx.Error(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.Error(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.Error(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 = optional.Some(ctxUserID)
 | 
						|
		}
 | 
						|
		if ctx.FormBool("assigned") {
 | 
						|
			searchOpt.AssigneeID = optional.Some(ctxUserID)
 | 
						|
		}
 | 
						|
		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.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(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.Error(http.StatusInternalServerError, err.Error())
 | 
						|
		return 0
 | 
						|
	}
 | 
						|
 | 
						|
	return user.ID
 | 
						|
}
 | 
						|
 | 
						|
// ListIssues list the issues of a repository
 | 
						|
func ListIssues(ctx *context.Context) {
 | 
						|
	before, since, err := context.GetQueryBeforeSince(ctx.Base)
 | 
						|
	if err != nil {
 | 
						|
		ctx.Error(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 labelIDs []int64
 | 
						|
	if splitted := strings.Split(ctx.FormString("labels"), ","); len(splitted) > 0 {
 | 
						|
		labelIDs, err = issues_model.GetLabelIDsInRepoByNames(ctx, ctx.Repo.Repository.ID, splitted)
 | 
						|
		if err != nil {
 | 
						|
			ctx.Error(http.StatusInternalServerError, err.Error())
 | 
						|
			return
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	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.Error(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.Error(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)
 | 
						|
	}
 | 
						|
	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 len(mileIDs) == 1 && mileIDs[0] == db.NoConditionID {
 | 
						|
		searchOpt.MilestoneIDs = []int64{0}
 | 
						|
	} else {
 | 
						|
		searchOpt.MilestoneIDs = mileIDs
 | 
						|
	}
 | 
						|
 | 
						|
	if createdByID > 0 {
 | 
						|
		searchOpt.PosterID = optional.Some(createdByID)
 | 
						|
	}
 | 
						|
	if assignedByID > 0 {
 | 
						|
		searchOpt.AssigneeID = optional.Some(assignedByID)
 | 
						|
	}
 | 
						|
	if mentionedByID > 0 {
 | 
						|
		searchOpt.MentionID = optional.Some(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(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
 | 
						|
	}
 | 
						|
 | 
						|
	var isClosed bool
 | 
						|
	switch action := ctx.FormString("action"); action {
 | 
						|
	case "open":
 | 
						|
		isClosed = false
 | 
						|
	case "close":
 | 
						|
		isClosed = true
 | 
						|
	default:
 | 
						|
		log.Warn("Unrecognized action: %s", action)
 | 
						|
	}
 | 
						|
 | 
						|
	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 issue.IsClosed != isClosed {
 | 
						|
			if err := issue_service.ChangeStatus(ctx, issue, ctx.Doer, "", isClosed); 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("ChangeStatus", err)
 | 
						|
				return
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	ctx.JSONOK()
 | 
						|
}
 | 
						|
 | 
						|
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 issues(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"
 | 
						|
	}
 | 
						|
 | 
						|
	var (
 | 
						|
		assigneeID        = ctx.FormInt64("assignee")
 | 
						|
		posterID          = ctx.FormInt64("poster")
 | 
						|
		mentionedID       int64
 | 
						|
		reviewRequestedID int64
 | 
						|
		reviewedID        int64
 | 
						|
	)
 | 
						|
 | 
						|
	if ctx.IsSigned {
 | 
						|
		switch viewType {
 | 
						|
		case "created_by":
 | 
						|
			posterID = ctx.Doer.ID
 | 
						|
		case "mentioned":
 | 
						|
			mentionedID = ctx.Doer.ID
 | 
						|
		case "assigned":
 | 
						|
			assigneeID = ctx.Doer.ID
 | 
						|
		case "review_requested":
 | 
						|
			reviewRequestedID = ctx.Doer.ID
 | 
						|
		case "reviewed_by":
 | 
						|
			reviewedID = ctx.Doer.ID
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	repo := ctx.Repo.Repository
 | 
						|
	var labelIDs []int64
 | 
						|
	// 1,-2 means including label 1 and excluding label 2
 | 
						|
	// 0 means issues with no label
 | 
						|
	// blank means labels will not be filtered for issues
 | 
						|
	selectLabels := ctx.FormString("labels")
 | 
						|
	if selectLabels == "" {
 | 
						|
		ctx.Data["AllLabels"] = true
 | 
						|
	} else if selectLabels == "0" {
 | 
						|
		ctx.Data["NoLabel"] = true
 | 
						|
	}
 | 
						|
	if len(selectLabels) > 0 {
 | 
						|
		labelIDs, err = base.StringsToInt64s(strings.Split(selectLabels, ","))
 | 
						|
		if err != nil {
 | 
						|
			ctx.Flash.Error(ctx.Tr("invalid_data", selectLabels), true)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	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}
 | 
						|
	}
 | 
						|
 | 
						|
	var issueStats *issues_model.IssueStats
 | 
						|
	statsOpts := &issues_model.IssuesOptions{
 | 
						|
		RepoIDs:           []int64{repo.ID},
 | 
						|
		LabelIDs:          labelIDs,
 | 
						|
		MilestoneIDs:      mileIDs,
 | 
						|
		ProjectID:         projectID,
 | 
						|
		AssigneeID:        assigneeID,
 | 
						|
		MentionedID:       mentionedID,
 | 
						|
		PosterID:          posterID,
 | 
						|
		ReviewRequestedID: reviewRequestedID,
 | 
						|
		ReviewedID:        reviewedID,
 | 
						|
		IsPull:            isPullOption,
 | 
						|
		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(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
 | 
						|
	}
 | 
						|
 | 
						|
	archived := ctx.FormBool("archived")
 | 
						|
 | 
						|
	page := ctx.FormInt("page")
 | 
						|
	if page <= 1 {
 | 
						|
		page = 1
 | 
						|
	}
 | 
						|
 | 
						|
	var total int
 | 
						|
	switch {
 | 
						|
	case isShowClosed.Value():
 | 
						|
		total = int(issueStats.ClosedCount)
 | 
						|
	case !isShowClosed.Has():
 | 
						|
		total = int(issueStats.OpenCount + issueStats.ClosedCount)
 | 
						|
	default:
 | 
						|
		total = int(issueStats.OpenCount)
 | 
						|
	}
 | 
						|
	pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5)
 | 
						|
 | 
						|
	var issues issues_model.IssueList
 | 
						|
	{
 | 
						|
		ids, err := issueIDsFromSearch(ctx, keyword, &issues_model.IssuesOptions{
 | 
						|
			Paginator: &db.ListOptions{
 | 
						|
				Page:     pager.Paginater.Current(),
 | 
						|
				PageSize: setting.UI.IssuePagingNum,
 | 
						|
			},
 | 
						|
			RepoIDs:           []int64{repo.ID},
 | 
						|
			AssigneeID:        assigneeID,
 | 
						|
			PosterID:          posterID,
 | 
						|
			MentionedID:       mentionedID,
 | 
						|
			ReviewRequestedID: reviewRequestedID,
 | 
						|
			ReviewedID:        reviewedID,
 | 
						|
			MilestoneIDs:      mileIDs,
 | 
						|
			ProjectID:         projectID,
 | 
						|
			IsClosed:          isShowClosed,
 | 
						|
			IsPull:            isPullOption,
 | 
						|
			LabelIDs:          labelIDs,
 | 
						|
			SortType:          sortType,
 | 
						|
		})
 | 
						|
		if err != nil {
 | 
						|
			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
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	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
 | 
						|
	}
 | 
						|
	ctx.Data["Assignees"] = shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers)
 | 
						|
 | 
						|
	handleTeamMentions(ctx)
 | 
						|
	if ctx.Written() {
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{})
 | 
						|
	if err != nil {
 | 
						|
		ctx.ServerError("GetLabelsByRepoID", err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if repo.Owner.IsOrganization() {
 | 
						|
		orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
 | 
						|
		if err != nil {
 | 
						|
			ctx.ServerError("GetLabelsByOrgID", err)
 | 
						|
			return
 | 
						|
		}
 | 
						|
 | 
						|
		ctx.Data["OrgLabels"] = orgLabels
 | 
						|
		labels = append(labels, orgLabels...)
 | 
						|
	}
 | 
						|
 | 
						|
	// Get the exclusive scope for every label ID
 | 
						|
	labelExclusiveScopes := make([]string, 0, len(labelIDs))
 | 
						|
	for _, labelID := range labelIDs {
 | 
						|
		foundExclusiveScope := false
 | 
						|
		for _, label := range labels {
 | 
						|
			if label.ID == labelID || label.ID == -labelID {
 | 
						|
				labelExclusiveScopes = append(labelExclusiveScopes, label.ExclusiveScope())
 | 
						|
				foundExclusiveScope = true
 | 
						|
				break
 | 
						|
			}
 | 
						|
		}
 | 
						|
		if !foundExclusiveScope {
 | 
						|
			labelExclusiveScopes = append(labelExclusiveScopes, "")
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	for _, l := range labels {
 | 
						|
		l.LoadSelectedLabelsAfterClick(labelIDs, labelExclusiveScopes)
 | 
						|
	}
 | 
						|
	ctx.Data["Labels"] = labels
 | 
						|
	ctx.Data["NumLabels"] = len(labels)
 | 
						|
 | 
						|
	if ctx.FormInt64("assignee") == 0 {
 | 
						|
		assigneeID = 0 // Reset ID to prevent unexpected selection of assignee.
 | 
						|
	}
 | 
						|
 | 
						|
	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
 | 
						|
		if typ == "reject" {
 | 
						|
			reviewTyp = issues_model.ReviewTypeReject
 | 
						|
		} else if typ == "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
 | 
						|
	}
 | 
						|
 | 
						|
	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
 | 
						|
	linkStr := "%s?q=%s&type=%s&sort=%s&state=%s&labels=%s&milestone=%d&project=%d&assignee=%d&poster=%d&archived=%t"
 | 
						|
	ctx.Data["AllStatesLink"] = fmt.Sprintf(linkStr, ctx.Link,
 | 
						|
		url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "all", url.QueryEscape(selectLabels),
 | 
						|
		milestoneID, projectID, assigneeID, posterID, archived)
 | 
						|
	ctx.Data["OpenLink"] = fmt.Sprintf(linkStr, ctx.Link,
 | 
						|
		url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "open", url.QueryEscape(selectLabels),
 | 
						|
		milestoneID, projectID, assigneeID, posterID, archived)
 | 
						|
	ctx.Data["ClosedLink"] = fmt.Sprintf(linkStr, ctx.Link,
 | 
						|
		url.QueryEscape(keyword), url.QueryEscape(viewType), url.QueryEscape(sortType), "closed", url.QueryEscape(selectLabels),
 | 
						|
		milestoneID, projectID, assigneeID, posterID, archived)
 | 
						|
	ctx.Data["SelLabelIDs"] = labelIDs
 | 
						|
	ctx.Data["SelectLabels"] = selectLabels
 | 
						|
	ctx.Data["ViewType"] = viewType
 | 
						|
	ctx.Data["SortType"] = sortType
 | 
						|
	ctx.Data["MilestoneID"] = milestoneID
 | 
						|
	ctx.Data["ProjectID"] = projectID
 | 
						|
	ctx.Data["AssigneeID"] = assigneeID
 | 
						|
	ctx.Data["PosterID"] = posterID
 | 
						|
	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"
 | 
						|
	}
 | 
						|
	ctx.Data["ShowArchivedLabels"] = archived
 | 
						|
 | 
						|
	pager.AddParamString("q", keyword)
 | 
						|
	pager.AddParamString("type", viewType)
 | 
						|
	pager.AddParamString("sort", sortType)
 | 
						|
	pager.AddParamString("state", fmt.Sprint(ctx.Data["State"]))
 | 
						|
	pager.AddParamString("labels", fmt.Sprint(selectLabels))
 | 
						|
	pager.AddParamString("milestone", fmt.Sprint(milestoneID))
 | 
						|
	pager.AddParamString("project", fmt.Sprint(projectID))
 | 
						|
	pager.AddParamString("assignee", fmt.Sprint(assigneeID))
 | 
						|
	pager.AddParamString("poster", fmt.Sprint(posterID))
 | 
						|
	pager.AddParamString("archived", fmt.Sprint(archived))
 | 
						|
 | 
						|
	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)
 | 
						|
	}
 | 
						|
 | 
						|
	issues(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)
 | 
						|
}
 |