You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

issue.go 38KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Copyright 2018 The Gitea Authors. All rights reserved.
  3. // Use of this source code is governed by a MIT-style
  4. // license that can be found in the LICENSE file.
  5. package repo
  6. import (
  7. "bytes"
  8. "errors"
  9. "fmt"
  10. "io"
  11. "io/ioutil"
  12. "net/http"
  13. "strconv"
  14. "strings"
  15. "github.com/Unknwon/com"
  16. "github.com/Unknwon/paginater"
  17. "code.gitea.io/git"
  18. "code.gitea.io/gitea/models"
  19. "code.gitea.io/gitea/modules/auth"
  20. "code.gitea.io/gitea/modules/base"
  21. "code.gitea.io/gitea/modules/context"
  22. "code.gitea.io/gitea/modules/indexer"
  23. "code.gitea.io/gitea/modules/log"
  24. "code.gitea.io/gitea/modules/markup/markdown"
  25. "code.gitea.io/gitea/modules/notification"
  26. "code.gitea.io/gitea/modules/setting"
  27. "code.gitea.io/gitea/modules/util"
  28. )
  29. const (
  30. tplIssues base.TplName = "repo/issue/list"
  31. tplIssueNew base.TplName = "repo/issue/new"
  32. tplIssueView base.TplName = "repo/issue/view"
  33. tplReactions base.TplName = "repo/issue/view_content/reactions"
  34. issueTemplateKey = "IssueTemplate"
  35. )
  36. var (
  37. // ErrFileTypeForbidden not allowed file type error
  38. ErrFileTypeForbidden = errors.New("File type is not allowed")
  39. // ErrTooManyFiles upload too many files
  40. ErrTooManyFiles = errors.New("Maximum number of files to upload exceeded")
  41. // IssueTemplateCandidates issue templates
  42. IssueTemplateCandidates = []string{
  43. "ISSUE_TEMPLATE.md",
  44. "issue_template.md",
  45. ".gitea/ISSUE_TEMPLATE.md",
  46. ".gitea/issue_template.md",
  47. ".github/ISSUE_TEMPLATE.md",
  48. ".github/issue_template.md",
  49. }
  50. )
  51. // MustEnableIssues check if repository enable internal issues
  52. func MustEnableIssues(ctx *context.Context) {
  53. if !ctx.Repo.CanRead(models.UnitTypeIssues) &&
  54. !ctx.Repo.CanRead(models.UnitTypeExternalTracker) {
  55. ctx.NotFound("MustEnableIssues", nil)
  56. return
  57. }
  58. unit, err := ctx.Repo.Repository.GetUnit(models.UnitTypeExternalTracker)
  59. if err == nil {
  60. ctx.Redirect(unit.ExternalTrackerConfig().ExternalTrackerURL)
  61. return
  62. }
  63. }
  64. // MustAllowPulls check if repository enable pull requests and user have right to do that
  65. func MustAllowPulls(ctx *context.Context) {
  66. if !ctx.Repo.Repository.CanEnablePulls() || !ctx.Repo.CanRead(models.UnitTypePullRequests) {
  67. ctx.NotFound("MustAllowPulls", nil)
  68. return
  69. }
  70. // User can send pull request if owns a forked repository.
  71. if ctx.IsSigned && ctx.User.HasForkedRepo(ctx.Repo.Repository.ID) {
  72. ctx.Repo.PullRequest.Allowed = true
  73. ctx.Repo.PullRequest.HeadInfo = ctx.User.Name + ":" + ctx.Repo.BranchName
  74. }
  75. }
  76. func issues(ctx *context.Context, milestoneID int64, isPullOption util.OptionalBool) {
  77. var err error
  78. viewType := ctx.Query("type")
  79. sortType := ctx.Query("sort")
  80. types := []string{"all", "your_repositories", "assigned", "created_by", "mentioned"}
  81. if !com.IsSliceContainsStr(types, viewType) {
  82. viewType = "all"
  83. }
  84. var (
  85. assigneeID = ctx.QueryInt64("assignee")
  86. posterID int64
  87. mentionedID int64
  88. forceEmpty bool
  89. )
  90. if ctx.IsSigned {
  91. switch viewType {
  92. case "created_by":
  93. posterID = ctx.User.ID
  94. case "mentioned":
  95. mentionedID = ctx.User.ID
  96. }
  97. }
  98. repo := ctx.Repo.Repository
  99. selectLabels := ctx.Query("labels")
  100. isShowClosed := ctx.Query("state") == "closed"
  101. keyword := strings.Trim(ctx.Query("q"), " ")
  102. if bytes.Contains([]byte(keyword), []byte{0x00}) {
  103. keyword = ""
  104. }
  105. var issueIDs []int64
  106. if len(keyword) > 0 {
  107. issueIDs, err = indexer.SearchIssuesByKeyword(repo.ID, keyword)
  108. if len(issueIDs) == 0 {
  109. forceEmpty = true
  110. }
  111. }
  112. var issueStats *models.IssueStats
  113. if forceEmpty {
  114. issueStats = &models.IssueStats{}
  115. } else {
  116. issueStats, err = models.GetIssueStats(&models.IssueStatsOptions{
  117. RepoID: repo.ID,
  118. Labels: selectLabels,
  119. MilestoneID: milestoneID,
  120. AssigneeID: assigneeID,
  121. MentionedID: mentionedID,
  122. PosterID: posterID,
  123. IsPull: isPullOption,
  124. IssueIDs: issueIDs,
  125. })
  126. if err != nil {
  127. ctx.ServerError("GetIssueStats", err)
  128. return
  129. }
  130. }
  131. page := ctx.QueryInt("page")
  132. if page <= 1 {
  133. page = 1
  134. }
  135. var total int
  136. if !isShowClosed {
  137. total = int(issueStats.OpenCount)
  138. } else {
  139. total = int(issueStats.ClosedCount)
  140. }
  141. pager := paginater.New(total, setting.UI.IssuePagingNum, page, 5)
  142. ctx.Data["Page"] = pager
  143. var issues []*models.Issue
  144. if forceEmpty {
  145. issues = []*models.Issue{}
  146. } else {
  147. issues, err = models.Issues(&models.IssuesOptions{
  148. RepoIDs: []int64{repo.ID},
  149. AssigneeID: assigneeID,
  150. PosterID: posterID,
  151. MentionedID: mentionedID,
  152. MilestoneID: milestoneID,
  153. Page: pager.Current(),
  154. PageSize: setting.UI.IssuePagingNum,
  155. IsClosed: util.OptionalBoolOf(isShowClosed),
  156. IsPull: isPullOption,
  157. Labels: selectLabels,
  158. SortType: sortType,
  159. IssueIDs: issueIDs,
  160. })
  161. if err != nil {
  162. ctx.ServerError("Issues", err)
  163. return
  164. }
  165. }
  166. // Get posters.
  167. for i := range issues {
  168. // Check read status
  169. if !ctx.IsSigned {
  170. issues[i].IsRead = true
  171. } else if err = issues[i].GetIsRead(ctx.User.ID); err != nil {
  172. ctx.ServerError("GetIsRead", err)
  173. return
  174. }
  175. }
  176. ctx.Data["Issues"] = issues
  177. // Get assignees.
  178. ctx.Data["Assignees"], err = repo.GetAssignees()
  179. if err != nil {
  180. ctx.ServerError("GetAssignees", err)
  181. return
  182. }
  183. labels, err := models.GetLabelsByRepoID(repo.ID, "")
  184. if err != nil {
  185. ctx.ServerError("GetLabelsByRepoID", err)
  186. return
  187. }
  188. ctx.Data["Labels"] = labels
  189. if ctx.QueryInt64("assignee") == 0 {
  190. assigneeID = 0 // Reset ID to prevent unexpected selection of assignee.
  191. }
  192. ctx.Data["IssueStats"] = issueStats
  193. ctx.Data["SelectLabels"] = com.StrTo(selectLabels).MustInt64()
  194. ctx.Data["ViewType"] = viewType
  195. ctx.Data["SortType"] = sortType
  196. ctx.Data["MilestoneID"] = milestoneID
  197. ctx.Data["AssigneeID"] = assigneeID
  198. ctx.Data["IsShowClosed"] = isShowClosed
  199. ctx.Data["Keyword"] = keyword
  200. if isShowClosed {
  201. ctx.Data["State"] = "closed"
  202. } else {
  203. ctx.Data["State"] = "open"
  204. }
  205. }
  206. // Issues render issues page
  207. func Issues(ctx *context.Context) {
  208. isPullList := ctx.Params(":type") == "pulls"
  209. if isPullList {
  210. MustAllowPulls(ctx)
  211. if ctx.Written() {
  212. return
  213. }
  214. ctx.Data["Title"] = ctx.Tr("repo.pulls")
  215. ctx.Data["PageIsPullList"] = true
  216. } else {
  217. MustEnableIssues(ctx)
  218. if ctx.Written() {
  219. return
  220. }
  221. ctx.Data["Title"] = ctx.Tr("repo.issues")
  222. ctx.Data["PageIsIssueList"] = true
  223. }
  224. issues(ctx, ctx.QueryInt64("milestone"), util.OptionalBoolOf(isPullList))
  225. var err error
  226. // Get milestones.
  227. ctx.Data["Milestones"], err = models.GetMilestonesByRepoID(ctx.Repo.Repository.ID)
  228. if err != nil {
  229. ctx.ServerError("GetAllRepoMilestones", err)
  230. return
  231. }
  232. ctx.HTML(200, tplIssues)
  233. }
  234. // RetrieveRepoMilestonesAndAssignees find all the milestones and assignees of a repository
  235. func RetrieveRepoMilestonesAndAssignees(ctx *context.Context, repo *models.Repository) {
  236. var err error
  237. ctx.Data["OpenMilestones"], err = models.GetMilestones(repo.ID, -1, false, "")
  238. if err != nil {
  239. ctx.ServerError("GetMilestones", err)
  240. return
  241. }
  242. ctx.Data["ClosedMilestones"], err = models.GetMilestones(repo.ID, -1, true, "")
  243. if err != nil {
  244. ctx.ServerError("GetMilestones", err)
  245. return
  246. }
  247. ctx.Data["Assignees"], err = repo.GetAssignees()
  248. if err != nil {
  249. ctx.ServerError("GetAssignees", err)
  250. return
  251. }
  252. }
  253. // RetrieveRepoMetas find all the meta information of a repository
  254. func RetrieveRepoMetas(ctx *context.Context, repo *models.Repository) []*models.Label {
  255. if !ctx.Repo.CanWrite(models.UnitTypeIssues) {
  256. return nil
  257. }
  258. labels, err := models.GetLabelsByRepoID(repo.ID, "")
  259. if err != nil {
  260. ctx.ServerError("GetLabelsByRepoID", err)
  261. return nil
  262. }
  263. ctx.Data["Labels"] = labels
  264. RetrieveRepoMilestonesAndAssignees(ctx, repo)
  265. if ctx.Written() {
  266. return nil
  267. }
  268. brs, err := ctx.Repo.GitRepo.GetBranches()
  269. if err != nil {
  270. ctx.ServerError("GetBranches", err)
  271. return nil
  272. }
  273. ctx.Data["Branches"] = brs
  274. // Contains true if the user can create issue dependencies
  275. ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx.User)
  276. return labels
  277. }
  278. func getFileContentFromDefaultBranch(ctx *context.Context, filename string) (string, bool) {
  279. var r io.Reader
  280. var bytes []byte
  281. if ctx.Repo.Commit == nil {
  282. var err error
  283. ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
  284. if err != nil {
  285. return "", false
  286. }
  287. }
  288. entry, err := ctx.Repo.Commit.GetTreeEntryByPath(filename)
  289. if err != nil {
  290. return "", false
  291. }
  292. if entry.Blob().Size() >= setting.UI.MaxDisplayFileSize {
  293. return "", false
  294. }
  295. r, err = entry.Blob().Data()
  296. if err != nil {
  297. return "", false
  298. }
  299. bytes, err = ioutil.ReadAll(r)
  300. if err != nil {
  301. return "", false
  302. }
  303. return string(bytes), true
  304. }
  305. func setTemplateIfExists(ctx *context.Context, ctxDataKey string, possibleFiles []string) {
  306. for _, filename := range possibleFiles {
  307. content, found := getFileContentFromDefaultBranch(ctx, filename)
  308. if found {
  309. ctx.Data[ctxDataKey] = content
  310. return
  311. }
  312. }
  313. }
  314. // NewIssue render createing issue page
  315. func NewIssue(ctx *context.Context) {
  316. ctx.Data["Title"] = ctx.Tr("repo.issues.new")
  317. ctx.Data["PageIsIssueList"] = true
  318. ctx.Data["RequireHighlightJS"] = true
  319. ctx.Data["RequireSimpleMDE"] = true
  320. ctx.Data["RequireTribute"] = true
  321. ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
  322. milestoneID := ctx.QueryInt64("milestone")
  323. milestone, err := models.GetMilestoneByID(milestoneID)
  324. if err != nil {
  325. log.Error(4, "GetMilestoneByID: %d: %v", milestoneID, err)
  326. } else {
  327. ctx.Data["milestone_id"] = milestoneID
  328. ctx.Data["Milestone"] = milestone
  329. }
  330. setTemplateIfExists(ctx, issueTemplateKey, IssueTemplateCandidates)
  331. renderAttachmentSettings(ctx)
  332. RetrieveRepoMetas(ctx, ctx.Repo.Repository)
  333. if ctx.Written() {
  334. return
  335. }
  336. ctx.HTML(200, tplIssueNew)
  337. }
  338. // ValidateRepoMetas check and returns repository's meta informations
  339. func ValidateRepoMetas(ctx *context.Context, form auth.CreateIssueForm, isPull bool) ([]int64, []int64, int64) {
  340. var (
  341. repo = ctx.Repo.Repository
  342. err error
  343. )
  344. labels := RetrieveRepoMetas(ctx, ctx.Repo.Repository)
  345. if ctx.Written() {
  346. return nil, nil, 0
  347. }
  348. var labelIDs []int64
  349. hasSelected := false
  350. // Check labels.
  351. if len(form.LabelIDs) > 0 {
  352. labelIDs, err = base.StringsToInt64s(strings.Split(form.LabelIDs, ","))
  353. if err != nil {
  354. return nil, nil, 0
  355. }
  356. labelIDMark := base.Int64sToMap(labelIDs)
  357. for i := range labels {
  358. if labelIDMark[labels[i].ID] {
  359. labels[i].IsChecked = true
  360. hasSelected = true
  361. }
  362. }
  363. }
  364. ctx.Data["Labels"] = labels
  365. ctx.Data["HasSelectedLabel"] = hasSelected
  366. ctx.Data["label_ids"] = form.LabelIDs
  367. // Check milestone.
  368. milestoneID := form.MilestoneID
  369. if milestoneID > 0 {
  370. ctx.Data["Milestone"], err = repo.GetMilestoneByID(milestoneID)
  371. if err != nil {
  372. ctx.ServerError("GetMilestoneByID", err)
  373. return nil, nil, 0
  374. }
  375. ctx.Data["milestone_id"] = milestoneID
  376. }
  377. // Check assignees
  378. var assigneeIDs []int64
  379. if len(form.AssigneeIDs) > 0 {
  380. assigneeIDs, err = base.StringsToInt64s(strings.Split(form.AssigneeIDs, ","))
  381. if err != nil {
  382. return nil, nil, 0
  383. }
  384. // Check if the passed assignees actually exists and has write access to the repo
  385. for _, aID := range assigneeIDs {
  386. user, err := models.GetUserByID(aID)
  387. if err != nil {
  388. ctx.ServerError("GetUserByID", err)
  389. return nil, nil, 0
  390. }
  391. perm, err := models.GetUserRepoPermission(repo, user)
  392. if err != nil {
  393. ctx.ServerError("GetUserRepoPermission", err)
  394. return nil, nil, 0
  395. }
  396. if !perm.CanWriteIssuesOrPulls(isPull) {
  397. ctx.ServerError("CanWriteIssuesOrPulls", fmt.Errorf("No permission for %s", user.Name))
  398. return nil, nil, 0
  399. }
  400. }
  401. }
  402. // Keep the old assignee id thingy for compatibility reasons
  403. if form.AssigneeID > 0 {
  404. assigneeIDs = append(assigneeIDs, form.AssigneeID)
  405. }
  406. return labelIDs, assigneeIDs, milestoneID
  407. }
  408. // NewIssuePost response for creating new issue
  409. func NewIssuePost(ctx *context.Context, form auth.CreateIssueForm) {
  410. ctx.Data["Title"] = ctx.Tr("repo.issues.new")
  411. ctx.Data["PageIsIssueList"] = true
  412. ctx.Data["RequireHighlightJS"] = true
  413. ctx.Data["RequireSimpleMDE"] = true
  414. ctx.Data["ReadOnly"] = false
  415. ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
  416. renderAttachmentSettings(ctx)
  417. var (
  418. repo = ctx.Repo.Repository
  419. attachments []string
  420. )
  421. labelIDs, assigneeIDs, milestoneID := ValidateRepoMetas(ctx, form, false)
  422. if ctx.Written() {
  423. return
  424. }
  425. if setting.AttachmentEnabled {
  426. attachments = form.Files
  427. }
  428. if ctx.HasError() {
  429. ctx.HTML(200, tplIssueNew)
  430. return
  431. }
  432. issue := &models.Issue{
  433. RepoID: repo.ID,
  434. Title: form.Title,
  435. PosterID: ctx.User.ID,
  436. Poster: ctx.User,
  437. MilestoneID: milestoneID,
  438. Content: form.Content,
  439. Ref: form.Ref,
  440. }
  441. if err := models.NewIssue(repo, issue, labelIDs, assigneeIDs, attachments); err != nil {
  442. if models.IsErrUserDoesNotHaveAccessToRepo(err) {
  443. ctx.Error(400, "UserDoesNotHaveAccessToRepo", err.Error())
  444. return
  445. }
  446. ctx.ServerError("NewIssue", err)
  447. return
  448. }
  449. notification.NotifyNewIssue(issue)
  450. log.Trace("Issue created: %d/%d", repo.ID, issue.ID)
  451. ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + com.ToStr(issue.Index))
  452. }
  453. // commentTag returns the CommentTag for a comment in/with the given repo, poster and issue
  454. func commentTag(repo *models.Repository, poster *models.User, issue *models.Issue) (models.CommentTag, error) {
  455. perm, err := models.GetUserRepoPermission(repo, poster)
  456. if err != nil {
  457. return models.CommentTagNone, err
  458. }
  459. if perm.IsOwner() {
  460. return models.CommentTagOwner, nil
  461. } else if poster.ID == issue.PosterID {
  462. return models.CommentTagPoster, nil
  463. } else if perm.CanWrite(models.UnitTypeCode) {
  464. return models.CommentTagWriter, nil
  465. }
  466. return models.CommentTagNone, nil
  467. }
  468. // ViewIssue render issue view page
  469. func ViewIssue(ctx *context.Context) {
  470. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  471. if err != nil {
  472. if models.IsErrIssueNotExist(err) {
  473. ctx.NotFound("GetIssueByIndex", err)
  474. } else {
  475. ctx.ServerError("GetIssueByIndex", err)
  476. }
  477. return
  478. }
  479. // Make sure type and URL matches.
  480. if ctx.Params(":type") == "issues" && issue.IsPull {
  481. ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index))
  482. return
  483. } else if ctx.Params(":type") == "pulls" && !issue.IsPull {
  484. ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + com.ToStr(issue.Index))
  485. return
  486. }
  487. if issue.IsPull {
  488. MustAllowPulls(ctx)
  489. if ctx.Written() {
  490. return
  491. }
  492. ctx.Data["PageIsPullList"] = true
  493. ctx.Data["PageIsPullConversation"] = true
  494. } else {
  495. MustEnableIssues(ctx)
  496. if ctx.Written() {
  497. return
  498. }
  499. ctx.Data["PageIsIssueList"] = true
  500. }
  501. ctx.Data["RequireHighlightJS"] = true
  502. ctx.Data["RequireDropzone"] = true
  503. ctx.Data["RequireTribute"] = true
  504. renderAttachmentSettings(ctx)
  505. err = issue.LoadAttributes()
  506. if err != nil {
  507. ctx.ServerError("GetIssueByIndex", err)
  508. return
  509. }
  510. ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, issue.Title)
  511. var iw *models.IssueWatch
  512. var exists bool
  513. if ctx.User != nil {
  514. iw, exists, err = models.GetIssueWatch(ctx.User.ID, issue.ID)
  515. if err != nil {
  516. ctx.ServerError("GetIssueWatch", err)
  517. return
  518. }
  519. if !exists {
  520. iw = &models.IssueWatch{
  521. UserID: ctx.User.ID,
  522. IssueID: issue.ID,
  523. IsWatching: models.IsWatching(ctx.User.ID, ctx.Repo.Repository.ID),
  524. }
  525. }
  526. }
  527. ctx.Data["IssueWatch"] = iw
  528. issue.RenderedContent = string(markdown.Render([]byte(issue.Content), ctx.Repo.RepoLink,
  529. ctx.Repo.Repository.ComposeMetas()))
  530. repo := ctx.Repo.Repository
  531. // Get more information if it's a pull request.
  532. if issue.IsPull {
  533. if issue.PullRequest.HasMerged {
  534. ctx.Data["DisableStatusChange"] = issue.PullRequest.HasMerged
  535. PrepareMergedViewPullInfo(ctx, issue)
  536. } else {
  537. PrepareViewPullInfo(ctx, issue)
  538. }
  539. if ctx.Written() {
  540. return
  541. }
  542. }
  543. // Metas.
  544. // Check labels.
  545. labelIDMark := make(map[int64]bool)
  546. for i := range issue.Labels {
  547. labelIDMark[issue.Labels[i].ID] = true
  548. }
  549. labels, err := models.GetLabelsByRepoID(repo.ID, "")
  550. if err != nil {
  551. ctx.ServerError("GetLabelsByRepoID", err)
  552. return
  553. }
  554. hasSelected := false
  555. for i := range labels {
  556. if labelIDMark[labels[i].ID] {
  557. labels[i].IsChecked = true
  558. hasSelected = true
  559. }
  560. }
  561. ctx.Data["HasSelectedLabel"] = hasSelected
  562. ctx.Data["Labels"] = labels
  563. // Check milestone and assignee.
  564. if ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) {
  565. RetrieveRepoMilestonesAndAssignees(ctx, repo)
  566. if ctx.Written() {
  567. return
  568. }
  569. }
  570. if ctx.IsSigned {
  571. // Update issue-user.
  572. if err = issue.ReadBy(ctx.User.ID); err != nil {
  573. ctx.ServerError("ReadBy", err)
  574. return
  575. }
  576. }
  577. var (
  578. tag models.CommentTag
  579. ok bool
  580. marked = make(map[int64]models.CommentTag)
  581. comment *models.Comment
  582. participants = make([]*models.User, 1, 10)
  583. )
  584. if ctx.Repo.Repository.IsTimetrackerEnabled() {
  585. if ctx.IsSigned {
  586. // Deal with the stopwatch
  587. ctx.Data["IsStopwatchRunning"] = models.StopwatchExists(ctx.User.ID, issue.ID)
  588. if !ctx.Data["IsStopwatchRunning"].(bool) {
  589. var exists bool
  590. var sw *models.Stopwatch
  591. if exists, sw, err = models.HasUserStopwatch(ctx.User.ID); err != nil {
  592. ctx.ServerError("HasUserStopwatch", err)
  593. return
  594. }
  595. ctx.Data["HasUserStopwatch"] = exists
  596. if exists {
  597. // Add warning if the user has already a stopwatch
  598. var otherIssue *models.Issue
  599. if otherIssue, err = models.GetIssueByID(sw.IssueID); err != nil {
  600. ctx.ServerError("GetIssueByID", err)
  601. return
  602. }
  603. if err = otherIssue.LoadRepo(); err != nil {
  604. ctx.ServerError("LoadRepo", err)
  605. return
  606. }
  607. // Add link to the issue of the already running stopwatch
  608. ctx.Data["OtherStopwatchURL"] = otherIssue.HTMLURL()
  609. }
  610. }
  611. ctx.Data["CanUseTimetracker"] = ctx.Repo.CanUseTimetracker(issue, ctx.User)
  612. } else {
  613. ctx.Data["CanUseTimetracker"] = false
  614. }
  615. if ctx.Data["WorkingUsers"], err = models.TotalTimes(models.FindTrackedTimesOptions{IssueID: issue.ID}); err != nil {
  616. ctx.ServerError("TotalTimes", err)
  617. return
  618. }
  619. }
  620. // Check if the user can use the dependencies
  621. ctx.Data["CanCreateIssueDependencies"] = ctx.Repo.CanCreateIssueDependencies(ctx.User)
  622. // Render comments and and fetch participants.
  623. participants[0] = issue.Poster
  624. for _, comment = range issue.Comments {
  625. if err := comment.LoadPoster(); err != nil {
  626. ctx.ServerError("LoadPoster", err)
  627. return
  628. }
  629. if comment.Type == models.CommentTypeComment {
  630. if err := comment.LoadAttachments(); err != nil {
  631. ctx.ServerError("LoadAttachments", err)
  632. return
  633. }
  634. comment.RenderedContent = string(markdown.Render([]byte(comment.Content), ctx.Repo.RepoLink,
  635. ctx.Repo.Repository.ComposeMetas()))
  636. // Check tag.
  637. tag, ok = marked[comment.PosterID]
  638. if ok {
  639. comment.ShowTag = tag
  640. continue
  641. }
  642. comment.ShowTag, err = commentTag(repo, comment.Poster, issue)
  643. if err != nil {
  644. ctx.ServerError("commentTag", err)
  645. return
  646. }
  647. marked[comment.PosterID] = comment.ShowTag
  648. isAdded := false
  649. for j := range participants {
  650. if comment.Poster == participants[j] {
  651. isAdded = true
  652. break
  653. }
  654. }
  655. if !isAdded && !issue.IsPoster(comment.Poster.ID) {
  656. participants = append(participants, comment.Poster)
  657. }
  658. } else if comment.Type == models.CommentTypeLabel {
  659. if err = comment.LoadLabel(); err != nil {
  660. ctx.ServerError("LoadLabel", err)
  661. return
  662. }
  663. } else if comment.Type == models.CommentTypeMilestone {
  664. if err = comment.LoadMilestone(); err != nil {
  665. ctx.ServerError("LoadMilestone", err)
  666. return
  667. }
  668. ghostMilestone := &models.Milestone{
  669. ID: -1,
  670. Name: ctx.Tr("repo.issues.deleted_milestone"),
  671. }
  672. if comment.OldMilestoneID > 0 && comment.OldMilestone == nil {
  673. comment.OldMilestone = ghostMilestone
  674. }
  675. if comment.MilestoneID > 0 && comment.Milestone == nil {
  676. comment.Milestone = ghostMilestone
  677. }
  678. } else if comment.Type == models.CommentTypeAssignees {
  679. if err = comment.LoadAssigneeUser(); err != nil {
  680. ctx.ServerError("LoadAssigneeUser", err)
  681. return
  682. }
  683. } else if comment.Type == models.CommentTypeRemoveDependency || comment.Type == models.CommentTypeAddDependency {
  684. if err = comment.LoadDepIssueDetails(); err != nil {
  685. ctx.ServerError("LoadDepIssueDetails", err)
  686. return
  687. }
  688. } else if comment.Type == models.CommentTypeCode || comment.Type == models.CommentTypeReview {
  689. if err = comment.LoadReview(); err != nil && !models.IsErrReviewNotExist(err) {
  690. ctx.ServerError("LoadReview", err)
  691. return
  692. }
  693. if comment.Review == nil {
  694. continue
  695. }
  696. if err = comment.Review.LoadAttributes(); err != nil {
  697. ctx.ServerError("Review.LoadAttributes", err)
  698. return
  699. }
  700. if err = comment.Review.LoadCodeComments(); err != nil {
  701. ctx.ServerError("Review.LoadCodeComments", err)
  702. return
  703. }
  704. }
  705. }
  706. if issue.IsPull {
  707. pull := issue.PullRequest
  708. pull.Issue = issue
  709. canDelete := false
  710. if ctx.IsSigned {
  711. if err := pull.GetHeadRepo(); err != nil {
  712. log.Error(4, "GetHeadRepo: %v", err)
  713. } else if pull.HeadRepo != nil && pull.HeadBranch != pull.HeadRepo.DefaultBranch {
  714. perm, err := models.GetUserRepoPermission(pull.HeadRepo, ctx.User)
  715. if err != nil {
  716. ctx.ServerError("GetUserRepoPermission", err)
  717. return
  718. }
  719. if perm.CanWrite(models.UnitTypeCode) {
  720. // Check if branch is not protected
  721. if protected, err := pull.HeadRepo.IsProtectedBranch(pull.HeadBranch, ctx.User); err != nil {
  722. log.Error(4, "IsProtectedBranch: %v", err)
  723. } else if !protected {
  724. canDelete = true
  725. ctx.Data["DeleteBranchLink"] = ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index) + "/cleanup"
  726. }
  727. }
  728. }
  729. }
  730. prUnit, err := repo.GetUnit(models.UnitTypePullRequests)
  731. if err != nil {
  732. ctx.ServerError("GetUnit", err)
  733. return
  734. }
  735. prConfig := prUnit.PullRequestsConfig()
  736. ctx.Data["AllowMerge"] = ctx.Repo.CanWrite(models.UnitTypeCode)
  737. if err := pull.CheckUserAllowedToMerge(ctx.User); err != nil {
  738. if !models.IsErrNotAllowedToMerge(err) {
  739. ctx.ServerError("CheckUserAllowedToMerge", err)
  740. return
  741. }
  742. ctx.Data["AllowMerge"] = false
  743. }
  744. // Check correct values and select default
  745. if ms, ok := ctx.Data["MergeStyle"].(models.MergeStyle); !ok ||
  746. !prConfig.IsMergeStyleAllowed(ms) {
  747. if prConfig.AllowMerge {
  748. ctx.Data["MergeStyle"] = models.MergeStyleMerge
  749. } else if prConfig.AllowRebase {
  750. ctx.Data["MergeStyle"] = models.MergeStyleRebase
  751. } else if prConfig.AllowSquash {
  752. ctx.Data["MergeStyle"] = models.MergeStyleSquash
  753. } else {
  754. ctx.Data["MergeStyle"] = ""
  755. }
  756. }
  757. if err = pull.LoadProtectedBranch(); err != nil {
  758. ctx.ServerError("LoadProtectedBranch", err)
  759. return
  760. }
  761. if pull.ProtectedBranch != nil {
  762. cnt := pull.ProtectedBranch.GetGrantedApprovalsCount(pull)
  763. ctx.Data["IsBlockedByApprovals"] = pull.ProtectedBranch.RequiredApprovals > 0 && cnt < pull.ProtectedBranch.RequiredApprovals
  764. ctx.Data["GrantedApprovals"] = cnt
  765. }
  766. ctx.Data["IsPullBranchDeletable"] = canDelete && pull.HeadRepo != nil && git.IsBranchExist(pull.HeadRepo.RepoPath(), pull.HeadBranch)
  767. ctx.Data["PullReviewersWithType"], err = models.GetReviewersByPullID(issue.ID)
  768. if err != nil {
  769. ctx.ServerError("GetReviewersByPullID", err)
  770. return
  771. }
  772. }
  773. // Get Dependencies
  774. ctx.Data["BlockedByDependencies"], err = issue.BlockedByDependencies()
  775. ctx.Data["BlockingDependencies"], err = issue.BlockingDependencies()
  776. ctx.Data["Participants"] = participants
  777. ctx.Data["NumParticipants"] = len(participants)
  778. ctx.Data["Issue"] = issue
  779. ctx.Data["ReadOnly"] = true
  780. ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login?redirect_to=" + ctx.Data["Link"].(string)
  781. ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.User.ID)
  782. ctx.Data["IsIssueWriter"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
  783. ctx.HTML(200, tplIssueView)
  784. }
  785. // GetActionIssue will return the issue which is used in the context.
  786. func GetActionIssue(ctx *context.Context) *models.Issue {
  787. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  788. if err != nil {
  789. ctx.NotFoundOrServerError("GetIssueByIndex", models.IsErrIssueNotExist, err)
  790. return nil
  791. }
  792. issue.Repo = ctx.Repo.Repository
  793. checkIssueRights(ctx, issue)
  794. if ctx.Written() {
  795. return nil
  796. }
  797. if err = issue.LoadAttributes(); err != nil {
  798. ctx.ServerError("LoadAttributes", nil)
  799. return nil
  800. }
  801. return issue
  802. }
  803. func checkIssueRights(ctx *context.Context, issue *models.Issue) {
  804. if issue.IsPull && !ctx.Repo.CanRead(models.UnitTypePullRequests) ||
  805. !issue.IsPull && !ctx.Repo.CanRead(models.UnitTypeIssues) {
  806. ctx.NotFound("IssueOrPullRequestUnitNotAllowed", nil)
  807. }
  808. }
  809. func getActionIssues(ctx *context.Context) []*models.Issue {
  810. commaSeparatedIssueIDs := ctx.Query("issue_ids")
  811. if len(commaSeparatedIssueIDs) == 0 {
  812. return nil
  813. }
  814. issueIDs := make([]int64, 0, 10)
  815. for _, stringIssueID := range strings.Split(commaSeparatedIssueIDs, ",") {
  816. issueID, err := strconv.ParseInt(stringIssueID, 10, 64)
  817. if err != nil {
  818. ctx.ServerError("ParseInt", err)
  819. return nil
  820. }
  821. issueIDs = append(issueIDs, issueID)
  822. }
  823. issues, err := models.GetIssuesByIDs(issueIDs)
  824. if err != nil {
  825. ctx.ServerError("GetIssuesByIDs", err)
  826. return nil
  827. }
  828. // Check access rights for all issues
  829. issueUnitEnabled := ctx.Repo.CanRead(models.UnitTypeIssues)
  830. prUnitEnabled := ctx.Repo.CanRead(models.UnitTypePullRequests)
  831. for _, issue := range issues {
  832. if issue.IsPull && !prUnitEnabled || !issue.IsPull && !issueUnitEnabled {
  833. ctx.NotFound("IssueOrPullRequestUnitNotAllowed", nil)
  834. return nil
  835. }
  836. if err = issue.LoadAttributes(); err != nil {
  837. ctx.ServerError("LoadAttributes", err)
  838. return nil
  839. }
  840. }
  841. return issues
  842. }
  843. // UpdateIssueTitle change issue's title
  844. func UpdateIssueTitle(ctx *context.Context) {
  845. issue := GetActionIssue(ctx)
  846. if ctx.Written() {
  847. return
  848. }
  849. if !ctx.IsSigned || (!issue.IsPoster(ctx.User.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) {
  850. ctx.Error(403)
  851. return
  852. }
  853. title := ctx.QueryTrim("title")
  854. if len(title) == 0 {
  855. ctx.Error(204)
  856. return
  857. }
  858. if err := issue.ChangeTitle(ctx.User, title); err != nil {
  859. ctx.ServerError("ChangeTitle", err)
  860. return
  861. }
  862. ctx.JSON(200, map[string]interface{}{
  863. "title": issue.Title,
  864. })
  865. }
  866. // UpdateIssueContent change issue's content
  867. func UpdateIssueContent(ctx *context.Context) {
  868. issue := GetActionIssue(ctx)
  869. if ctx.Written() {
  870. return
  871. }
  872. if !ctx.IsSigned || (ctx.User.ID != issue.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) {
  873. ctx.Error(403)
  874. return
  875. }
  876. content := ctx.Query("content")
  877. if err := issue.ChangeContent(ctx.User, content); err != nil {
  878. ctx.ServerError("ChangeContent", err)
  879. return
  880. }
  881. ctx.JSON(200, map[string]interface{}{
  882. "content": string(markdown.Render([]byte(issue.Content), ctx.Query("context"), ctx.Repo.Repository.ComposeMetas())),
  883. })
  884. }
  885. // UpdateIssueMilestone change issue's milestone
  886. func UpdateIssueMilestone(ctx *context.Context) {
  887. issues := getActionIssues(ctx)
  888. if ctx.Written() {
  889. return
  890. }
  891. milestoneID := ctx.QueryInt64("id")
  892. for _, issue := range issues {
  893. oldMilestoneID := issue.MilestoneID
  894. if oldMilestoneID == milestoneID {
  895. continue
  896. }
  897. issue.MilestoneID = milestoneID
  898. if err := models.ChangeMilestoneAssign(issue, ctx.User, oldMilestoneID); err != nil {
  899. ctx.ServerError("ChangeMilestoneAssign", err)
  900. return
  901. }
  902. }
  903. ctx.JSON(200, map[string]interface{}{
  904. "ok": true,
  905. })
  906. }
  907. // UpdateIssueAssignee change issue's assignee
  908. func UpdateIssueAssignee(ctx *context.Context) {
  909. issues := getActionIssues(ctx)
  910. if ctx.Written() {
  911. return
  912. }
  913. assigneeID := ctx.QueryInt64("id")
  914. action := ctx.Query("action")
  915. for _, issue := range issues {
  916. switch action {
  917. case "clear":
  918. if err := models.DeleteNotPassedAssignee(issue, ctx.User, []*models.User{}); err != nil {
  919. ctx.ServerError("ClearAssignees", err)
  920. return
  921. }
  922. default:
  923. if err := issue.ChangeAssignee(ctx.User, assigneeID); err != nil {
  924. ctx.ServerError("ChangeAssignee", err)
  925. return
  926. }
  927. }
  928. }
  929. ctx.JSON(200, map[string]interface{}{
  930. "ok": true,
  931. })
  932. }
  933. // UpdateIssueStatus change issue's status
  934. func UpdateIssueStatus(ctx *context.Context) {
  935. issues := getActionIssues(ctx)
  936. if ctx.Written() {
  937. return
  938. }
  939. var isClosed bool
  940. switch action := ctx.Query("action"); action {
  941. case "open":
  942. isClosed = false
  943. case "close":
  944. isClosed = true
  945. default:
  946. log.Warn("Unrecognized action: %s", action)
  947. }
  948. if _, err := models.IssueList(issues).LoadRepositories(); err != nil {
  949. ctx.ServerError("LoadRepositories", err)
  950. return
  951. }
  952. for _, issue := range issues {
  953. if issue.IsClosed != isClosed {
  954. if err := issue.ChangeStatus(ctx.User, isClosed); err != nil {
  955. if models.IsErrDependenciesLeft(err) {
  956. ctx.JSON(http.StatusPreconditionFailed, map[string]interface{}{
  957. "error": "cannot close this issue because it still has open dependencies",
  958. })
  959. return
  960. }
  961. ctx.ServerError("ChangeStatus", err)
  962. return
  963. }
  964. notification.NotifyIssueChangeStatus(ctx.User, issue, isClosed)
  965. }
  966. }
  967. ctx.JSON(200, map[string]interface{}{
  968. "ok": true,
  969. })
  970. }
  971. // NewComment create a comment for issue
  972. func NewComment(ctx *context.Context, form auth.CreateCommentForm) {
  973. issue := GetActionIssue(ctx)
  974. if ctx.Written() {
  975. return
  976. }
  977. if !ctx.IsSigned || (ctx.User.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)) {
  978. ctx.Error(403)
  979. return
  980. }
  981. var attachments []string
  982. if setting.AttachmentEnabled {
  983. attachments = form.Files
  984. }
  985. if ctx.HasError() {
  986. ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
  987. ctx.Redirect(fmt.Sprintf("%s/issues/%d", ctx.Repo.RepoLink, issue.Index))
  988. return
  989. }
  990. var comment *models.Comment
  991. defer func() {
  992. // Check if issue admin/poster changes the status of issue.
  993. if (ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) || (ctx.IsSigned && issue.IsPoster(ctx.User.ID))) &&
  994. (form.Status == "reopen" || form.Status == "close") &&
  995. !(issue.IsPull && issue.PullRequest.HasMerged) {
  996. // Duplication and conflict check should apply to reopen pull request.
  997. var pr *models.PullRequest
  998. if form.Status == "reopen" && issue.IsPull {
  999. pull := issue.PullRequest
  1000. pr, err := models.GetUnmergedPullRequest(pull.HeadRepoID, pull.BaseRepoID, pull.HeadBranch, pull.BaseBranch)
  1001. if err != nil {
  1002. if !models.IsErrPullRequestNotExist(err) {
  1003. ctx.ServerError("GetUnmergedPullRequest", err)
  1004. return
  1005. }
  1006. }
  1007. // Regenerate patch and test conflict.
  1008. if pr == nil {
  1009. if err = issue.PullRequest.UpdatePatch(); err != nil {
  1010. ctx.ServerError("UpdatePatch", err)
  1011. return
  1012. }
  1013. issue.PullRequest.AddToTaskQueue()
  1014. }
  1015. }
  1016. if pr != nil {
  1017. ctx.Flash.Info(ctx.Tr("repo.pulls.open_unmerged_pull_exists", pr.Index))
  1018. } else {
  1019. isClosed := form.Status == "close"
  1020. if err := issue.ChangeStatus(ctx.User, isClosed); err != nil {
  1021. log.Error(4, "ChangeStatus: %v", err)
  1022. if models.IsErrDependenciesLeft(err) {
  1023. if issue.IsPull {
  1024. ctx.Flash.Error(ctx.Tr("repo.issues.dependency.pr_close_blocked"))
  1025. ctx.Redirect(fmt.Sprintf("%s/pulls/%d", ctx.Repo.RepoLink, issue.Index), http.StatusSeeOther)
  1026. } else {
  1027. ctx.Flash.Error(ctx.Tr("repo.issues.dependency.issue_close_blocked"))
  1028. ctx.Redirect(fmt.Sprintf("%s/issues/%d", ctx.Repo.RepoLink, issue.Index), http.StatusSeeOther)
  1029. }
  1030. return
  1031. }
  1032. } else {
  1033. log.Trace("Issue [%d] status changed to closed: %v", issue.ID, issue.IsClosed)
  1034. notification.NotifyIssueChangeStatus(ctx.User, issue, isClosed)
  1035. }
  1036. }
  1037. }
  1038. // Redirect to comment hashtag if there is any actual content.
  1039. typeName := "issues"
  1040. if issue.IsPull {
  1041. typeName = "pulls"
  1042. }
  1043. if comment != nil {
  1044. ctx.Redirect(fmt.Sprintf("%s/%s/%d#%s", ctx.Repo.RepoLink, typeName, issue.Index, comment.HashTag()))
  1045. } else {
  1046. ctx.Redirect(fmt.Sprintf("%s/%s/%d", ctx.Repo.RepoLink, typeName, issue.Index))
  1047. }
  1048. }()
  1049. // Fix #321: Allow empty comments, as long as we have attachments.
  1050. if len(form.Content) == 0 && len(attachments) == 0 {
  1051. return
  1052. }
  1053. comment, err := models.CreateIssueComment(ctx.User, ctx.Repo.Repository, issue, form.Content, attachments)
  1054. if err != nil {
  1055. ctx.ServerError("CreateIssueComment", err)
  1056. return
  1057. }
  1058. notification.NotifyCreateIssueComment(ctx.User, ctx.Repo.Repository, issue, comment)
  1059. log.Trace("Comment created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID)
  1060. }
  1061. // UpdateCommentContent change comment of issue's content
  1062. func UpdateCommentContent(ctx *context.Context) {
  1063. comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
  1064. if err != nil {
  1065. ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err)
  1066. return
  1067. }
  1068. if err := comment.LoadIssue(); err != nil {
  1069. ctx.NotFoundOrServerError("LoadIssue", models.IsErrIssueNotExist, err)
  1070. return
  1071. }
  1072. if !ctx.IsSigned || (ctx.User.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
  1073. ctx.Error(403)
  1074. return
  1075. } else if comment.Type != models.CommentTypeComment && comment.Type != models.CommentTypeCode {
  1076. ctx.Error(204)
  1077. return
  1078. }
  1079. oldContent := comment.Content
  1080. comment.Content = ctx.Query("content")
  1081. if len(comment.Content) == 0 {
  1082. ctx.JSON(200, map[string]interface{}{
  1083. "content": "",
  1084. })
  1085. return
  1086. }
  1087. if err = models.UpdateComment(ctx.User, comment, oldContent); err != nil {
  1088. ctx.ServerError("UpdateComment", err)
  1089. return
  1090. }
  1091. ctx.JSON(200, map[string]interface{}{
  1092. "content": string(markdown.Render([]byte(comment.Content), ctx.Query("context"), ctx.Repo.Repository.ComposeMetas())),
  1093. })
  1094. }
  1095. // DeleteComment delete comment of issue
  1096. func DeleteComment(ctx *context.Context) {
  1097. comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
  1098. if err != nil {
  1099. ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err)
  1100. return
  1101. }
  1102. if err := comment.LoadIssue(); err != nil {
  1103. ctx.NotFoundOrServerError("LoadIssue", models.IsErrIssueNotExist, err)
  1104. return
  1105. }
  1106. if !ctx.IsSigned || (ctx.User.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
  1107. ctx.Error(403)
  1108. return
  1109. } else if comment.Type != models.CommentTypeComment && comment.Type != models.CommentTypeCode {
  1110. ctx.Error(204)
  1111. return
  1112. }
  1113. if err = models.DeleteComment(ctx.User, comment); err != nil {
  1114. ctx.ServerError("DeleteCommentByID", err)
  1115. return
  1116. }
  1117. ctx.Status(200)
  1118. }
  1119. // ChangeIssueReaction create a reaction for issue
  1120. func ChangeIssueReaction(ctx *context.Context, form auth.ReactionForm) {
  1121. issue := GetActionIssue(ctx)
  1122. if ctx.Written() {
  1123. return
  1124. }
  1125. if !ctx.IsSigned || (ctx.User.ID != issue.PosterID && !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull)) {
  1126. ctx.Error(403)
  1127. return
  1128. }
  1129. if ctx.HasError() {
  1130. ctx.ServerError("ChangeIssueReaction", errors.New(ctx.GetErrMsg()))
  1131. return
  1132. }
  1133. switch ctx.Params(":action") {
  1134. case "react":
  1135. reaction, err := models.CreateIssueReaction(ctx.User, issue, form.Content)
  1136. if err != nil {
  1137. log.Info("CreateIssueReaction: %s", err)
  1138. break
  1139. }
  1140. // Reload new reactions
  1141. issue.Reactions = nil
  1142. if err = issue.LoadAttributes(); err != nil {
  1143. log.Info("issue.LoadAttributes: %s", err)
  1144. break
  1145. }
  1146. log.Trace("Reaction for issue created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, reaction.ID)
  1147. case "unreact":
  1148. if err := models.DeleteIssueReaction(ctx.User, issue, form.Content); err != nil {
  1149. ctx.ServerError("DeleteIssueReaction", err)
  1150. return
  1151. }
  1152. // Reload new reactions
  1153. issue.Reactions = nil
  1154. if err := issue.LoadAttributes(); err != nil {
  1155. log.Info("issue.LoadAttributes: %s", err)
  1156. break
  1157. }
  1158. log.Trace("Reaction for issue removed: %d/%d", ctx.Repo.Repository.ID, issue.ID)
  1159. default:
  1160. ctx.NotFound(fmt.Sprintf("Unknown action %s", ctx.Params(":action")), nil)
  1161. return
  1162. }
  1163. if len(issue.Reactions) == 0 {
  1164. ctx.JSON(200, map[string]interface{}{
  1165. "empty": true,
  1166. "html": "",
  1167. })
  1168. return
  1169. }
  1170. html, err := ctx.HTMLString(string(tplReactions), map[string]interface{}{
  1171. "ctx": ctx.Data,
  1172. "ActionURL": fmt.Sprintf("%s/issues/%d/reactions", ctx.Repo.RepoLink, issue.Index),
  1173. "Reactions": issue.Reactions.GroupByType(),
  1174. })
  1175. if err != nil {
  1176. ctx.ServerError("ChangeIssueReaction.HTMLString", err)
  1177. return
  1178. }
  1179. ctx.JSON(200, map[string]interface{}{
  1180. "html": html,
  1181. })
  1182. }
  1183. // ChangeCommentReaction create a reaction for comment
  1184. func ChangeCommentReaction(ctx *context.Context, form auth.ReactionForm) {
  1185. comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
  1186. if err != nil {
  1187. ctx.NotFoundOrServerError("GetCommentByID", models.IsErrCommentNotExist, err)
  1188. return
  1189. }
  1190. if err := comment.LoadIssue(); err != nil {
  1191. ctx.NotFoundOrServerError("LoadIssue", models.IsErrIssueNotExist, err)
  1192. return
  1193. }
  1194. if !ctx.IsSigned || (ctx.User.ID != comment.PosterID && !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull)) {
  1195. ctx.Error(403)
  1196. return
  1197. } else if comment.Type != models.CommentTypeComment && comment.Type != models.CommentTypeCode {
  1198. ctx.Error(204)
  1199. return
  1200. }
  1201. switch ctx.Params(":action") {
  1202. case "react":
  1203. reaction, err := models.CreateCommentReaction(ctx.User, comment.Issue, comment, form.Content)
  1204. if err != nil {
  1205. log.Info("CreateCommentReaction: %s", err)
  1206. break
  1207. }
  1208. // Reload new reactions
  1209. comment.Reactions = nil
  1210. if err = comment.LoadReactions(); err != nil {
  1211. log.Info("comment.LoadReactions: %s", err)
  1212. break
  1213. }
  1214. log.Trace("Reaction for comment created: %d/%d/%d/%d", ctx.Repo.Repository.ID, comment.Issue.ID, comment.ID, reaction.ID)
  1215. case "unreact":
  1216. if err := models.DeleteCommentReaction(ctx.User, comment.Issue, comment, form.Content); err != nil {
  1217. ctx.ServerError("DeleteCommentReaction", err)
  1218. return
  1219. }
  1220. // Reload new reactions
  1221. comment.Reactions = nil
  1222. if err = comment.LoadReactions(); err != nil {
  1223. log.Info("comment.LoadReactions: %s", err)
  1224. break
  1225. }
  1226. log.Trace("Reaction for comment removed: %d/%d/%d", ctx.Repo.Repository.ID, comment.Issue.ID, comment.ID)
  1227. default:
  1228. ctx.NotFound(fmt.Sprintf("Unknown action %s", ctx.Params(":action")), nil)
  1229. return
  1230. }
  1231. if len(comment.Reactions) == 0 {
  1232. ctx.JSON(200, map[string]interface{}{
  1233. "empty": true,
  1234. "html": "",
  1235. })
  1236. return
  1237. }
  1238. html, err := ctx.HTMLString(string(tplReactions), map[string]interface{}{
  1239. "ctx": ctx.Data,
  1240. "ActionURL": fmt.Sprintf("%s/comments/%d/reactions", ctx.Repo.RepoLink, comment.ID),
  1241. "Reactions": comment.Reactions.GroupByType(),
  1242. })
  1243. if err != nil {
  1244. ctx.ServerError("ChangeCommentReaction.HTMLString", err)
  1245. return
  1246. }
  1247. ctx.JSON(200, map[string]interface{}{
  1248. "html": html,
  1249. })
  1250. }