gitea源码

issue_list.go 22KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798
  1. // Copyright 2024 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package repo
  4. import (
  5. "bytes"
  6. "maps"
  7. "net/http"
  8. "slices"
  9. "sort"
  10. "strconv"
  11. "strings"
  12. "code.gitea.io/gitea/models/db"
  13. git_model "code.gitea.io/gitea/models/git"
  14. issues_model "code.gitea.io/gitea/models/issues"
  15. "code.gitea.io/gitea/models/organization"
  16. repo_model "code.gitea.io/gitea/models/repo"
  17. "code.gitea.io/gitea/models/unit"
  18. user_model "code.gitea.io/gitea/models/user"
  19. issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
  20. db_indexer "code.gitea.io/gitea/modules/indexer/issues/db"
  21. "code.gitea.io/gitea/modules/log"
  22. "code.gitea.io/gitea/modules/optional"
  23. "code.gitea.io/gitea/modules/setting"
  24. "code.gitea.io/gitea/modules/util"
  25. "code.gitea.io/gitea/routers/web/shared/issue"
  26. shared_user "code.gitea.io/gitea/routers/web/shared/user"
  27. "code.gitea.io/gitea/services/context"
  28. "code.gitea.io/gitea/services/convert"
  29. issue_service "code.gitea.io/gitea/services/issue"
  30. pull_service "code.gitea.io/gitea/services/pull"
  31. )
  32. func retrieveProjectsForIssueList(ctx *context.Context, repo *repo_model.Repository) {
  33. ctx.Data["OpenProjects"], ctx.Data["ClosedProjects"] = retrieveProjectsInternal(ctx, repo)
  34. }
  35. // SearchIssues searches for issues across the repositories that the user has access to
  36. func SearchIssues(ctx *context.Context) {
  37. before, since, err := context.GetQueryBeforeSince(ctx.Base)
  38. if err != nil {
  39. ctx.HTTPError(http.StatusUnprocessableEntity, err.Error())
  40. return
  41. }
  42. var isClosed optional.Option[bool]
  43. switch ctx.FormString("state") {
  44. case "closed":
  45. isClosed = optional.Some(true)
  46. case "all":
  47. isClosed = optional.None[bool]()
  48. default:
  49. isClosed = optional.Some(false)
  50. }
  51. var (
  52. repoIDs []int64
  53. allPublic bool
  54. )
  55. {
  56. // find repos user can access (for issue search)
  57. opts := repo_model.SearchRepoOptions{
  58. Private: false,
  59. AllPublic: true,
  60. TopicOnly: false,
  61. Collaborate: optional.None[bool](),
  62. // This needs to be a column that is not nil in fixtures or
  63. // MySQL will return different results when sorting by null in some cases
  64. OrderBy: db.SearchOrderByAlphabetically,
  65. Actor: ctx.Doer,
  66. }
  67. if ctx.IsSigned {
  68. opts.Private = true
  69. opts.AllLimited = true
  70. }
  71. if ctx.FormString("owner") != "" {
  72. owner, err := user_model.GetUserByName(ctx, ctx.FormString("owner"))
  73. if err != nil {
  74. if user_model.IsErrUserNotExist(err) {
  75. ctx.HTTPError(http.StatusBadRequest, "Owner not found", err.Error())
  76. } else {
  77. ctx.HTTPError(http.StatusInternalServerError, "GetUserByName", err.Error())
  78. }
  79. return
  80. }
  81. opts.OwnerID = owner.ID
  82. opts.AllLimited = false
  83. opts.AllPublic = false
  84. opts.Collaborate = optional.Some(false)
  85. }
  86. if ctx.FormString("team") != "" {
  87. if ctx.FormString("owner") == "" {
  88. ctx.HTTPError(http.StatusBadRequest, "", "Owner organisation is required for filtering on team")
  89. return
  90. }
  91. team, err := organization.GetTeam(ctx, opts.OwnerID, ctx.FormString("team"))
  92. if err != nil {
  93. if organization.IsErrTeamNotExist(err) {
  94. ctx.HTTPError(http.StatusBadRequest, "Team not found", err.Error())
  95. } else {
  96. ctx.HTTPError(http.StatusInternalServerError, "GetUserByName", err.Error())
  97. }
  98. return
  99. }
  100. opts.TeamID = team.ID
  101. }
  102. if opts.AllPublic {
  103. allPublic = true
  104. opts.AllPublic = false // set it false to avoid returning too many repos, we could filter by indexer
  105. }
  106. repoIDs, _, err = repo_model.SearchRepositoryIDs(ctx, opts)
  107. if err != nil {
  108. ctx.HTTPError(http.StatusInternalServerError, "SearchRepositoryIDs", err.Error())
  109. return
  110. }
  111. if len(repoIDs) == 0 {
  112. // no repos found, don't let the indexer return all repos
  113. repoIDs = []int64{0}
  114. }
  115. }
  116. keyword := ctx.FormTrim("q")
  117. if strings.IndexByte(keyword, 0) >= 0 {
  118. keyword = ""
  119. }
  120. isPull := optional.None[bool]()
  121. switch ctx.FormString("type") {
  122. case "pulls":
  123. isPull = optional.Some(true)
  124. case "issues":
  125. isPull = optional.Some(false)
  126. }
  127. var includedAnyLabels []int64
  128. {
  129. labels := ctx.FormTrim("labels")
  130. var includedLabelNames []string
  131. if len(labels) > 0 {
  132. includedLabelNames = strings.Split(labels, ",")
  133. }
  134. includedAnyLabels, err = issues_model.GetLabelIDsByNames(ctx, includedLabelNames)
  135. if err != nil {
  136. ctx.HTTPError(http.StatusInternalServerError, "GetLabelIDsByNames", err.Error())
  137. return
  138. }
  139. }
  140. var includedMilestones []int64
  141. {
  142. milestones := ctx.FormTrim("milestones")
  143. var includedMilestoneNames []string
  144. if len(milestones) > 0 {
  145. includedMilestoneNames = strings.Split(milestones, ",")
  146. }
  147. includedMilestones, err = issues_model.GetMilestoneIDsByNames(ctx, includedMilestoneNames)
  148. if err != nil {
  149. ctx.HTTPError(http.StatusInternalServerError, "GetMilestoneIDsByNames", err.Error())
  150. return
  151. }
  152. }
  153. projectID := optional.None[int64]()
  154. if v := ctx.FormInt64("project"); v > 0 {
  155. projectID = optional.Some(v)
  156. }
  157. // this api is also used in UI,
  158. // so the default limit is set to fit UI needs
  159. limit := ctx.FormInt("limit")
  160. if limit == 0 {
  161. limit = setting.UI.IssuePagingNum
  162. } else if limit > setting.API.MaxResponseItems {
  163. limit = setting.API.MaxResponseItems
  164. }
  165. searchOpt := &issue_indexer.SearchOptions{
  166. Paginator: &db.ListOptions{
  167. Page: ctx.FormInt("page"),
  168. PageSize: limit,
  169. },
  170. Keyword: keyword,
  171. RepoIDs: repoIDs,
  172. AllPublic: allPublic,
  173. IsPull: isPull,
  174. IsClosed: isClosed,
  175. IncludedAnyLabelIDs: includedAnyLabels,
  176. MilestoneIDs: includedMilestones,
  177. ProjectID: projectID,
  178. SortBy: issue_indexer.SortByCreatedDesc,
  179. }
  180. if since != 0 {
  181. searchOpt.UpdatedAfterUnix = optional.Some(since)
  182. }
  183. if before != 0 {
  184. searchOpt.UpdatedBeforeUnix = optional.Some(before)
  185. }
  186. if ctx.IsSigned {
  187. ctxUserID := ctx.Doer.ID
  188. if ctx.FormBool("created") {
  189. searchOpt.PosterID = strconv.FormatInt(ctxUserID, 10)
  190. }
  191. if ctx.FormBool("assigned") {
  192. searchOpt.AssigneeID = strconv.FormatInt(ctxUserID, 10)
  193. }
  194. if ctx.FormBool("mentioned") {
  195. searchOpt.MentionID = optional.Some(ctxUserID)
  196. }
  197. if ctx.FormBool("review_requested") {
  198. searchOpt.ReviewRequestedID = optional.Some(ctxUserID)
  199. }
  200. if ctx.FormBool("reviewed") {
  201. searchOpt.ReviewedID = optional.Some(ctxUserID)
  202. }
  203. }
  204. // FIXME: It's unsupported to sort by priority repo when searching by indexer,
  205. // it's indeed an regression, but I think it is worth to support filtering by indexer first.
  206. _ = ctx.FormInt64("priority_repo_id")
  207. ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
  208. if err != nil {
  209. ctx.HTTPError(http.StatusInternalServerError, "SearchIssues", err.Error())
  210. return
  211. }
  212. issues, err := issues_model.GetIssuesByIDs(ctx, ids, true)
  213. if err != nil {
  214. ctx.HTTPError(http.StatusInternalServerError, "FindIssuesByIDs", err.Error())
  215. return
  216. }
  217. ctx.SetTotalCountHeader(total)
  218. ctx.JSON(http.StatusOK, convert.ToIssueList(ctx, ctx.Doer, issues))
  219. }
  220. func getUserIDForFilter(ctx *context.Context, queryName string) int64 {
  221. userName := ctx.FormString(queryName)
  222. if len(userName) == 0 {
  223. return 0
  224. }
  225. user, err := user_model.GetUserByName(ctx, userName)
  226. if user_model.IsErrUserNotExist(err) {
  227. ctx.NotFound(err)
  228. return 0
  229. }
  230. if err != nil {
  231. ctx.HTTPError(http.StatusInternalServerError, err.Error())
  232. return 0
  233. }
  234. return user.ID
  235. }
  236. // SearchRepoIssuesJSON lists the issues of a repository
  237. // This function was copied from API (decouple the web and API routes),
  238. // it is only used by frontend to search some dependency or related issues
  239. func SearchRepoIssuesJSON(ctx *context.Context) {
  240. before, since, err := context.GetQueryBeforeSince(ctx.Base)
  241. if err != nil {
  242. ctx.HTTPError(http.StatusUnprocessableEntity, err.Error())
  243. return
  244. }
  245. var isClosed optional.Option[bool]
  246. switch ctx.FormString("state") {
  247. case "closed":
  248. isClosed = optional.Some(true)
  249. case "all":
  250. isClosed = optional.None[bool]()
  251. default:
  252. isClosed = optional.Some(false)
  253. }
  254. keyword := ctx.FormTrim("q")
  255. if strings.IndexByte(keyword, 0) >= 0 {
  256. keyword = ""
  257. }
  258. var mileIDs []int64
  259. if part := strings.Split(ctx.FormString("milestones"), ","); len(part) > 0 {
  260. for i := range part {
  261. // uses names and fall back to ids
  262. // non-existent milestones are discarded
  263. mile, err := issues_model.GetMilestoneByRepoIDANDName(ctx, ctx.Repo.Repository.ID, part[i])
  264. if err == nil {
  265. mileIDs = append(mileIDs, mile.ID)
  266. continue
  267. }
  268. if !issues_model.IsErrMilestoneNotExist(err) {
  269. ctx.HTTPError(http.StatusInternalServerError, err.Error())
  270. return
  271. }
  272. id, err := strconv.ParseInt(part[i], 10, 64)
  273. if err != nil {
  274. continue
  275. }
  276. mile, err = issues_model.GetMilestoneByRepoID(ctx, ctx.Repo.Repository.ID, id)
  277. if err == nil {
  278. mileIDs = append(mileIDs, mile.ID)
  279. continue
  280. }
  281. if issues_model.IsErrMilestoneNotExist(err) {
  282. continue
  283. }
  284. ctx.HTTPError(http.StatusInternalServerError, err.Error())
  285. }
  286. }
  287. projectID := optional.None[int64]()
  288. if v := ctx.FormInt64("project"); v > 0 {
  289. projectID = optional.Some(v)
  290. }
  291. isPull := optional.None[bool]()
  292. switch ctx.FormString("type") {
  293. case "pulls":
  294. isPull = optional.Some(true)
  295. case "issues":
  296. isPull = optional.Some(false)
  297. }
  298. // FIXME: we should be more efficient here
  299. createdByID := getUserIDForFilter(ctx, "created_by")
  300. if ctx.Written() {
  301. return
  302. }
  303. assignedByID := getUserIDForFilter(ctx, "assigned_by")
  304. if ctx.Written() {
  305. return
  306. }
  307. mentionedByID := getUserIDForFilter(ctx, "mentioned_by")
  308. if ctx.Written() {
  309. return
  310. }
  311. searchOpt := &issue_indexer.SearchOptions{
  312. Paginator: &db.ListOptions{
  313. Page: ctx.FormInt("page"),
  314. PageSize: convert.ToCorrectPageSize(ctx.FormInt("limit")),
  315. },
  316. Keyword: keyword,
  317. RepoIDs: []int64{ctx.Repo.Repository.ID},
  318. IsPull: isPull,
  319. IsClosed: isClosed,
  320. ProjectID: projectID,
  321. SortBy: issue_indexer.SortByCreatedDesc,
  322. }
  323. if since != 0 {
  324. searchOpt.UpdatedAfterUnix = optional.Some(since)
  325. }
  326. if before != 0 {
  327. searchOpt.UpdatedBeforeUnix = optional.Some(before)
  328. }
  329. // TODO: the "labels" query parameter is never used, so no need to handle it
  330. if len(mileIDs) == 1 && mileIDs[0] == db.NoConditionID {
  331. searchOpt.MilestoneIDs = []int64{0}
  332. } else {
  333. searchOpt.MilestoneIDs = mileIDs
  334. }
  335. if createdByID > 0 {
  336. searchOpt.PosterID = strconv.FormatInt(createdByID, 10)
  337. }
  338. if assignedByID > 0 {
  339. searchOpt.AssigneeID = strconv.FormatInt(assignedByID, 10)
  340. }
  341. if mentionedByID > 0 {
  342. searchOpt.MentionID = optional.Some(mentionedByID)
  343. }
  344. ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
  345. if err != nil {
  346. ctx.HTTPError(http.StatusInternalServerError, "SearchIssues", err.Error())
  347. return
  348. }
  349. issues, err := issues_model.GetIssuesByIDs(ctx, ids, true)
  350. if err != nil {
  351. ctx.HTTPError(http.StatusInternalServerError, "FindIssuesByIDs", err.Error())
  352. return
  353. }
  354. ctx.SetTotalCountHeader(total)
  355. ctx.JSON(http.StatusOK, convert.ToIssueList(ctx, ctx.Doer, issues))
  356. }
  357. func BatchDeleteIssues(ctx *context.Context) {
  358. issues := getActionIssues(ctx)
  359. if ctx.Written() {
  360. return
  361. }
  362. for _, issue := range issues {
  363. if err := issue_service.DeleteIssue(ctx, ctx.Doer, issue); err != nil {
  364. ctx.ServerError("DeleteIssue", err)
  365. return
  366. }
  367. }
  368. ctx.JSONOK()
  369. }
  370. // UpdateIssueStatus change issue's status
  371. func UpdateIssueStatus(ctx *context.Context) {
  372. issues := getActionIssues(ctx)
  373. if ctx.Written() {
  374. return
  375. }
  376. action := ctx.FormString("action")
  377. if action != "open" && action != "close" {
  378. log.Warn("Unrecognized action: %s", action)
  379. ctx.JSONOK()
  380. return
  381. }
  382. if _, err := issues.LoadRepositories(ctx); err != nil {
  383. ctx.ServerError("LoadRepositories", err)
  384. return
  385. }
  386. if err := issues.LoadPullRequests(ctx); err != nil {
  387. ctx.ServerError("LoadPullRequests", err)
  388. return
  389. }
  390. for _, issue := range issues {
  391. if issue.IsPull && issue.PullRequest.HasMerged {
  392. continue
  393. }
  394. if action == "close" && !issue.IsClosed {
  395. if err := issue_service.CloseIssue(ctx, issue, ctx.Doer, ""); err != nil {
  396. if issues_model.IsErrDependenciesLeft(err) {
  397. ctx.JSON(http.StatusPreconditionFailed, map[string]any{
  398. "error": ctx.Tr("repo.issues.dependency.issue_batch_close_blocked", issue.Index),
  399. })
  400. return
  401. }
  402. ctx.ServerError("CloseIssue", err)
  403. return
  404. }
  405. } else if action == "open" && issue.IsClosed {
  406. if err := issue_service.ReopenIssue(ctx, issue, ctx.Doer, ""); err != nil {
  407. ctx.ServerError("ReopenIssue", err)
  408. return
  409. }
  410. }
  411. }
  412. ctx.JSONOK()
  413. }
  414. func prepareIssueFilterExclusiveOrderScopes(ctx *context.Context, allLabels []*issues_model.Label) {
  415. scopeSet := make(map[string]bool)
  416. for _, label := range allLabels {
  417. scope := label.ExclusiveScope()
  418. if len(scope) > 0 && label.ExclusiveOrder > 0 {
  419. scopeSet[scope] = true
  420. }
  421. }
  422. scopes := slices.Collect(maps.Keys(scopeSet))
  423. sort.Strings(scopes)
  424. ctx.Data["ExclusiveLabelScopes"] = scopes
  425. }
  426. func renderMilestones(ctx *context.Context) {
  427. // Get milestones
  428. milestones, err := db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
  429. RepoID: ctx.Repo.Repository.ID,
  430. })
  431. if err != nil {
  432. ctx.ServerError("GetAllRepoMilestones", err)
  433. return
  434. }
  435. openMilestones, closedMilestones := issues_model.MilestoneList{}, issues_model.MilestoneList{}
  436. for _, milestone := range milestones {
  437. if milestone.IsClosed {
  438. closedMilestones = append(closedMilestones, milestone)
  439. } else {
  440. openMilestones = append(openMilestones, milestone)
  441. }
  442. }
  443. ctx.Data["OpenMilestones"] = openMilestones
  444. ctx.Data["ClosedMilestones"] = closedMilestones
  445. }
  446. func prepareIssueFilterAndList(ctx *context.Context, milestoneID, projectID int64, isPullOption optional.Option[bool]) {
  447. var err error
  448. viewType := ctx.FormString("type")
  449. sortType := ctx.FormString("sort")
  450. types := []string{"all", "your_repositories", "assigned", "created_by", "mentioned", "review_requested", "reviewed_by"}
  451. if !util.SliceContainsString(types, viewType, true) {
  452. viewType = "all"
  453. }
  454. assigneeID := ctx.FormString("assignee")
  455. posterUsername := ctx.FormString("poster")
  456. posterUserID := shared_user.GetFilterUserIDByName(ctx, posterUsername)
  457. var mentionedID, reviewRequestedID, reviewedID int64
  458. if ctx.IsSigned {
  459. switch viewType {
  460. case "created_by":
  461. posterUserID = strconv.FormatInt(ctx.Doer.ID, 10)
  462. case "mentioned":
  463. mentionedID = ctx.Doer.ID
  464. case "assigned":
  465. assigneeID = strconv.FormatInt(ctx.Doer.ID, 10)
  466. case "review_requested":
  467. reviewRequestedID = ctx.Doer.ID
  468. case "reviewed_by":
  469. reviewedID = ctx.Doer.ID
  470. }
  471. }
  472. repo := ctx.Repo.Repository
  473. keyword := strings.Trim(ctx.FormString("q"), " ")
  474. if bytes.Contains([]byte(keyword), []byte{0x00}) {
  475. keyword = ""
  476. }
  477. var mileIDs []int64
  478. if milestoneID > 0 || milestoneID == db.NoConditionID { // -1 to get those issues which have no any milestone assigned
  479. mileIDs = []int64{milestoneID}
  480. }
  481. preparedLabelFilter := issue.PrepareFilterIssueLabels(ctx, repo.ID, ctx.Repo.Owner)
  482. if ctx.Written() {
  483. return
  484. }
  485. prepareIssueFilterExclusiveOrderScopes(ctx, preparedLabelFilter.AllLabels)
  486. var keywordMatchedIssueIDs []int64
  487. var issueStats *issues_model.IssueStats
  488. statsOpts := &issues_model.IssuesOptions{
  489. RepoIDs: []int64{repo.ID},
  490. LabelIDs: preparedLabelFilter.SelectedLabelIDs,
  491. MilestoneIDs: mileIDs,
  492. ProjectID: projectID,
  493. AssigneeID: assigneeID,
  494. MentionedID: mentionedID,
  495. PosterID: posterUserID,
  496. ReviewRequestedID: reviewRequestedID,
  497. ReviewedID: reviewedID,
  498. IsPull: isPullOption,
  499. IssueIDs: nil,
  500. }
  501. if keyword != "" {
  502. keywordMatchedIssueIDs, _, err = issue_indexer.SearchIssues(ctx, issue_indexer.ToSearchOptions(keyword, statsOpts))
  503. if err != nil {
  504. if issue_indexer.IsAvailable(ctx) {
  505. ctx.ServerError("issueIDsFromSearch", err)
  506. return
  507. }
  508. ctx.Data["IssueIndexerUnavailable"] = true
  509. return
  510. }
  511. if len(keywordMatchedIssueIDs) == 0 {
  512. // It did search with the keyword, but no issue found, just set issueStats to empty, then no need to do query again.
  513. issueStats = &issues_model.IssueStats{}
  514. // set keywordMatchedIssueIDs to empty slice, so we can distinguish it from "nil"
  515. keywordMatchedIssueIDs = []int64{}
  516. }
  517. statsOpts.IssueIDs = keywordMatchedIssueIDs
  518. }
  519. if issueStats == nil {
  520. // Either it did search with the keyword, and found some issues, it needs to get issueStats of these issues.
  521. // Or the keyword is empty, so it doesn't need issueIDs as filter, just get issueStats with statsOpts.
  522. issueStats, err = issues_model.GetIssueStats(ctx, statsOpts)
  523. if err != nil {
  524. ctx.ServerError("GetIssueStats", err)
  525. return
  526. }
  527. }
  528. var isShowClosed optional.Option[bool]
  529. switch ctx.FormString("state") {
  530. case "closed":
  531. isShowClosed = optional.Some(true)
  532. case "all":
  533. isShowClosed = optional.None[bool]()
  534. default:
  535. isShowClosed = optional.Some(false)
  536. }
  537. // if there are closed issues and no open issues, default to showing all issues
  538. if len(ctx.FormString("state")) == 0 && issueStats.OpenCount == 0 && issueStats.ClosedCount != 0 {
  539. isShowClosed = optional.None[bool]()
  540. }
  541. if repo.IsTimetrackerEnabled(ctx) {
  542. totalTrackedTime, err := issues_model.GetIssueTotalTrackedTime(ctx, statsOpts, isShowClosed)
  543. if err != nil {
  544. ctx.ServerError("GetIssueTotalTrackedTime", err)
  545. return
  546. }
  547. ctx.Data["TotalTrackedTime"] = totalTrackedTime
  548. }
  549. // prepare pager
  550. total := int(issueStats.OpenCount + issueStats.ClosedCount)
  551. if isShowClosed.Has() {
  552. total = util.Iif(isShowClosed.Value(), int(issueStats.ClosedCount), int(issueStats.OpenCount))
  553. }
  554. page := max(ctx.FormInt("page"), 1)
  555. pager := context.NewPagination(total, setting.UI.IssuePagingNum, page, 5)
  556. // prepare real issue list:
  557. var issues issues_model.IssueList
  558. if keywordMatchedIssueIDs == nil || len(keywordMatchedIssueIDs) > 0 {
  559. // Either it did search with the keyword, and found some issues, then keywordMatchedIssueIDs is not null, it needs to use db indexer.
  560. // Or the keyword is empty, it also needs to usd db indexer.
  561. // In either case, no need to use keyword anymore
  562. searchResult, err := db_indexer.GetIndexer().FindWithIssueOptions(ctx, &issues_model.IssuesOptions{
  563. Paginator: &db.ListOptions{
  564. Page: pager.Paginater.Current(),
  565. PageSize: setting.UI.IssuePagingNum,
  566. },
  567. RepoIDs: []int64{repo.ID},
  568. AssigneeID: assigneeID,
  569. PosterID: posterUserID,
  570. MentionedID: mentionedID,
  571. ReviewRequestedID: reviewRequestedID,
  572. ReviewedID: reviewedID,
  573. MilestoneIDs: mileIDs,
  574. ProjectID: projectID,
  575. IsClosed: isShowClosed,
  576. IsPull: isPullOption,
  577. LabelIDs: preparedLabelFilter.SelectedLabelIDs,
  578. SortType: sortType,
  579. IssueIDs: keywordMatchedIssueIDs,
  580. })
  581. if err != nil {
  582. ctx.ServerError("DBIndexer.Search", err)
  583. return
  584. }
  585. issueIDs := issue_indexer.SearchResultToIDSlice(searchResult)
  586. issues, err = issues_model.GetIssuesByIDs(ctx, issueIDs, true)
  587. if err != nil {
  588. ctx.ServerError("GetIssuesByIDs", err)
  589. return
  590. }
  591. }
  592. approvalCounts, err := issues.GetApprovalCounts(ctx)
  593. if err != nil {
  594. ctx.ServerError("ApprovalCounts", err)
  595. return
  596. }
  597. if ctx.IsSigned {
  598. if err := issues.LoadIsRead(ctx, ctx.Doer.ID); err != nil {
  599. ctx.ServerError("LoadIsRead", err)
  600. return
  601. }
  602. } else {
  603. for i := range issues {
  604. issues[i].IsRead = true
  605. }
  606. }
  607. commitStatuses, lastStatus, err := pull_service.GetIssuesAllCommitStatus(ctx, issues)
  608. if err != nil {
  609. ctx.ServerError("GetIssuesAllCommitStatus", err)
  610. return
  611. }
  612. if !ctx.Repo.CanRead(unit.TypeActions) {
  613. for key := range commitStatuses {
  614. git_model.CommitStatusesHideActionsURL(ctx, commitStatuses[key])
  615. }
  616. }
  617. if err := issues.LoadAttributes(ctx); err != nil {
  618. ctx.ServerError("issues.LoadAttributes", err)
  619. return
  620. }
  621. ctx.Data["Issues"] = issues
  622. ctx.Data["CommitLastStatus"] = lastStatus
  623. ctx.Data["CommitStatuses"] = commitStatuses
  624. // Get assignees.
  625. assigneeUsers, err := repo_model.GetRepoAssignees(ctx, repo)
  626. if err != nil {
  627. ctx.ServerError("GetRepoAssignees", err)
  628. return
  629. }
  630. handleMentionableAssigneesAndTeams(ctx, shared_user.MakeSelfOnTop(ctx.Doer, assigneeUsers))
  631. if ctx.Written() {
  632. return
  633. }
  634. ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] = issue_service.GetRefEndNamesAndURLs(issues, ctx.Repo.RepoLink)
  635. ctx.Data["ApprovalCounts"] = func(issueID int64, typ string) int64 {
  636. counts, ok := approvalCounts[issueID]
  637. if !ok || len(counts) == 0 {
  638. return 0
  639. }
  640. reviewTyp := issues_model.ReviewTypeApprove
  641. switch typ {
  642. case "reject":
  643. reviewTyp = issues_model.ReviewTypeReject
  644. case "waiting":
  645. reviewTyp = issues_model.ReviewTypeRequest
  646. }
  647. for _, count := range counts {
  648. if count.Type == reviewTyp {
  649. return count.Count
  650. }
  651. }
  652. return 0
  653. }
  654. retrieveProjectsForIssueList(ctx, repo)
  655. if ctx.Written() {
  656. return
  657. }
  658. pinned, err := issues_model.GetPinnedIssues(ctx, repo.ID, isPullOption.Value())
  659. if err != nil {
  660. ctx.ServerError("GetPinnedIssues", err)
  661. return
  662. }
  663. showArchivedLabels := ctx.FormBool("archived_labels")
  664. ctx.Data["ShowArchivedLabels"] = showArchivedLabels
  665. ctx.Data["PinnedIssues"] = pinned
  666. ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.IsAdmin() || ctx.Doer.IsAdmin)
  667. ctx.Data["IssueStats"] = issueStats
  668. ctx.Data["OpenCount"] = issueStats.OpenCount
  669. ctx.Data["ClosedCount"] = issueStats.ClosedCount
  670. ctx.Data["SelLabelIDs"] = preparedLabelFilter.SelectedLabelIDs
  671. ctx.Data["ViewType"] = viewType
  672. ctx.Data["SortType"] = sortType
  673. ctx.Data["MilestoneID"] = milestoneID
  674. ctx.Data["ProjectID"] = projectID
  675. ctx.Data["AssigneeID"] = assigneeID
  676. ctx.Data["PosterUsername"] = posterUsername
  677. ctx.Data["Keyword"] = keyword
  678. ctx.Data["IsShowClosed"] = isShowClosed
  679. switch {
  680. case isShowClosed.Value():
  681. ctx.Data["State"] = "closed"
  682. case !isShowClosed.Has():
  683. ctx.Data["State"] = "all"
  684. default:
  685. ctx.Data["State"] = "open"
  686. }
  687. pager.AddParamFromRequest(ctx.Req)
  688. ctx.Data["Page"] = pager
  689. }
  690. // Issues render issues page
  691. func Issues(ctx *context.Context) {
  692. isPullList := ctx.PathParam("type") == "pulls"
  693. if isPullList {
  694. MustAllowPulls(ctx)
  695. if ctx.Written() {
  696. return
  697. }
  698. ctx.Data["Title"] = ctx.Tr("repo.pulls")
  699. ctx.Data["PageIsPullList"] = true
  700. prepareRecentlyPushedNewBranches(ctx)
  701. if ctx.Written() {
  702. return
  703. }
  704. } else {
  705. MustEnableIssues(ctx)
  706. if ctx.Written() {
  707. return
  708. }
  709. ctx.Data["Title"] = ctx.Tr("repo.issues")
  710. ctx.Data["PageIsIssueList"] = true
  711. ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo)
  712. }
  713. prepareIssueFilterAndList(ctx, ctx.FormInt64("milestone"), ctx.FormInt64("project"), optional.Some(isPullList))
  714. if ctx.Written() {
  715. return
  716. }
  717. renderMilestones(ctx)
  718. if ctx.Written() {
  719. return
  720. }
  721. ctx.Data["CanWriteIssuesOrPulls"] = ctx.Repo.CanWriteIssuesOrPulls(isPullList)
  722. ctx.HTML(http.StatusOK, tplIssues)
  723. }