gitea源码

issue_page_meta.go 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. // Copyright 2024 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package repo
  4. import (
  5. "sort"
  6. "strconv"
  7. "strings"
  8. "code.gitea.io/gitea/models/db"
  9. issues_model "code.gitea.io/gitea/models/issues"
  10. "code.gitea.io/gitea/models/organization"
  11. project_model "code.gitea.io/gitea/models/project"
  12. repo_model "code.gitea.io/gitea/models/repo"
  13. user_model "code.gitea.io/gitea/models/user"
  14. "code.gitea.io/gitea/modules/container"
  15. "code.gitea.io/gitea/modules/optional"
  16. shared_user "code.gitea.io/gitea/routers/web/shared/user"
  17. "code.gitea.io/gitea/services/context"
  18. issue_service "code.gitea.io/gitea/services/issue"
  19. pull_service "code.gitea.io/gitea/services/pull"
  20. )
  21. type issueSidebarMilestoneData struct {
  22. SelectedMilestoneID int64
  23. OpenMilestones []*issues_model.Milestone
  24. ClosedMilestones []*issues_model.Milestone
  25. }
  26. type issueSidebarAssigneesData struct {
  27. SelectedAssigneeIDs string
  28. CandidateAssignees []*user_model.User
  29. }
  30. type issueSidebarProjectsData struct {
  31. SelectedProjectID int64
  32. OpenProjects []*project_model.Project
  33. ClosedProjects []*project_model.Project
  34. }
  35. type IssuePageMetaData struct {
  36. RepoLink string
  37. Repository *repo_model.Repository
  38. Issue *issues_model.Issue
  39. IsPullRequest bool
  40. CanModifyIssueOrPull bool
  41. ReviewersData *issueSidebarReviewersData
  42. LabelsData *issueSidebarLabelsData
  43. MilestonesData *issueSidebarMilestoneData
  44. ProjectsData *issueSidebarProjectsData
  45. AssigneesData *issueSidebarAssigneesData
  46. }
  47. func retrieveRepoIssueMetaData(ctx *context.Context, repo *repo_model.Repository, issue *issues_model.Issue, isPull bool) *IssuePageMetaData {
  48. data := &IssuePageMetaData{
  49. RepoLink: ctx.Repo.RepoLink,
  50. Repository: repo,
  51. Issue: issue,
  52. IsPullRequest: isPull,
  53. ReviewersData: &issueSidebarReviewersData{},
  54. LabelsData: &issueSidebarLabelsData{},
  55. MilestonesData: &issueSidebarMilestoneData{},
  56. ProjectsData: &issueSidebarProjectsData{},
  57. AssigneesData: &issueSidebarAssigneesData{},
  58. }
  59. ctx.Data["IssuePageMetaData"] = data
  60. if isPull {
  61. data.retrieveReviewersData(ctx)
  62. if ctx.Written() {
  63. return data
  64. }
  65. }
  66. data.retrieveLabelsData(ctx)
  67. if ctx.Written() {
  68. return data
  69. }
  70. // it sets "Branches" template data,
  71. // it is used to render the "edit PR target branches" dropdown, and the "branch selector" in the issue's sidebar.
  72. PrepareBranchList(ctx)
  73. if ctx.Written() {
  74. return data
  75. }
  76. // it sets the "Assignees" template data, and the data is also used to "mention" users.
  77. data.retrieveAssigneesData(ctx)
  78. if ctx.Written() {
  79. return data
  80. }
  81. // TODO: the issue/pull permissions are quite complex and unclear
  82. // A reader could create an issue/PR with setting some meta (eg: assignees from issue template, reviewers, target branch)
  83. // A reader(creator) could update some meta (eg: target branch), but can't change assignees anymore.
  84. // For non-creator users, only writers could update some meta (eg: assignees, milestone, project)
  85. // Need to clarify the logic and add some tests in the future
  86. data.CanModifyIssueOrPull = ctx.Repo.CanWriteIssuesOrPulls(isPull) && !ctx.Repo.Repository.IsArchived
  87. if !data.CanModifyIssueOrPull {
  88. return data
  89. }
  90. data.retrieveMilestonesDataForIssueWriter(ctx)
  91. if ctx.Written() {
  92. return data
  93. }
  94. data.retrieveProjectsDataForIssueWriter(ctx)
  95. if ctx.Written() {
  96. return data
  97. }
  98. ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx, ctx.Doer, isPull)
  99. return data
  100. }
  101. func (d *IssuePageMetaData) retrieveMilestonesDataForIssueWriter(ctx *context.Context) {
  102. var err error
  103. if d.Issue != nil {
  104. d.MilestonesData.SelectedMilestoneID = d.Issue.MilestoneID
  105. }
  106. d.MilestonesData.OpenMilestones, err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
  107. RepoID: d.Repository.ID,
  108. IsClosed: optional.Some(false),
  109. })
  110. if err != nil {
  111. ctx.ServerError("GetMilestones", err)
  112. return
  113. }
  114. d.MilestonesData.ClosedMilestones, err = db.Find[issues_model.Milestone](ctx, issues_model.FindMilestoneOptions{
  115. RepoID: d.Repository.ID,
  116. IsClosed: optional.Some(true),
  117. })
  118. if err != nil {
  119. ctx.ServerError("GetMilestones", err)
  120. return
  121. }
  122. }
  123. func (d *IssuePageMetaData) retrieveAssigneesData(ctx *context.Context) {
  124. var err error
  125. d.AssigneesData.CandidateAssignees, err = repo_model.GetRepoAssignees(ctx, d.Repository)
  126. if err != nil {
  127. ctx.ServerError("GetRepoAssignees", err)
  128. return
  129. }
  130. d.AssigneesData.CandidateAssignees = shared_user.MakeSelfOnTop(ctx.Doer, d.AssigneesData.CandidateAssignees)
  131. if d.Issue != nil {
  132. _ = d.Issue.LoadAssignees(ctx)
  133. ids := make([]string, 0, len(d.Issue.Assignees))
  134. for _, a := range d.Issue.Assignees {
  135. ids = append(ids, strconv.FormatInt(a.ID, 10))
  136. }
  137. d.AssigneesData.SelectedAssigneeIDs = strings.Join(ids, ",")
  138. }
  139. // FIXME: this is a tricky part which writes ctx.Data["Mentionable*"]
  140. handleMentionableAssigneesAndTeams(ctx, d.AssigneesData.CandidateAssignees)
  141. }
  142. func (d *IssuePageMetaData) retrieveProjectsDataForIssueWriter(ctx *context.Context) {
  143. if d.Issue != nil && d.Issue.Project != nil {
  144. d.ProjectsData.SelectedProjectID = d.Issue.Project.ID
  145. }
  146. d.ProjectsData.OpenProjects, d.ProjectsData.ClosedProjects = retrieveProjectsInternal(ctx, ctx.Repo.Repository)
  147. }
  148. // repoReviewerSelection items to bee shown
  149. type repoReviewerSelection struct {
  150. IsTeam bool
  151. Team *organization.Team
  152. User *user_model.User
  153. Review *issues_model.Review
  154. CanBeDismissed bool
  155. CanChange bool
  156. Requested bool
  157. ItemID int64
  158. }
  159. type issueSidebarReviewersData struct {
  160. CanChooseReviewer bool
  161. OriginalReviews issues_model.ReviewList
  162. TeamReviewers []*repoReviewerSelection
  163. Reviewers []*repoReviewerSelection
  164. CurrentPullReviewers []*repoReviewerSelection
  165. }
  166. // RetrieveRepoReviewers find all reviewers of a repository. If issue is nil, it means the doer is creating a new PR.
  167. func (d *IssuePageMetaData) retrieveReviewersData(ctx *context.Context) {
  168. data := d.ReviewersData
  169. repo := d.Repository
  170. if ctx.Doer != nil && ctx.IsSigned {
  171. if d.Issue == nil {
  172. data.CanChooseReviewer = true
  173. } else {
  174. data.CanChooseReviewer = issue_service.CanDoerChangeReviewRequests(ctx, ctx.Doer, repo, d.Issue.PosterID)
  175. }
  176. }
  177. var posterID int64
  178. var isClosed bool
  179. var reviews issues_model.ReviewList
  180. var err error
  181. if d.Issue == nil {
  182. if ctx.Doer != nil {
  183. posterID = ctx.Doer.ID
  184. }
  185. } else {
  186. posterID = d.Issue.PosterID
  187. if d.Issue.OriginalAuthorID > 0 {
  188. posterID = 0 // for migrated PRs, no poster ID
  189. }
  190. isClosed = d.Issue.IsClosed || d.Issue.PullRequest.HasMerged
  191. reviews, data.OriginalReviews, err = issues_model.GetReviewsByIssueID(ctx, d.Issue.ID)
  192. if err != nil {
  193. ctx.ServerError("GetReviewersByIssueID", err)
  194. return
  195. }
  196. if len(reviews) == 0 && !data.CanChooseReviewer {
  197. return
  198. }
  199. }
  200. var (
  201. pullReviews []*repoReviewerSelection
  202. reviewersResult []*repoReviewerSelection
  203. teamReviewersResult []*repoReviewerSelection
  204. teamReviewers []*organization.Team
  205. reviewers []*user_model.User
  206. )
  207. if data.CanChooseReviewer {
  208. var err error
  209. reviewers, err = pull_service.GetReviewers(ctx, repo, ctx.Doer.ID, posterID)
  210. if err != nil {
  211. ctx.ServerError("GetReviewers", err)
  212. return
  213. }
  214. teamReviewers, err = pull_service.GetReviewerTeams(ctx, repo)
  215. if err != nil {
  216. ctx.ServerError("GetReviewerTeams", err)
  217. return
  218. }
  219. if len(reviewers) > 0 {
  220. reviewersResult = make([]*repoReviewerSelection, 0, len(reviewers))
  221. }
  222. if len(teamReviewers) > 0 {
  223. teamReviewersResult = make([]*repoReviewerSelection, 0, len(teamReviewers))
  224. }
  225. }
  226. pullReviews = make([]*repoReviewerSelection, 0, len(reviews))
  227. for _, review := range reviews {
  228. tmp := &repoReviewerSelection{
  229. Requested: review.Type == issues_model.ReviewTypeRequest,
  230. Review: review,
  231. ItemID: review.ReviewerID,
  232. }
  233. if review.ReviewerTeamID > 0 {
  234. tmp.IsTeam = true
  235. tmp.ItemID = -review.ReviewerTeamID
  236. }
  237. if data.CanChooseReviewer {
  238. // Users who can choose reviewers can also remove review requests
  239. tmp.CanChange = true
  240. } else if ctx.Doer != nil && ctx.Doer.ID == review.ReviewerID && review.Type == issues_model.ReviewTypeRequest {
  241. // A user can refuse review requests
  242. tmp.CanChange = true
  243. }
  244. pullReviews = append(pullReviews, tmp)
  245. if data.CanChooseReviewer {
  246. if tmp.IsTeam {
  247. teamReviewersResult = append(teamReviewersResult, tmp)
  248. } else {
  249. reviewersResult = append(reviewersResult, tmp)
  250. }
  251. }
  252. }
  253. if len(pullReviews) > 0 {
  254. // Drop all non-existing users and teams from the reviews
  255. currentPullReviewers := make([]*repoReviewerSelection, 0, len(pullReviews))
  256. for _, item := range pullReviews {
  257. if item.Review.ReviewerID > 0 {
  258. if err := item.Review.LoadReviewer(ctx); err != nil {
  259. if user_model.IsErrUserNotExist(err) {
  260. continue
  261. }
  262. ctx.ServerError("LoadReviewer", err)
  263. return
  264. }
  265. item.User = item.Review.Reviewer
  266. } else if item.Review.ReviewerTeamID > 0 {
  267. if err := item.Review.LoadReviewerTeam(ctx); err != nil {
  268. if organization.IsErrTeamNotExist(err) {
  269. continue
  270. }
  271. ctx.ServerError("LoadReviewerTeam", err)
  272. return
  273. }
  274. item.Team = item.Review.ReviewerTeam
  275. } else {
  276. continue
  277. }
  278. item.CanBeDismissed = ctx.Repo.Permission.IsAdmin() && !isClosed &&
  279. (item.Review.Type == issues_model.ReviewTypeApprove || item.Review.Type == issues_model.ReviewTypeReject)
  280. currentPullReviewers = append(currentPullReviewers, item)
  281. }
  282. data.CurrentPullReviewers = currentPullReviewers
  283. }
  284. if data.CanChooseReviewer && reviewersResult != nil {
  285. preadded := len(reviewersResult)
  286. for _, reviewer := range reviewers {
  287. found := false
  288. reviewAddLoop:
  289. for _, tmp := range reviewersResult[:preadded] {
  290. if tmp.ItemID == reviewer.ID {
  291. tmp.User = reviewer
  292. found = true
  293. break reviewAddLoop
  294. }
  295. }
  296. if found {
  297. continue
  298. }
  299. reviewersResult = append(reviewersResult, &repoReviewerSelection{
  300. IsTeam: false,
  301. CanChange: true,
  302. User: reviewer,
  303. ItemID: reviewer.ID,
  304. })
  305. }
  306. data.Reviewers = reviewersResult
  307. }
  308. if data.CanChooseReviewer && teamReviewersResult != nil {
  309. preadded := len(teamReviewersResult)
  310. for _, team := range teamReviewers {
  311. found := false
  312. teamReviewAddLoop:
  313. for _, tmp := range teamReviewersResult[:preadded] {
  314. if tmp.ItemID == -team.ID {
  315. tmp.Team = team
  316. found = true
  317. break teamReviewAddLoop
  318. }
  319. }
  320. if found {
  321. continue
  322. }
  323. teamReviewersResult = append(teamReviewersResult, &repoReviewerSelection{
  324. IsTeam: true,
  325. CanChange: true,
  326. Team: team,
  327. ItemID: -team.ID,
  328. })
  329. }
  330. data.TeamReviewers = teamReviewersResult
  331. }
  332. }
  333. type issueSidebarLabelsData struct {
  334. AllLabels []*issues_model.Label
  335. RepoLabels []*issues_model.Label
  336. OrgLabels []*issues_model.Label
  337. SelectedLabelIDs string
  338. }
  339. func makeSelectedStringIDs[KeyType, ItemType comparable](
  340. allLabels []*issues_model.Label, candidateKey func(candidate *issues_model.Label) KeyType,
  341. selectedItems []ItemType, selectedKey func(selected ItemType) KeyType,
  342. ) string {
  343. selectedIDSet := make(container.Set[string])
  344. allLabelMap := map[KeyType]*issues_model.Label{}
  345. for _, label := range allLabels {
  346. allLabelMap[candidateKey(label)] = label
  347. }
  348. for _, item := range selectedItems {
  349. if label, ok := allLabelMap[selectedKey(item)]; ok {
  350. label.IsChecked = true
  351. selectedIDSet.Add(strconv.FormatInt(label.ID, 10))
  352. }
  353. }
  354. ids := selectedIDSet.Values()
  355. sort.Strings(ids)
  356. return strings.Join(ids, ",")
  357. }
  358. func (d *issueSidebarLabelsData) SetSelectedLabels(labels []*issues_model.Label) {
  359. d.SelectedLabelIDs = makeSelectedStringIDs(
  360. d.AllLabels, func(label *issues_model.Label) int64 { return label.ID },
  361. labels, func(label *issues_model.Label) int64 { return label.ID },
  362. )
  363. }
  364. func (d *issueSidebarLabelsData) SetSelectedLabelNames(labelNames []string) {
  365. d.SelectedLabelIDs = makeSelectedStringIDs(
  366. d.AllLabels, func(label *issues_model.Label) string { return strings.ToLower(label.Name) },
  367. labelNames, strings.ToLower,
  368. )
  369. }
  370. func (d *issueSidebarLabelsData) SetSelectedLabelIDs(labelIDs []int64) {
  371. d.SelectedLabelIDs = makeSelectedStringIDs(
  372. d.AllLabels, func(label *issues_model.Label) int64 { return label.ID },
  373. labelIDs, func(labelID int64) int64 { return labelID },
  374. )
  375. }
  376. func (d *IssuePageMetaData) retrieveLabelsData(ctx *context.Context) {
  377. repo := d.Repository
  378. labelsData := d.LabelsData
  379. labels, err := issues_model.GetLabelsByRepoID(ctx, repo.ID, "", db.ListOptions{})
  380. if err != nil {
  381. ctx.ServerError("GetLabelsByRepoID", err)
  382. return
  383. }
  384. labelsData.RepoLabels = labels
  385. if repo.Owner.IsOrganization() {
  386. orgLabels, err := issues_model.GetLabelsByOrgID(ctx, repo.Owner.ID, ctx.FormString("sort"), db.ListOptions{})
  387. if err != nil {
  388. return
  389. }
  390. labelsData.OrgLabels = orgLabels
  391. }
  392. labelsData.AllLabels = append(labelsData.AllLabels, labelsData.RepoLabels...)
  393. labelsData.AllLabels = append(labelsData.AllLabels, labelsData.OrgLabels...)
  394. }