gitea源码

issue_view.go 33KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010
  1. // Copyright 2024 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package repo
  4. import (
  5. "fmt"
  6. "math/big"
  7. "net/http"
  8. "net/url"
  9. "sort"
  10. "strconv"
  11. activities_model "code.gitea.io/gitea/models/activities"
  12. asymkey_model "code.gitea.io/gitea/models/asymkey"
  13. "code.gitea.io/gitea/models/db"
  14. git_model "code.gitea.io/gitea/models/git"
  15. issues_model "code.gitea.io/gitea/models/issues"
  16. "code.gitea.io/gitea/models/organization"
  17. access_model "code.gitea.io/gitea/models/perm/access"
  18. project_model "code.gitea.io/gitea/models/project"
  19. pull_model "code.gitea.io/gitea/models/pull"
  20. "code.gitea.io/gitea/models/renderhelper"
  21. repo_model "code.gitea.io/gitea/models/repo"
  22. "code.gitea.io/gitea/models/unit"
  23. user_model "code.gitea.io/gitea/models/user"
  24. "code.gitea.io/gitea/modules/emoji"
  25. "code.gitea.io/gitea/modules/git"
  26. "code.gitea.io/gitea/modules/gitrepo"
  27. "code.gitea.io/gitea/modules/log"
  28. "code.gitea.io/gitea/modules/markup"
  29. "code.gitea.io/gitea/modules/markup/markdown"
  30. "code.gitea.io/gitea/modules/setting"
  31. "code.gitea.io/gitea/modules/templates"
  32. "code.gitea.io/gitea/modules/templates/vars"
  33. "code.gitea.io/gitea/modules/util"
  34. asymkey_service "code.gitea.io/gitea/services/asymkey"
  35. "code.gitea.io/gitea/services/context"
  36. "code.gitea.io/gitea/services/context/upload"
  37. issue_service "code.gitea.io/gitea/services/issue"
  38. pull_service "code.gitea.io/gitea/services/pull"
  39. user_service "code.gitea.io/gitea/services/user"
  40. )
  41. // roleDescriptor returns the role descriptor for a comment in/with the given repo, poster and issue
  42. func roleDescriptor(ctx *context.Context, repo *repo_model.Repository, poster *user_model.User, permsCache map[int64]access_model.Permission, issue *issues_model.Issue, hasOriginalAuthor bool) (roleDesc issues_model.RoleDescriptor, err error) {
  43. if hasOriginalAuthor {
  44. // the poster is a migrated user, so no need to detect the role
  45. return roleDesc, nil
  46. }
  47. if poster.IsGhost() || !poster.IsIndividual() {
  48. return roleDesc, nil
  49. }
  50. roleDesc.IsPoster = issue.IsPoster(poster.ID) // check whether the comment's poster is the issue's poster
  51. // Guess the role of the poster in the repo by permission
  52. perm, hasPermCache := permsCache[poster.ID]
  53. if !hasPermCache {
  54. perm, err = access_model.GetUserRepoPermission(ctx, repo, poster)
  55. if err != nil {
  56. return roleDesc, err
  57. }
  58. }
  59. if permsCache != nil {
  60. permsCache[poster.ID] = perm
  61. }
  62. // Check if the poster is owner of the repo.
  63. if perm.IsOwner() {
  64. // If the poster isn't a site admin, then is must be the repo's owner
  65. if !poster.IsAdmin {
  66. roleDesc.RoleInRepo = issues_model.RoleRepoOwner
  67. return roleDesc, nil
  68. }
  69. // Otherwise (poster is site admin), check if poster is the real repo admin.
  70. isRealRepoAdmin, err := access_model.IsUserRealRepoAdmin(ctx, repo, poster)
  71. if err != nil {
  72. return roleDesc, err
  73. }
  74. if isRealRepoAdmin {
  75. roleDesc.RoleInRepo = issues_model.RoleRepoOwner
  76. return roleDesc, nil
  77. }
  78. }
  79. // If repo is organization, check Member role
  80. if err = repo.LoadOwner(ctx); err != nil {
  81. return roleDesc, err
  82. }
  83. if repo.Owner.IsOrganization() {
  84. if isMember, err := organization.IsOrganizationMember(ctx, repo.Owner.ID, poster.ID); err != nil {
  85. return roleDesc, err
  86. } else if isMember {
  87. roleDesc.RoleInRepo = issues_model.RoleRepoMember
  88. return roleDesc, nil
  89. }
  90. }
  91. // If the poster is the collaborator of the repo
  92. if isCollaborator, err := repo_model.IsCollaborator(ctx, repo.ID, poster.ID); err != nil {
  93. return roleDesc, err
  94. } else if isCollaborator {
  95. roleDesc.RoleInRepo = issues_model.RoleRepoCollaborator
  96. return roleDesc, nil
  97. }
  98. hasMergedPR, err := issues_model.HasMergedPullRequestInRepo(ctx, repo.ID, poster.ID)
  99. if err != nil {
  100. return roleDesc, err
  101. } else if hasMergedPR {
  102. roleDesc.RoleInRepo = issues_model.RoleRepoContributor
  103. } else if issue.IsPull {
  104. // only display first time contributor in the first opening pull request
  105. roleDesc.RoleInRepo = issues_model.RoleRepoFirstTimeContributor
  106. }
  107. return roleDesc, nil
  108. }
  109. func getBranchData(ctx *context.Context, issue *issues_model.Issue) {
  110. ctx.Data["BaseBranch"] = nil
  111. ctx.Data["HeadBranch"] = nil
  112. ctx.Data["HeadUserName"] = nil
  113. ctx.Data["BaseName"] = ctx.Repo.Repository.OwnerName
  114. if issue.IsPull {
  115. pull := issue.PullRequest
  116. ctx.Data["BaseBranch"] = pull.BaseBranch
  117. ctx.Data["HeadBranch"] = pull.HeadBranch
  118. ctx.Data["HeadUserName"] = pull.MustHeadUserName(ctx)
  119. }
  120. }
  121. // checkBlockedByIssues return canRead and notPermitted
  122. func checkBlockedByIssues(ctx *context.Context, blockers []*issues_model.DependencyInfo) (canRead, notPermitted []*issues_model.DependencyInfo) {
  123. repoPerms := make(map[int64]access_model.Permission)
  124. repoPerms[ctx.Repo.Repository.ID] = ctx.Repo.Permission
  125. for _, blocker := range blockers {
  126. // Get the permissions for this repository
  127. // If the repo ID exists in the map, return the exist permissions
  128. // else get the permission and add it to the map
  129. var perm access_model.Permission
  130. existPerm, ok := repoPerms[blocker.RepoID]
  131. if ok {
  132. perm = existPerm
  133. } else {
  134. var err error
  135. perm, err = access_model.GetUserRepoPermission(ctx, &blocker.Repository, ctx.Doer)
  136. if err != nil {
  137. ctx.ServerError("GetUserRepoPermission", err)
  138. return nil, nil
  139. }
  140. repoPerms[blocker.RepoID] = perm
  141. }
  142. if perm.CanReadIssuesOrPulls(blocker.Issue.IsPull) {
  143. canRead = append(canRead, blocker)
  144. } else {
  145. notPermitted = append(notPermitted, blocker)
  146. }
  147. }
  148. sortDependencyInfo(canRead)
  149. sortDependencyInfo(notPermitted)
  150. return canRead, notPermitted
  151. }
  152. func sortDependencyInfo(blockers []*issues_model.DependencyInfo) {
  153. sort.Slice(blockers, func(i, j int) bool {
  154. if blockers[i].RepoID == blockers[j].RepoID {
  155. return blockers[i].Issue.CreatedUnix < blockers[j].Issue.CreatedUnix
  156. }
  157. return blockers[i].RepoID < blockers[j].RepoID
  158. })
  159. }
  160. func addParticipant(poster *user_model.User, participants []*user_model.User) []*user_model.User {
  161. for _, part := range participants {
  162. if poster.ID == part.ID {
  163. return participants
  164. }
  165. }
  166. return append(participants, poster)
  167. }
  168. func filterXRefComments(ctx *context.Context, issue *issues_model.Issue) error {
  169. // Remove comments that the user has no permissions to see
  170. for i := 0; i < len(issue.Comments); {
  171. c := issue.Comments[i]
  172. if issues_model.CommentTypeIsRef(c.Type) && c.RefRepoID != issue.RepoID && c.RefRepoID != 0 {
  173. var err error
  174. // Set RefRepo for description in template
  175. c.RefRepo, err = repo_model.GetRepositoryByID(ctx, c.RefRepoID)
  176. if err != nil {
  177. return err
  178. }
  179. perm, err := access_model.GetUserRepoPermission(ctx, c.RefRepo, ctx.Doer)
  180. if err != nil {
  181. return err
  182. }
  183. if !perm.CanReadIssuesOrPulls(c.RefIsPull) {
  184. issue.Comments = append(issue.Comments[:i], issue.Comments[i+1:]...)
  185. continue
  186. }
  187. }
  188. i++
  189. }
  190. return nil
  191. }
  192. // combineLabelComments combine the nearby label comments as one.
  193. func combineLabelComments(issue *issues_model.Issue) {
  194. var prev, cur *issues_model.Comment
  195. for i := 0; i < len(issue.Comments); i++ {
  196. cur = issue.Comments[i]
  197. if i > 0 {
  198. prev = issue.Comments[i-1]
  199. }
  200. if i == 0 || cur.Type != issues_model.CommentTypeLabel ||
  201. (prev != nil && prev.PosterID != cur.PosterID) ||
  202. (prev != nil && cur.CreatedUnix-prev.CreatedUnix >= 60) {
  203. if cur.Type == issues_model.CommentTypeLabel && cur.Label != nil {
  204. if cur.Content != "1" {
  205. cur.RemovedLabels = append(cur.RemovedLabels, cur.Label)
  206. } else {
  207. cur.AddedLabels = append(cur.AddedLabels, cur.Label)
  208. }
  209. }
  210. continue
  211. }
  212. if cur.Label != nil { // now cur MUST be label comment
  213. if prev.Type == issues_model.CommentTypeLabel { // we can combine them only prev is a label comment
  214. if cur.Content != "1" {
  215. // remove labels from the AddedLabels list if the label that was removed is already
  216. // in this list, and if it's not in this list, add the label to RemovedLabels
  217. addedAndRemoved := false
  218. for i, label := range prev.AddedLabels {
  219. if cur.Label.ID == label.ID {
  220. prev.AddedLabels = append(prev.AddedLabels[:i], prev.AddedLabels[i+1:]...)
  221. addedAndRemoved = true
  222. break
  223. }
  224. }
  225. if !addedAndRemoved {
  226. prev.RemovedLabels = append(prev.RemovedLabels, cur.Label)
  227. }
  228. } else {
  229. // remove labels from the RemovedLabels list if the label that was added is already
  230. // in this list, and if it's not in this list, add the label to AddedLabels
  231. removedAndAdded := false
  232. for i, label := range prev.RemovedLabels {
  233. if cur.Label.ID == label.ID {
  234. prev.RemovedLabels = append(prev.RemovedLabels[:i], prev.RemovedLabels[i+1:]...)
  235. removedAndAdded = true
  236. break
  237. }
  238. }
  239. if !removedAndAdded {
  240. prev.AddedLabels = append(prev.AddedLabels, cur.Label)
  241. }
  242. }
  243. prev.CreatedUnix = cur.CreatedUnix
  244. // remove the current comment since it has been combined to prev comment
  245. issue.Comments = append(issue.Comments[:i], issue.Comments[i+1:]...)
  246. i--
  247. } else { // if prev is not a label comment, start a new group
  248. if cur.Content != "1" {
  249. cur.RemovedLabels = append(cur.RemovedLabels, cur.Label)
  250. } else {
  251. cur.AddedLabels = append(cur.AddedLabels, cur.Label)
  252. }
  253. }
  254. }
  255. }
  256. }
  257. func prepareIssueViewLoad(ctx *context.Context) *issues_model.Issue {
  258. issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.PathParamInt64("index"))
  259. if err != nil {
  260. ctx.NotFoundOrServerError("GetIssueByIndex", issues_model.IsErrIssueNotExist, err)
  261. return nil
  262. }
  263. issue.Repo = ctx.Repo.Repository
  264. ctx.Data["Issue"] = issue
  265. if err = issue.LoadPullRequest(ctx); err != nil {
  266. ctx.ServerError("LoadPullRequest", err)
  267. return nil
  268. }
  269. return issue
  270. }
  271. func handleViewIssueRedirectExternal(ctx *context.Context) {
  272. if ctx.PathParam("type") == "issues" {
  273. // If issue was requested we check if repo has external tracker and redirect
  274. extIssueUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypeExternalTracker)
  275. if err == nil && extIssueUnit != nil {
  276. if extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == markup.IssueNameStyleNumeric || extIssueUnit.ExternalTrackerConfig().ExternalTrackerStyle == "" {
  277. metas := ctx.Repo.Repository.ComposeCommentMetas(ctx)
  278. metas["index"] = ctx.PathParam("index")
  279. res, err := vars.Expand(extIssueUnit.ExternalTrackerConfig().ExternalTrackerFormat, metas)
  280. if err != nil {
  281. log.Error("unable to expand template vars for issue url. issue: %s, err: %v", metas["index"], err)
  282. ctx.ServerError("Expand", err)
  283. return
  284. }
  285. ctx.Redirect(res)
  286. return
  287. }
  288. } else if err != nil && !repo_model.IsErrUnitTypeNotExist(err) {
  289. ctx.ServerError("GetUnit", err)
  290. return
  291. }
  292. }
  293. }
  294. // ViewIssue render issue view page
  295. func ViewIssue(ctx *context.Context) {
  296. handleViewIssueRedirectExternal(ctx)
  297. if ctx.Written() {
  298. return
  299. }
  300. issue := prepareIssueViewLoad(ctx)
  301. if ctx.Written() {
  302. return
  303. }
  304. // Make sure type and URL matches.
  305. if ctx.PathParam("type") == "issues" && issue.IsPull {
  306. ctx.Redirect(issue.Link())
  307. return
  308. } else if ctx.PathParam("type") == "pulls" && !issue.IsPull {
  309. ctx.Redirect(issue.Link())
  310. return
  311. }
  312. if issue.IsPull {
  313. MustAllowPulls(ctx)
  314. if ctx.Written() {
  315. return
  316. }
  317. ctx.Data["PageIsPullList"] = true
  318. ctx.Data["PageIsPullConversation"] = true
  319. } else {
  320. MustEnableIssues(ctx)
  321. if ctx.Written() {
  322. return
  323. }
  324. ctx.Data["PageIsIssueList"] = true
  325. ctx.Data["NewIssueChooseTemplate"] = issue_service.HasTemplatesOrContactLinks(ctx.Repo.Repository, ctx.Repo.GitRepo)
  326. }
  327. ctx.Data["IsProjectsEnabled"] = ctx.Repo.CanRead(unit.TypeProjects)
  328. ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
  329. upload.AddUploadContext(ctx, "comment")
  330. if err := issue.LoadAttributes(ctx); err != nil {
  331. ctx.ServerError("LoadAttributes", err)
  332. return
  333. }
  334. if err := filterXRefComments(ctx, issue); err != nil {
  335. ctx.ServerError("filterXRefComments", err)
  336. return
  337. }
  338. ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, emoji.ReplaceAliases(issue.Title))
  339. if ctx.IsSigned {
  340. // Update issue-user.
  341. if err := activities_model.SetIssueReadBy(ctx, issue.ID, ctx.Doer.ID); err != nil {
  342. ctx.ServerError("ReadBy", err)
  343. return
  344. }
  345. }
  346. pageMetaData := retrieveRepoIssueMetaData(ctx, ctx.Repo.Repository, issue, issue.IsPull)
  347. if ctx.Written() {
  348. return
  349. }
  350. pageMetaData.LabelsData.SetSelectedLabels(issue.Labels)
  351. prepareFuncs := []func(*context.Context, *issues_model.Issue){
  352. prepareIssueViewContent,
  353. prepareIssueViewCommentsAndSidebarParticipants,
  354. prepareIssueViewSidebarWatch,
  355. prepareIssueViewSidebarTimeTracker,
  356. prepareIssueViewSidebarDependency,
  357. prepareIssueViewSidebarPin,
  358. func(ctx *context.Context, issue *issues_model.Issue) { preparePullViewPullInfo(ctx, issue) },
  359. preparePullViewReviewAndMerge,
  360. }
  361. for _, prepareFunc := range prepareFuncs {
  362. prepareFunc(ctx, issue)
  363. if ctx.Written() {
  364. return
  365. }
  366. }
  367. // Get more information if it's a pull request.
  368. if issue.IsPull {
  369. if issue.PullRequest.HasMerged {
  370. ctx.Data["DisableStatusChange"] = issue.PullRequest.HasMerged
  371. } else {
  372. ctx.Data["DisableStatusChange"] = ctx.Data["IsPullRequestBroken"] == true && issue.IsClosed
  373. }
  374. }
  375. ctx.Data["Reference"] = issue.Ref
  376. ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login?redirect_to=" + url.QueryEscape(ctx.Data["Link"].(string))
  377. ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.Doer.ID)
  378. ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
  379. ctx.Data["HasProjectsWritePermission"] = ctx.Repo.CanWrite(unit.TypeProjects)
  380. ctx.Data["IsRepoAdmin"] = ctx.IsSigned && (ctx.Repo.IsAdmin() || ctx.Doer.IsAdmin)
  381. ctx.Data["LockReasons"] = setting.Repository.Issue.LockReasons
  382. ctx.Data["RefEndName"] = git.RefName(issue.Ref).ShortName()
  383. tags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
  384. if err != nil {
  385. ctx.ServerError("GetTagNamesByRepoID", err)
  386. return
  387. }
  388. ctx.Data["Tags"] = tags
  389. ctx.Data["CanBlockUser"] = func(blocker, blockee *user_model.User) bool {
  390. return user_service.CanBlockUser(ctx, ctx.Doer, blocker, blockee)
  391. }
  392. if issue.PullRequest != nil && !issue.PullRequest.IsChecking() && !setting.IsProd {
  393. ctx.Data["PullMergeBoxReloadingInterval"] = 1 // in dev env, force using the reloading logic to make sure it won't break
  394. }
  395. ctx.HTML(http.StatusOK, tplIssueView)
  396. }
  397. func ViewPullMergeBox(ctx *context.Context) {
  398. issue := prepareIssueViewLoad(ctx)
  399. if !issue.IsPull {
  400. ctx.NotFound(nil)
  401. return
  402. }
  403. preparePullViewPullInfo(ctx, issue)
  404. preparePullViewReviewAndMerge(ctx, issue)
  405. ctx.Data["PullMergeBoxReloading"] = issue.PullRequest.IsChecking()
  406. // TODO: it should use a dedicated struct to render the pull merge box, to make sure all data is prepared correctly
  407. ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.Doer.ID)
  408. ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
  409. ctx.HTML(http.StatusOK, tplPullMergeBox)
  410. }
  411. func prepareIssueViewSidebarDependency(ctx *context.Context, issue *issues_model.Issue) {
  412. if issue.IsPull && !ctx.Repo.CanRead(unit.TypeIssues) {
  413. ctx.Data["IssueDependencySearchType"] = "pulls"
  414. } else if !issue.IsPull && !ctx.Repo.CanRead(unit.TypePullRequests) {
  415. ctx.Data["IssueDependencySearchType"] = "issues"
  416. } else {
  417. ctx.Data["IssueDependencySearchType"] = "all"
  418. }
  419. // Check if the user can use the dependencies
  420. ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx, ctx.Doer, issue.IsPull)
  421. // check if dependencies can be created across repositories
  422. ctx.Data["AllowCrossRepositoryDependencies"] = setting.Service.AllowCrossRepositoryDependencies
  423. // Get Dependencies
  424. blockedBy, err := issue.BlockedByDependencies(ctx, db.ListOptions{})
  425. if err != nil {
  426. ctx.ServerError("BlockedByDependencies", err)
  427. return
  428. }
  429. ctx.Data["BlockedByDependencies"], ctx.Data["BlockedByDependenciesNotPermitted"] = checkBlockedByIssues(ctx, blockedBy)
  430. if ctx.Written() {
  431. return
  432. }
  433. blocking, err := issue.BlockingDependencies(ctx)
  434. if err != nil {
  435. ctx.ServerError("BlockingDependencies", err)
  436. return
  437. }
  438. ctx.Data["BlockingDependencies"], ctx.Data["BlockingDependenciesNotPermitted"] = checkBlockedByIssues(ctx, blocking)
  439. }
  440. func preparePullViewSigning(ctx *context.Context, issue *issues_model.Issue) {
  441. if !issue.IsPull {
  442. return
  443. }
  444. pull := issue.PullRequest
  445. ctx.Data["WillSign"] = false
  446. if ctx.Doer != nil {
  447. sign, key, _, err := asymkey_service.SignMerge(ctx, pull, ctx.Doer, pull.BaseRepo.RepoPath(), pull.BaseBranch, pull.GetGitHeadRefName())
  448. ctx.Data["WillSign"] = sign
  449. ctx.Data["SigningKeyMergeDisplay"] = asymkey_model.GetDisplaySigningKey(key)
  450. if err != nil {
  451. if asymkey_service.IsErrWontSign(err) {
  452. ctx.Data["WontSignReason"] = err.(*asymkey_service.ErrWontSign).Reason
  453. } else {
  454. ctx.Data["WontSignReason"] = "error"
  455. log.Error("Error whilst checking if could sign pr %d in repo %s. Error: %v", pull.ID, pull.BaseRepo.FullName(), err)
  456. }
  457. }
  458. } else {
  459. ctx.Data["WontSignReason"] = "not_signed_in"
  460. }
  461. }
  462. func prepareIssueViewSidebarWatch(ctx *context.Context, issue *issues_model.Issue) {
  463. iw := new(issues_model.IssueWatch)
  464. if ctx.Doer != nil {
  465. iw.UserID = ctx.Doer.ID
  466. iw.IssueID = issue.ID
  467. var err error
  468. iw.IsWatching, err = issues_model.CheckIssueWatch(ctx, ctx.Doer, issue)
  469. if err != nil {
  470. ctx.ServerError("CheckIssueWatch", err)
  471. return
  472. }
  473. }
  474. ctx.Data["IssueWatch"] = iw
  475. }
  476. func prepareIssueViewSidebarTimeTracker(ctx *context.Context, issue *issues_model.Issue) {
  477. if !ctx.Repo.Repository.IsTimetrackerEnabled(ctx) {
  478. return
  479. }
  480. if ctx.IsSigned {
  481. // Deal with the stopwatch
  482. ctx.Data["IsStopwatchRunning"] = issues_model.StopwatchExists(ctx, ctx.Doer.ID, issue.ID)
  483. if !ctx.Data["IsStopwatchRunning"].(bool) {
  484. exists, _, swIssue, err := issues_model.HasUserStopwatch(ctx, ctx.Doer.ID)
  485. if err != nil {
  486. ctx.ServerError("HasUserStopwatch", err)
  487. return
  488. }
  489. ctx.Data["HasUserStopwatch"] = exists
  490. if exists {
  491. // Add warning if the user has already a stopwatch
  492. // Add link to the issue of the already running stopwatch
  493. ctx.Data["OtherStopwatchURL"] = swIssue.Link()
  494. }
  495. }
  496. ctx.Data["CanUseTimetracker"] = ctx.Repo.CanUseTimetracker(ctx, issue, ctx.Doer)
  497. } else {
  498. ctx.Data["CanUseTimetracker"] = false
  499. }
  500. var err error
  501. if ctx.Data["WorkingUsers"], err = issues_model.TotalTimesForEachUser(ctx, &issues_model.FindTrackedTimesOptions{IssueID: issue.ID}); err != nil {
  502. ctx.ServerError("TotalTimesForEachUser", err)
  503. return
  504. }
  505. }
  506. func preparePullViewDeleteBranch(ctx *context.Context, issue *issues_model.Issue, canDelete bool) {
  507. if !issue.IsPull {
  508. return
  509. }
  510. pull := issue.PullRequest
  511. isPullBranchDeletable := canDelete &&
  512. pull.HeadRepo != nil &&
  513. gitrepo.IsBranchExist(ctx, pull.HeadRepo, pull.HeadBranch) &&
  514. (!pull.HasMerged || ctx.Data["HeadBranchCommitID"] == ctx.Data["PullHeadCommitID"])
  515. if isPullBranchDeletable && pull.HasMerged {
  516. exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pull.HeadRepoID, pull.HeadBranch)
  517. if err != nil {
  518. ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err)
  519. return
  520. }
  521. isPullBranchDeletable = !exist
  522. }
  523. ctx.Data["IsPullBranchDeletable"] = isPullBranchDeletable
  524. }
  525. func prepareIssueViewSidebarPin(ctx *context.Context, issue *issues_model.Issue) {
  526. var pinAllowed bool
  527. if err := issue.LoadPinOrder(ctx); err != nil {
  528. ctx.ServerError("LoadPinOrder", err)
  529. return
  530. }
  531. if issue.PinOrder == 0 {
  532. var err error
  533. pinAllowed, err = issues_model.IsNewPinAllowed(ctx, issue.RepoID, issue.IsPull)
  534. if err != nil {
  535. ctx.ServerError("IsNewPinAllowed", err)
  536. return
  537. }
  538. } else {
  539. pinAllowed = true
  540. }
  541. ctx.Data["NewPinAllowed"] = pinAllowed
  542. ctx.Data["PinEnabled"] = setting.Repository.Issue.MaxPinned != 0
  543. }
  544. func prepareIssueViewCommentsAndSidebarParticipants(ctx *context.Context, issue *issues_model.Issue) {
  545. var (
  546. role issues_model.RoleDescriptor
  547. ok bool
  548. marked = make(map[int64]issues_model.RoleDescriptor)
  549. comment *issues_model.Comment
  550. participants = make([]*user_model.User, 1, 10)
  551. latestCloseCommentID int64
  552. err error
  553. )
  554. marked[issue.PosterID] = issue.ShowRole
  555. // Render comments and fetch participants.
  556. participants[0] = issue.Poster
  557. if err := issue.Comments.LoadAttachmentsByIssue(ctx); err != nil {
  558. ctx.ServerError("LoadAttachmentsByIssue", err)
  559. return
  560. }
  561. if err := issue.Comments.LoadPosters(ctx); err != nil {
  562. ctx.ServerError("LoadPosters", err)
  563. return
  564. }
  565. permCache := make(map[int64]access_model.Permission)
  566. for _, comment = range issue.Comments {
  567. comment.Issue = issue
  568. if comment.Type == issues_model.CommentTypeComment || comment.Type == issues_model.CommentTypeReview {
  569. rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo, renderhelper.RepoCommentOptions{
  570. FootnoteContextID: strconv.FormatInt(comment.ID, 10),
  571. })
  572. comment.RenderedContent, err = markdown.RenderString(rctx, comment.Content)
  573. if err != nil {
  574. ctx.ServerError("RenderString", err)
  575. return
  576. }
  577. // Check tag.
  578. role, ok = marked[comment.PosterID]
  579. if ok {
  580. comment.ShowRole = role
  581. continue
  582. }
  583. comment.ShowRole, err = roleDescriptor(ctx, issue.Repo, comment.Poster, permCache, issue, comment.HasOriginalAuthor())
  584. if err != nil {
  585. ctx.ServerError("roleDescriptor", err)
  586. return
  587. }
  588. marked[comment.PosterID] = comment.ShowRole
  589. participants = addParticipant(comment.Poster, participants)
  590. } else if comment.Type == issues_model.CommentTypeLabel {
  591. if err = comment.LoadLabel(ctx); err != nil {
  592. ctx.ServerError("LoadLabel", err)
  593. return
  594. }
  595. } else if comment.Type == issues_model.CommentTypeMilestone {
  596. if err = comment.LoadMilestone(ctx); err != nil {
  597. ctx.ServerError("LoadMilestone", err)
  598. return
  599. }
  600. ghostMilestone := &issues_model.Milestone{
  601. ID: -1,
  602. Name: ctx.Locale.TrString("repo.issues.deleted_milestone"),
  603. }
  604. if comment.OldMilestoneID > 0 && comment.OldMilestone == nil {
  605. comment.OldMilestone = ghostMilestone
  606. }
  607. if comment.MilestoneID > 0 && comment.Milestone == nil {
  608. comment.Milestone = ghostMilestone
  609. }
  610. } else if comment.Type == issues_model.CommentTypeProject {
  611. if err = comment.LoadProject(ctx); err != nil {
  612. ctx.ServerError("LoadProject", err)
  613. return
  614. }
  615. ghostProject := &project_model.Project{
  616. ID: project_model.GhostProjectID,
  617. Title: ctx.Locale.TrString("repo.issues.deleted_project"),
  618. }
  619. if comment.OldProjectID > 0 && comment.OldProject == nil {
  620. comment.OldProject = ghostProject
  621. }
  622. if comment.ProjectID > 0 && comment.Project == nil {
  623. comment.Project = ghostProject
  624. }
  625. } else if comment.Type == issues_model.CommentTypeProjectColumn {
  626. if err = comment.LoadProject(ctx); err != nil {
  627. ctx.ServerError("LoadProject", err)
  628. return
  629. }
  630. } else if comment.Type == issues_model.CommentTypeAssignees || comment.Type == issues_model.CommentTypeReviewRequest {
  631. if err = comment.LoadAssigneeUserAndTeam(ctx); err != nil {
  632. ctx.ServerError("LoadAssigneeUserAndTeam", err)
  633. return
  634. }
  635. } else if comment.Type == issues_model.CommentTypeRemoveDependency || comment.Type == issues_model.CommentTypeAddDependency {
  636. if err = comment.LoadDepIssueDetails(ctx); err != nil {
  637. if !issues_model.IsErrIssueNotExist(err) {
  638. ctx.ServerError("LoadDepIssueDetails", err)
  639. return
  640. }
  641. }
  642. } else if comment.Type.HasContentSupport() {
  643. rctx := renderhelper.NewRenderContextRepoComment(ctx, issue.Repo, renderhelper.RepoCommentOptions{
  644. FootnoteContextID: strconv.FormatInt(comment.ID, 10),
  645. })
  646. comment.RenderedContent, err = markdown.RenderString(rctx, comment.Content)
  647. if err != nil {
  648. ctx.ServerError("RenderString", err)
  649. return
  650. }
  651. if err = comment.LoadReview(ctx); err != nil && !issues_model.IsErrReviewNotExist(err) {
  652. ctx.ServerError("LoadReview", err)
  653. return
  654. }
  655. participants = addParticipant(comment.Poster, participants)
  656. if comment.Review == nil {
  657. continue
  658. }
  659. if err = comment.Review.LoadAttributes(ctx); err != nil {
  660. if !user_model.IsErrUserNotExist(err) {
  661. ctx.ServerError("Review.LoadAttributes", err)
  662. return
  663. }
  664. comment.Review.Reviewer = user_model.NewGhostUser()
  665. }
  666. if err = comment.Review.LoadCodeComments(ctx); err != nil {
  667. ctx.ServerError("Review.LoadCodeComments", err)
  668. return
  669. }
  670. for _, codeComments := range comment.Review.CodeComments {
  671. for _, lineComments := range codeComments {
  672. for _, c := range lineComments {
  673. // Check tag.
  674. role, ok = marked[c.PosterID]
  675. if ok {
  676. c.ShowRole = role
  677. continue
  678. }
  679. c.ShowRole, err = roleDescriptor(ctx, issue.Repo, c.Poster, permCache, issue, c.HasOriginalAuthor())
  680. if err != nil {
  681. ctx.ServerError("roleDescriptor", err)
  682. return
  683. }
  684. marked[c.PosterID] = c.ShowRole
  685. participants = addParticipant(c.Poster, participants)
  686. }
  687. }
  688. }
  689. if err = comment.LoadResolveDoer(ctx); err != nil {
  690. ctx.ServerError("LoadResolveDoer", err)
  691. return
  692. }
  693. } else if comment.Type == issues_model.CommentTypePullRequestPush {
  694. participants = addParticipant(comment.Poster, participants)
  695. if err = issue_service.LoadCommentPushCommits(ctx, comment); err != nil {
  696. ctx.ServerError("LoadCommentPushCommits", err)
  697. return
  698. }
  699. if !ctx.Repo.CanRead(unit.TypeActions) {
  700. for _, commit := range comment.Commits {
  701. if commit.Status == nil {
  702. continue
  703. }
  704. commit.Status.HideActionsURL(ctx)
  705. git_model.CommitStatusesHideActionsURL(ctx, commit.Statuses)
  706. }
  707. }
  708. } else if comment.Type == issues_model.CommentTypeAddTimeManual ||
  709. comment.Type == issues_model.CommentTypeStopTracking ||
  710. comment.Type == issues_model.CommentTypeDeleteTimeManual {
  711. // drop error since times could be pruned from DB..
  712. _ = comment.LoadTime(ctx)
  713. if comment.Content != "" {
  714. // Content before v1.21 did store the formatted string instead of seconds,
  715. // so "|" is used as delimiter to mark the new format
  716. if comment.Content[0] != '|' {
  717. // handle old time comments that have formatted text stored
  718. comment.RenderedContent = templates.SanitizeHTML(comment.Content)
  719. comment.Content = ""
  720. } else {
  721. // else it's just a duration in seconds to pass on to the frontend
  722. comment.Content = comment.Content[1:]
  723. }
  724. }
  725. }
  726. if comment.Type == issues_model.CommentTypeClose || comment.Type == issues_model.CommentTypeMergePull {
  727. // record ID of the latest closed/merged comment.
  728. // if PR is closed, the comments whose type is CommentTypePullRequestPush(29) after latestCloseCommentID won't be rendered.
  729. latestCloseCommentID = comment.ID
  730. }
  731. }
  732. ctx.Data["LatestCloseCommentID"] = latestCloseCommentID
  733. // Combine multiple label assignments into a single comment
  734. combineLabelComments(issue)
  735. var hiddenCommentTypes *big.Int
  736. if ctx.IsSigned {
  737. val, err := user_model.GetUserSetting(ctx, ctx.Doer.ID, user_model.SettingsKeyHiddenCommentTypes)
  738. if err != nil {
  739. ctx.ServerError("GetUserSetting", err)
  740. return
  741. }
  742. hiddenCommentTypes, _ = new(big.Int).SetString(val, 10) // we can safely ignore the failed conversion here
  743. }
  744. ctx.Data["ShouldShowCommentType"] = func(commentType issues_model.CommentType) bool {
  745. return hiddenCommentTypes == nil || hiddenCommentTypes.Bit(int(commentType)) == 0
  746. }
  747. // prepare for sidebar participants
  748. ctx.Data["Participants"] = participants
  749. ctx.Data["NumParticipants"] = len(participants)
  750. }
  751. func preparePullViewReviewAndMerge(ctx *context.Context, issue *issues_model.Issue) {
  752. getBranchData(ctx, issue)
  753. if !issue.IsPull {
  754. return
  755. }
  756. pull := issue.PullRequest
  757. pull.Issue = issue
  758. canDelete := false
  759. allowMerge := false
  760. canWriteToHeadRepo := false
  761. pull_service.StartPullRequestCheckOnView(ctx, pull)
  762. if ctx.IsSigned {
  763. if err := pull.LoadHeadRepo(ctx); err != nil {
  764. log.Error("LoadHeadRepo: %v", err)
  765. } else if pull.HeadRepo != nil {
  766. perm, err := access_model.GetUserRepoPermission(ctx, pull.HeadRepo, ctx.Doer)
  767. if err != nil {
  768. ctx.ServerError("GetUserRepoPermission", err)
  769. return
  770. }
  771. if perm.CanWrite(unit.TypeCode) {
  772. // Check if branch is not protected
  773. if pull.HeadBranch != pull.HeadRepo.DefaultBranch {
  774. if protected, err := git_model.IsBranchProtected(ctx, pull.HeadRepo.ID, pull.HeadBranch); err != nil {
  775. log.Error("IsProtectedBranch: %v", err)
  776. } else if !protected {
  777. canDelete = true
  778. ctx.Data["DeleteBranchLink"] = issue.Link() + "/cleanup"
  779. }
  780. }
  781. canWriteToHeadRepo = true
  782. }
  783. }
  784. if err := pull.LoadBaseRepo(ctx); err != nil {
  785. log.Error("LoadBaseRepo: %v", err)
  786. }
  787. perm, err := access_model.GetUserRepoPermission(ctx, pull.BaseRepo, ctx.Doer)
  788. if err != nil {
  789. ctx.ServerError("GetUserRepoPermission", err)
  790. return
  791. }
  792. if !canWriteToHeadRepo { // maintainers maybe allowed to push to head repo even if they can't write to it
  793. canWriteToHeadRepo = pull.AllowMaintainerEdit && perm.CanWrite(unit.TypeCode)
  794. }
  795. allowMerge, err = pull_service.IsUserAllowedToMerge(ctx, pull, perm, ctx.Doer)
  796. if err != nil {
  797. ctx.ServerError("IsUserAllowedToMerge", err)
  798. return
  799. }
  800. if ctx.Data["CanMarkConversation"], err = issues_model.CanMarkConversation(ctx, issue, ctx.Doer); err != nil {
  801. ctx.ServerError("CanMarkConversation", err)
  802. return
  803. }
  804. }
  805. ctx.Data["PullMergeBoxReloadingInterval"] = util.Iif(pull != nil && pull.IsChecking(), 2000, 0)
  806. ctx.Data["CanWriteToHeadRepo"] = canWriteToHeadRepo
  807. ctx.Data["ShowMergeInstructions"] = canWriteToHeadRepo
  808. ctx.Data["AllowMerge"] = allowMerge
  809. prUnit, err := issue.Repo.GetUnit(ctx, unit.TypePullRequests)
  810. if err != nil {
  811. ctx.ServerError("GetUnit", err)
  812. return
  813. }
  814. prConfig := prUnit.PullRequestsConfig()
  815. ctx.Data["AutodetectManualMerge"] = prConfig.AutodetectManualMerge
  816. var mergeStyle repo_model.MergeStyle
  817. // Check correct values and select default
  818. if ms, ok := ctx.Data["MergeStyle"].(repo_model.MergeStyle); !ok ||
  819. !prConfig.IsMergeStyleAllowed(ms) {
  820. defaultMergeStyle := prConfig.GetDefaultMergeStyle()
  821. if prConfig.IsMergeStyleAllowed(defaultMergeStyle) && !ok {
  822. mergeStyle = defaultMergeStyle
  823. } else if prConfig.AllowMerge {
  824. mergeStyle = repo_model.MergeStyleMerge
  825. } else if prConfig.AllowRebase {
  826. mergeStyle = repo_model.MergeStyleRebase
  827. } else if prConfig.AllowRebaseMerge {
  828. mergeStyle = repo_model.MergeStyleRebaseMerge
  829. } else if prConfig.AllowSquash {
  830. mergeStyle = repo_model.MergeStyleSquash
  831. } else if prConfig.AllowFastForwardOnly {
  832. mergeStyle = repo_model.MergeStyleFastForwardOnly
  833. } else if prConfig.AllowManualMerge {
  834. mergeStyle = repo_model.MergeStyleManuallyMerged
  835. }
  836. }
  837. ctx.Data["MergeStyle"] = mergeStyle
  838. defaultMergeMessage, defaultMergeBody, err := pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, mergeStyle)
  839. if err != nil {
  840. ctx.ServerError("GetDefaultMergeMessage", err)
  841. return
  842. }
  843. ctx.Data["DefaultMergeMessage"] = defaultMergeMessage
  844. ctx.Data["DefaultMergeBody"] = defaultMergeBody
  845. defaultSquashMergeMessage, defaultSquashMergeBody, err := pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, repo_model.MergeStyleSquash)
  846. if err != nil {
  847. ctx.ServerError("GetDefaultSquashMergeMessage", err)
  848. return
  849. }
  850. ctx.Data["DefaultSquashMergeMessage"] = defaultSquashMergeMessage
  851. ctx.Data["DefaultSquashMergeBody"] = defaultSquashMergeBody
  852. pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pull.BaseRepoID, pull.BaseBranch)
  853. if err != nil {
  854. ctx.ServerError("LoadProtectedBranch", err)
  855. return
  856. }
  857. if pb != nil {
  858. pb.Repo = pull.BaseRepo
  859. ctx.Data["ProtectedBranch"] = pb
  860. ctx.Data["IsBlockedByApprovals"] = !issues_model.HasEnoughApprovals(ctx, pb, pull)
  861. ctx.Data["IsBlockedByRejection"] = issues_model.MergeBlockedByRejectedReview(ctx, pb, pull)
  862. ctx.Data["IsBlockedByOfficialReviewRequests"] = issues_model.MergeBlockedByOfficialReviewRequests(ctx, pb, pull)
  863. ctx.Data["IsBlockedByOutdatedBranch"] = issues_model.MergeBlockedByOutdatedBranch(pb, pull)
  864. ctx.Data["GrantedApprovals"] = issues_model.GetGrantedApprovalsCount(ctx, pb, pull)
  865. ctx.Data["RequireSigned"] = pb.RequireSignedCommits
  866. ctx.Data["ChangedProtectedFiles"] = pull.ChangedProtectedFiles
  867. ctx.Data["IsBlockedByChangedProtectedFiles"] = len(pull.ChangedProtectedFiles) != 0
  868. ctx.Data["ChangedProtectedFilesNum"] = len(pull.ChangedProtectedFiles)
  869. ctx.Data["RequireApprovalsWhitelist"] = pb.EnableApprovalsWhitelist
  870. }
  871. preparePullViewSigning(ctx, issue)
  872. if ctx.Written() {
  873. return
  874. }
  875. preparePullViewDeleteBranch(ctx, issue, canDelete)
  876. if ctx.Written() {
  877. return
  878. }
  879. stillCanManualMerge := func() bool {
  880. if pull.HasMerged || issue.IsClosed || !ctx.IsSigned {
  881. return false
  882. }
  883. if pull.CanAutoMerge() || pull.IsWorkInProgress(ctx) || pull.IsChecking() {
  884. return false
  885. }
  886. if allowMerge && prConfig.AllowManualMerge {
  887. return true
  888. }
  889. return false
  890. }
  891. ctx.Data["StillCanManualMerge"] = stillCanManualMerge()
  892. // Check if there is a pending pr merge
  893. ctx.Data["HasPendingPullRequestMerge"], ctx.Data["PendingPullRequestMerge"], err = pull_model.GetScheduledMergeByPullID(ctx, pull.ID)
  894. if err != nil {
  895. ctx.ServerError("GetScheduledMergeByPullID", err)
  896. return
  897. }
  898. }
  899. func prepareIssueViewContent(ctx *context.Context, issue *issues_model.Issue) {
  900. var err error
  901. rctx := renderhelper.NewRenderContextRepoComment(ctx, ctx.Repo.Repository, renderhelper.RepoCommentOptions{
  902. FootnoteContextID: "0", // Set footnote context ID to 0 for the issue content
  903. })
  904. issue.RenderedContent, err = markdown.RenderString(rctx, issue.Content)
  905. if err != nil {
  906. ctx.ServerError("RenderString", err)
  907. return
  908. }
  909. if issue.ShowRole, err = roleDescriptor(ctx, issue.Repo, issue.Poster, nil, issue, issue.HasOriginalAuthor()); err != nil {
  910. ctx.ServerError("roleDescriptor", err)
  911. return
  912. }
  913. }