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

issue.go 30 kB

11 years ago
10 years ago
10 years ago
10 years ago
10 years ago
9 years ago
9 years ago
10 years ago
10 years ago
10 years ago
9 years ago
9 years ago
9 years ago
9 years ago
10 years ago
10 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
9 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package repo
  5. import (
  6. "errors"
  7. "fmt"
  8. "net/http"
  9. "net/url"
  10. "strings"
  11. "time"
  12. "github.com/Unknwon/com"
  13. "github.com/Unknwon/paginater"
  14. "github.com/gogits/gogs/models"
  15. "github.com/gogits/gogs/modules/auth"
  16. "github.com/gogits/gogs/modules/base"
  17. "github.com/gogits/gogs/modules/log"
  18. "github.com/gogits/gogs/modules/mailer"
  19. "github.com/gogits/gogs/modules/middleware"
  20. "github.com/gogits/gogs/modules/setting"
  21. )
  22. const (
  23. ISSUES base.TplName = "repo/issue/list"
  24. ISSUE_NEW base.TplName = "repo/issue/new"
  25. ISSUE_VIEW base.TplName = "repo/issue/view"
  26. LABELS base.TplName = "repo/issue/labels"
  27. MILESTONE base.TplName = "repo/issue/milestones"
  28. MILESTONE_NEW base.TplName = "repo/issue/milestone_new"
  29. MILESTONE_EDIT base.TplName = "repo/issue/milestone_edit"
  30. )
  31. var (
  32. ErrFileTypeForbidden = errors.New("File type is not allowed")
  33. ErrTooManyFiles = errors.New("Maximum number of files to upload exceeded")
  34. )
  35. func MustEnableIssues(ctx *middleware.Context) {
  36. if !ctx.Repo.Repository.EnableIssues {
  37. ctx.Handle(404, "MustEnableIssues", nil)
  38. }
  39. }
  40. func MustEnablePulls(ctx *middleware.Context) {
  41. if !ctx.Repo.Repository.EnablePulls {
  42. ctx.Handle(404, "MustEnablePulls", nil)
  43. }
  44. ctx.Data["HasForkedRepo"] = ctx.IsSigned && ctx.User.HasForkedRepo(ctx.Repo.Repository.ID)
  45. }
  46. func RetrieveLabels(ctx *middleware.Context) {
  47. labels, err := models.GetLabelsByRepoID(ctx.Repo.Repository.ID)
  48. if err != nil {
  49. ctx.Handle(500, "RetrieveLabels.GetLabels: %v", err)
  50. return
  51. }
  52. for _, l := range labels {
  53. l.CalOpenIssues()
  54. }
  55. ctx.Data["Labels"] = labels
  56. ctx.Data["NumLabels"] = len(labels)
  57. }
  58. func Issues(ctx *middleware.Context) {
  59. isPullList := ctx.Params(":type") == "pulls"
  60. if isPullList {
  61. MustEnablePulls(ctx)
  62. if ctx.Written() {
  63. return
  64. }
  65. ctx.Data["Title"] = ctx.Tr("repo.pulls")
  66. ctx.Data["PageIsPullList"] = true
  67. } else {
  68. MustEnableIssues(ctx)
  69. if ctx.Written() {
  70. return
  71. }
  72. ctx.Data["Title"] = ctx.Tr("repo.issues")
  73. ctx.Data["PageIsIssueList"] = true
  74. }
  75. viewType := ctx.Query("type")
  76. sortType := ctx.Query("sort")
  77. types := []string{"assigned", "created_by", "mentioned"}
  78. if !com.IsSliceContainsStr(types, viewType) {
  79. viewType = "all"
  80. }
  81. // Must sign in to see issues about you.
  82. if viewType != "all" && !ctx.IsSigned {
  83. ctx.SetCookie("redirect_to", "/"+url.QueryEscape(setting.AppSubUrl+ctx.Req.RequestURI), 0, setting.AppSubUrl)
  84. ctx.Redirect(setting.AppSubUrl + "/user/login")
  85. return
  86. }
  87. var (
  88. assigneeID = ctx.QueryInt64("assignee")
  89. posterID int64
  90. )
  91. filterMode := models.FM_ALL
  92. switch viewType {
  93. case "assigned":
  94. filterMode = models.FM_ASSIGN
  95. assigneeID = ctx.User.Id
  96. case "created_by":
  97. filterMode = models.FM_CREATE
  98. posterID = ctx.User.Id
  99. case "mentioned":
  100. filterMode = models.FM_MENTION
  101. }
  102. var uid int64 = -1
  103. if ctx.IsSigned {
  104. uid = ctx.User.Id
  105. }
  106. repo := ctx.Repo.Repository
  107. selectLabels := ctx.Query("labels")
  108. milestoneID := ctx.QueryInt64("milestone")
  109. isShowClosed := ctx.Query("state") == "closed"
  110. issueStats := models.GetIssueStats(&models.IssueStatsOptions{
  111. RepoID: repo.ID,
  112. UserID: uid,
  113. LabelID: com.StrTo(selectLabels).MustInt64(),
  114. MilestoneID: milestoneID,
  115. AssigneeID: assigneeID,
  116. FilterMode: filterMode,
  117. IsPull: isPullList,
  118. })
  119. page := ctx.QueryInt("page")
  120. if page <= 1 {
  121. page = 1
  122. }
  123. var total int
  124. if !isShowClosed {
  125. total = int(issueStats.OpenCount)
  126. } else {
  127. total = int(issueStats.ClosedCount)
  128. }
  129. pager := paginater.New(total, setting.IssuePagingNum, page, 5)
  130. ctx.Data["Page"] = pager
  131. // Get issues.
  132. issues, err := models.Issues(&models.IssuesOptions{
  133. UserID: uid,
  134. AssigneeID: assigneeID,
  135. RepoID: repo.ID,
  136. PosterID: posterID,
  137. MilestoneID: milestoneID,
  138. Page: pager.Current(),
  139. IsClosed: isShowClosed,
  140. IsMention: filterMode == models.FM_MENTION,
  141. IsPull: isPullList,
  142. Labels: selectLabels,
  143. SortType: sortType,
  144. })
  145. if err != nil {
  146. ctx.Handle(500, "Issues: %v", err)
  147. return
  148. }
  149. // Get issue-user relations.
  150. pairs, err := models.GetIssueUsers(repo.ID, posterID, isShowClosed)
  151. if err != nil {
  152. ctx.Handle(500, "GetIssueUsers: %v", err)
  153. return
  154. }
  155. // Get posters.
  156. for i := range issues {
  157. if err = issues[i].GetPoster(); err != nil {
  158. ctx.Handle(500, "GetPoster", fmt.Errorf("[#%d]%v", issues[i].ID, err))
  159. return
  160. }
  161. if err = issues[i].GetLabels(); err != nil {
  162. ctx.Handle(500, "GetLabels", fmt.Errorf("[#%d]%v", issues[i].ID, err))
  163. return
  164. }
  165. if !ctx.IsSigned {
  166. issues[i].IsRead = true
  167. continue
  168. }
  169. // Check read status.
  170. idx := models.PairsContains(pairs, issues[i].ID, ctx.User.Id)
  171. if idx > -1 {
  172. issues[i].IsRead = pairs[idx].IsRead
  173. } else {
  174. issues[i].IsRead = true
  175. }
  176. }
  177. ctx.Data["Issues"] = issues
  178. // Get milestones.
  179. ctx.Data["Milestones"], err = models.GetAllRepoMilestones(repo.ID)
  180. if err != nil {
  181. ctx.Handle(500, "GetAllRepoMilestones: %v", err)
  182. return
  183. }
  184. // Get assignees.
  185. ctx.Data["Assignees"], err = repo.GetAssignees()
  186. if err != nil {
  187. ctx.Handle(500, "GetAssignees: %v", err)
  188. return
  189. }
  190. ctx.Data["IssueStats"] = issueStats
  191. ctx.Data["SelectLabels"] = com.StrTo(selectLabels).MustInt64()
  192. ctx.Data["ViewType"] = viewType
  193. ctx.Data["SortType"] = sortType
  194. ctx.Data["MilestoneID"] = milestoneID
  195. ctx.Data["AssigneeID"] = assigneeID
  196. ctx.Data["IsShowClosed"] = isShowClosed
  197. if isShowClosed {
  198. ctx.Data["State"] = "closed"
  199. } else {
  200. ctx.Data["State"] = "open"
  201. }
  202. ctx.HTML(200, ISSUES)
  203. }
  204. func renderAttachmentSettings(ctx *middleware.Context) {
  205. ctx.Data["RequireDropzone"] = true
  206. ctx.Data["IsAttachmentEnabled"] = setting.AttachmentEnabled
  207. ctx.Data["AttachmentAllowedTypes"] = setting.AttachmentAllowedTypes
  208. ctx.Data["AttachmentMaxSize"] = setting.AttachmentMaxSize
  209. ctx.Data["AttachmentMaxFiles"] = setting.AttachmentMaxFiles
  210. }
  211. func RetrieveRepoMilestonesAndAssignees(ctx *middleware.Context, repo *models.Repository) {
  212. var err error
  213. ctx.Data["OpenMilestones"], err = models.GetMilestones(repo.ID, -1, false)
  214. if err != nil {
  215. ctx.Handle(500, "GetMilestones: %v", err)
  216. return
  217. }
  218. ctx.Data["ClosedMilestones"], err = models.GetMilestones(repo.ID, -1, true)
  219. if err != nil {
  220. ctx.Handle(500, "GetMilestones: %v", err)
  221. return
  222. }
  223. ctx.Data["Assignees"], err = repo.GetAssignees()
  224. if err != nil {
  225. ctx.Handle(500, "GetAssignees: %v", err)
  226. return
  227. }
  228. }
  229. func RetrieveRepoMetas(ctx *middleware.Context, repo *models.Repository) []*models.Label {
  230. if !ctx.Repo.IsAdmin() {
  231. return nil
  232. }
  233. labels, err := models.GetLabelsByRepoID(repo.ID)
  234. if err != nil {
  235. ctx.Handle(500, "GetLabelsByRepoID: %v", err)
  236. return nil
  237. }
  238. ctx.Data["Labels"] = labels
  239. RetrieveRepoMilestonesAndAssignees(ctx, repo)
  240. if ctx.Written() {
  241. return nil
  242. }
  243. return labels
  244. }
  245. func NewIssue(ctx *middleware.Context) {
  246. ctx.Data["Title"] = ctx.Tr("repo.issues.new")
  247. ctx.Data["PageIsIssueList"] = true
  248. renderAttachmentSettings(ctx)
  249. RetrieveRepoMetas(ctx, ctx.Repo.Repository)
  250. if ctx.Written() {
  251. return
  252. }
  253. ctx.Data["RequireHighlightJS"] = true
  254. ctx.HTML(200, ISSUE_NEW)
  255. }
  256. func ValidateRepoMetas(ctx *middleware.Context, form auth.CreateIssueForm) ([]int64, int64, int64) {
  257. var (
  258. repo = ctx.Repo.Repository
  259. err error
  260. )
  261. labels := RetrieveRepoMetas(ctx, ctx.Repo.Repository)
  262. if ctx.Written() {
  263. return nil, 0, 0
  264. }
  265. if !ctx.Repo.IsAdmin() {
  266. return nil, 0, 0
  267. }
  268. // Check labels.
  269. labelIDs := base.StringsToInt64s(strings.Split(form.LabelIDs, ","))
  270. labelIDMark := base.Int64sToMap(labelIDs)
  271. hasSelected := false
  272. for i := range labels {
  273. if labelIDMark[labels[i].ID] {
  274. labels[i].IsChecked = true
  275. hasSelected = true
  276. }
  277. }
  278. ctx.Data["HasSelectedLabel"] = hasSelected
  279. ctx.Data["label_ids"] = form.LabelIDs
  280. ctx.Data["Labels"] = labels
  281. // Check milestone.
  282. milestoneID := form.MilestoneID
  283. if milestoneID > 0 {
  284. ctx.Data["Milestone"], err = repo.GetMilestoneByID(milestoneID)
  285. if err != nil {
  286. ctx.Handle(500, "GetMilestoneByID: %v", err)
  287. return nil, 0, 0
  288. }
  289. ctx.Data["milestone_id"] = milestoneID
  290. }
  291. // Check assignee.
  292. assigneeID := form.AssigneeID
  293. if assigneeID > 0 {
  294. ctx.Data["Assignee"], err = repo.GetAssigneeByID(assigneeID)
  295. if err != nil {
  296. ctx.Handle(500, "GetAssigneeByID: %v", err)
  297. return nil, 0, 0
  298. }
  299. ctx.Data["assignee_id"] = assigneeID
  300. }
  301. return labelIDs, milestoneID, assigneeID
  302. }
  303. func notifyWatchersAndMentions(ctx *middleware.Context, issue *models.Issue) {
  304. // Update mentions
  305. mentions := base.MentionPattern.FindAllString(issue.Content, -1)
  306. if len(mentions) > 0 {
  307. for i := range mentions {
  308. mentions[i] = strings.TrimSpace(mentions[i])[1:]
  309. }
  310. if err := models.UpdateMentions(mentions, issue.ID); err != nil {
  311. ctx.Handle(500, "UpdateMentions", err)
  312. return
  313. }
  314. }
  315. repo := ctx.Repo.Repository
  316. // Mail watchers and mentions.
  317. if setting.Service.EnableNotifyMail {
  318. tos, err := mailer.SendIssueNotifyMail(ctx.User, ctx.Repo.Owner, repo, issue)
  319. if err != nil {
  320. ctx.Handle(500, "SendIssueNotifyMail", err)
  321. return
  322. }
  323. tos = append(tos, ctx.User.LowerName)
  324. newTos := make([]string, 0, len(mentions))
  325. for _, m := range mentions {
  326. if com.IsSliceContainsStr(tos, m) {
  327. continue
  328. }
  329. newTos = append(newTos, m)
  330. }
  331. if err = mailer.SendIssueMentionMail(ctx.Render, ctx.User, ctx.Repo.Owner,
  332. repo, issue, models.GetUserEmailsByNames(newTos)); err != nil {
  333. ctx.Handle(500, "SendIssueMentionMail", err)
  334. return
  335. }
  336. }
  337. }
  338. func NewIssuePost(ctx *middleware.Context, form auth.CreateIssueForm) {
  339. ctx.Data["Title"] = ctx.Tr("repo.issues.new")
  340. ctx.Data["PageIsIssueList"] = true
  341. renderAttachmentSettings(ctx)
  342. var (
  343. repo = ctx.Repo.Repository
  344. attachments []string
  345. )
  346. labelIDs, milestoneID, assigneeID := ValidateRepoMetas(ctx, form)
  347. if ctx.Written() {
  348. return
  349. }
  350. if setting.AttachmentEnabled {
  351. attachments = form.Attachments
  352. }
  353. if ctx.HasError() {
  354. ctx.HTML(200, ISSUE_NEW)
  355. return
  356. }
  357. issue := &models.Issue{
  358. RepoID: ctx.Repo.Repository.ID,
  359. Index: repo.NextIssueIndex(),
  360. Name: strings.TrimSpace(form.Title),
  361. PosterID: ctx.User.Id,
  362. Poster: ctx.User,
  363. MilestoneID: milestoneID,
  364. AssigneeID: assigneeID,
  365. Content: form.Content,
  366. }
  367. if err := models.NewIssue(repo, issue, labelIDs, attachments); err != nil {
  368. ctx.Handle(500, "NewIssue", err)
  369. return
  370. }
  371. notifyWatchersAndMentions(ctx, issue)
  372. if ctx.Written() {
  373. return
  374. }
  375. log.Trace("Issue created: %d/%d", repo.ID, issue.ID)
  376. ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + com.ToStr(issue.Index))
  377. }
  378. func UploadIssueAttachment(ctx *middleware.Context) {
  379. if !setting.AttachmentEnabled {
  380. ctx.Error(404, "attachment is not enabled")
  381. return
  382. }
  383. allowedTypes := strings.Split(setting.AttachmentAllowedTypes, ",")
  384. file, header, err := ctx.Req.FormFile("file")
  385. if err != nil {
  386. ctx.Error(500, fmt.Sprintf("FormFile: %v", err))
  387. return
  388. }
  389. defer file.Close()
  390. buf := make([]byte, 1024)
  391. n, _ := file.Read(buf)
  392. if n > 0 {
  393. buf = buf[:n]
  394. }
  395. fileType := http.DetectContentType(buf)
  396. allowed := false
  397. for _, t := range allowedTypes {
  398. t := strings.Trim(t, " ")
  399. if t == "*/*" || t == fileType {
  400. allowed = true
  401. break
  402. }
  403. }
  404. if !allowed {
  405. ctx.Error(400, ErrFileTypeForbidden.Error())
  406. return
  407. }
  408. attach, err := models.NewAttachment(header.Filename, buf, file)
  409. if err != nil {
  410. ctx.Error(500, fmt.Sprintf("NewAttachment: %v", err))
  411. return
  412. }
  413. log.Trace("New attachment uploaded: %s", attach.UUID)
  414. ctx.JSON(200, map[string]string{
  415. "uuid": attach.UUID,
  416. })
  417. }
  418. func ViewIssue(ctx *middleware.Context) {
  419. ctx.Data["RequireDropzone"] = true
  420. renderAttachmentSettings(ctx)
  421. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  422. if err != nil {
  423. if models.IsErrIssueNotExist(err) {
  424. ctx.Handle(404, "GetIssueByIndex", err)
  425. } else {
  426. ctx.Handle(500, "GetIssueByIndex", err)
  427. }
  428. return
  429. }
  430. ctx.Data["Title"] = issue.Name
  431. // Make sure type and URL matches.
  432. if ctx.Params(":type") == "issues" && issue.IsPull {
  433. ctx.Redirect(ctx.Repo.RepoLink + "/pulls/" + com.ToStr(issue.Index))
  434. return
  435. } else if ctx.Params(":type") == "pulls" && !issue.IsPull {
  436. ctx.Redirect(ctx.Repo.RepoLink + "/issues/" + com.ToStr(issue.Index))
  437. return
  438. }
  439. if issue.IsPull {
  440. if err = issue.GetPullRequest(); err != nil {
  441. ctx.Handle(500, "GetPullRequest", err)
  442. return
  443. }
  444. ctx.Data["PageIsPullList"] = true
  445. ctx.Data["PageIsPullConversation"] = true
  446. ctx.Data["HasForkedRepo"] = ctx.IsSigned && ctx.User.HasForkedRepo(ctx.Repo.Repository.ID)
  447. } else {
  448. MustEnableIssues(ctx)
  449. if ctx.Written() {
  450. return
  451. }
  452. ctx.Data["PageIsIssueList"] = true
  453. }
  454. if err = issue.GetPoster(); err != nil {
  455. ctx.Handle(500, "GetPoster", err)
  456. return
  457. }
  458. issue.RenderedContent = string(base.RenderMarkdown([]byte(issue.Content), ctx.Repo.RepoLink,
  459. ctx.Repo.Repository.ComposeMetas()))
  460. repo := ctx.Repo.Repository
  461. // Get more information if it's a pull request.
  462. if issue.IsPull {
  463. if issue.HasMerged {
  464. ctx.Data["DisableStatusChange"] = issue.HasMerged
  465. PrepareMergedViewPullInfo(ctx, issue)
  466. } else {
  467. PrepareViewPullInfo(ctx, issue)
  468. }
  469. if ctx.Written() {
  470. return
  471. }
  472. }
  473. // Metas.
  474. // Check labels.
  475. if err = issue.GetLabels(); err != nil {
  476. ctx.Handle(500, "GetLabels", err)
  477. return
  478. }
  479. labelIDMark := make(map[int64]bool)
  480. for i := range issue.Labels {
  481. labelIDMark[issue.Labels[i].ID] = true
  482. }
  483. labels, err := models.GetLabelsByRepoID(repo.ID)
  484. if err != nil {
  485. ctx.Handle(500, "GetLabelsByRepoID: %v", err)
  486. return
  487. }
  488. hasSelected := false
  489. for i := range labels {
  490. if labelIDMark[labels[i].ID] {
  491. labels[i].IsChecked = true
  492. hasSelected = true
  493. }
  494. }
  495. ctx.Data["HasSelectedLabel"] = hasSelected
  496. ctx.Data["Labels"] = labels
  497. // Check milestone and assignee.
  498. if ctx.Repo.IsAdmin() {
  499. RetrieveRepoMilestonesAndAssignees(ctx, repo)
  500. if ctx.Written() {
  501. return
  502. }
  503. }
  504. if ctx.IsSigned {
  505. // Update issue-user.
  506. if err = issue.ReadBy(ctx.User.Id); err != nil {
  507. ctx.Handle(500, "ReadBy", err)
  508. return
  509. }
  510. }
  511. var (
  512. tag models.CommentTag
  513. ok bool
  514. marked = make(map[int64]models.CommentTag)
  515. comment *models.Comment
  516. participants = make([]*models.User, 1, 10)
  517. )
  518. // Render comments and and fetch participants.
  519. participants[0] = issue.Poster
  520. for _, comment = range issue.Comments {
  521. if comment.Type == models.COMMENT_TYPE_COMMENT {
  522. comment.RenderedContent = string(base.RenderMarkdown([]byte(comment.Content), ctx.Repo.RepoLink,
  523. ctx.Repo.Repository.ComposeMetas()))
  524. // Check tag.
  525. tag, ok = marked[comment.PosterID]
  526. if ok {
  527. comment.ShowTag = tag
  528. continue
  529. }
  530. if repo.IsOwnedBy(comment.PosterID) ||
  531. (repo.Owner.IsOrganization() && repo.Owner.IsOwnedBy(comment.PosterID)) {
  532. comment.ShowTag = models.COMMENT_TAG_OWNER
  533. } else if comment.Poster.IsAdminOfRepo(repo) {
  534. comment.ShowTag = models.COMMENT_TAG_ADMIN
  535. } else if comment.PosterID == issue.PosterID {
  536. comment.ShowTag = models.COMMENT_TAG_POSTER
  537. }
  538. marked[comment.PosterID] = comment.ShowTag
  539. isAdded := false
  540. for j := range participants {
  541. if comment.Poster == participants[j] {
  542. isAdded = true
  543. break
  544. }
  545. }
  546. if !isAdded && !issue.IsPoster(comment.Poster.Id) {
  547. participants = append(participants, comment.Poster)
  548. }
  549. }
  550. }
  551. ctx.Data["Participants"] = participants
  552. ctx.Data["NumParticipants"] = len(participants)
  553. ctx.Data["Issue"] = issue
  554. ctx.Data["IsIssueOwner"] = ctx.Repo.IsAdmin() || (ctx.IsSigned && issue.IsPoster(ctx.User.Id))
  555. ctx.Data["SignInLink"] = setting.AppSubUrl + "/user/login"
  556. ctx.Data["RequireHighlightJS"] = true
  557. ctx.HTML(200, ISSUE_VIEW)
  558. }
  559. func getActionIssue(ctx *middleware.Context) *models.Issue {
  560. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  561. if err != nil {
  562. if models.IsErrIssueNotExist(err) {
  563. ctx.Error(404, "GetIssueByIndex")
  564. } else {
  565. ctx.Handle(500, "GetIssueByIndex", err)
  566. }
  567. return nil
  568. }
  569. return issue
  570. }
  571. func UpdateIssueTitle(ctx *middleware.Context) {
  572. issue := getActionIssue(ctx)
  573. if ctx.Written() {
  574. return
  575. }
  576. if !ctx.IsSigned || (ctx.User.Id != issue.PosterID && !ctx.Repo.IsAdmin()) {
  577. ctx.Error(403)
  578. return
  579. }
  580. issue.Name = ctx.QueryTrim("title")
  581. if len(issue.Name) == 0 {
  582. ctx.Error(204)
  583. return
  584. }
  585. if err := models.UpdateIssue(issue); err != nil {
  586. ctx.Handle(500, "UpdateIssue", err)
  587. return
  588. }
  589. ctx.JSON(200, map[string]interface{}{
  590. "title": issue.Name,
  591. })
  592. }
  593. func UpdateIssueContent(ctx *middleware.Context) {
  594. issue := getActionIssue(ctx)
  595. if ctx.Written() {
  596. return
  597. }
  598. if !ctx.IsSigned || (ctx.User.Id != issue.PosterID && !ctx.Repo.IsAdmin()) {
  599. ctx.Error(403)
  600. return
  601. }
  602. issue.Content = ctx.Query("content")
  603. if err := models.UpdateIssue(issue); err != nil {
  604. ctx.Handle(500, "UpdateIssue", err)
  605. return
  606. }
  607. ctx.JSON(200, map[string]interface{}{
  608. "content": string(base.RenderMarkdown([]byte(issue.Content), ctx.Query("context"), ctx.Repo.Repository.ComposeMetas())),
  609. })
  610. }
  611. func UpdateIssueLabel(ctx *middleware.Context) {
  612. issue := getActionIssue(ctx)
  613. if ctx.Written() {
  614. return
  615. }
  616. if ctx.Query("action") == "clear" {
  617. if err := issue.ClearLabels(); err != nil {
  618. ctx.Handle(500, "ClearLabels", err)
  619. return
  620. }
  621. } else {
  622. isAttach := ctx.Query("action") == "attach"
  623. label, err := models.GetLabelByID(ctx.QueryInt64("id"))
  624. if err != nil {
  625. if models.IsErrLabelNotExist(err) {
  626. ctx.Error(404, "GetLabelByID")
  627. } else {
  628. ctx.Handle(500, "GetLabelByID", err)
  629. }
  630. return
  631. }
  632. if isAttach && !issue.HasLabel(label.ID) {
  633. if err = issue.AddLabel(label); err != nil {
  634. ctx.Handle(500, "AddLabel", err)
  635. return
  636. }
  637. } else if !isAttach && issue.HasLabel(label.ID) {
  638. if err = issue.RemoveLabel(label); err != nil {
  639. ctx.Handle(500, "RemoveLabel", err)
  640. return
  641. }
  642. }
  643. }
  644. ctx.JSON(200, map[string]interface{}{
  645. "ok": true,
  646. })
  647. }
  648. func UpdateIssueMilestone(ctx *middleware.Context) {
  649. issue := getActionIssue(ctx)
  650. if ctx.Written() {
  651. return
  652. }
  653. oldMid := issue.MilestoneID
  654. mid := ctx.QueryInt64("id")
  655. if oldMid == mid {
  656. ctx.JSON(200, map[string]interface{}{
  657. "ok": true,
  658. })
  659. return
  660. }
  661. // Not check for invalid milestone id and give responsibility to owners.
  662. issue.MilestoneID = mid
  663. if err := models.ChangeMilestoneAssign(oldMid, issue); err != nil {
  664. ctx.Handle(500, "ChangeMilestoneAssign", err)
  665. return
  666. }
  667. ctx.JSON(200, map[string]interface{}{
  668. "ok": true,
  669. })
  670. }
  671. func UpdateIssueAssignee(ctx *middleware.Context) {
  672. issue := getActionIssue(ctx)
  673. if ctx.Written() {
  674. return
  675. }
  676. aid := ctx.QueryInt64("id")
  677. if issue.AssigneeID == aid {
  678. ctx.JSON(200, map[string]interface{}{
  679. "ok": true,
  680. })
  681. return
  682. }
  683. // Not check for invalid assignee id and give responsibility to owners.
  684. issue.AssigneeID = aid
  685. if err := models.UpdateIssueUserByAssignee(issue); err != nil {
  686. ctx.Handle(500, "UpdateIssueUserByAssignee: %v", err)
  687. return
  688. }
  689. ctx.JSON(200, map[string]interface{}{
  690. "ok": true,
  691. })
  692. }
  693. func NewComment(ctx *middleware.Context, form auth.CreateCommentForm) {
  694. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  695. if err != nil {
  696. if models.IsErrIssueNotExist(err) {
  697. ctx.Handle(404, "GetIssueByIndex", err)
  698. } else {
  699. ctx.Handle(500, "GetIssueByIndex", err)
  700. }
  701. return
  702. }
  703. if issue.IsPull {
  704. if err = issue.GetPullRequest(); err != nil {
  705. ctx.Handle(500, "GetPullRequest", err)
  706. return
  707. }
  708. }
  709. var attachments []string
  710. if setting.AttachmentEnabled {
  711. attachments = form.Attachments
  712. }
  713. if ctx.HasError() {
  714. ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
  715. ctx.Redirect(fmt.Sprintf("%s/issues/%d", ctx.Repo.RepoLink, issue.Index))
  716. return
  717. }
  718. var comment *models.Comment
  719. defer func() {
  720. // Check if issue admin/poster changes the status of issue.
  721. if (ctx.Repo.IsAdmin() || (ctx.IsSigned && issue.IsPoster(ctx.User.Id))) &&
  722. (form.Status == "reopen" || form.Status == "close") &&
  723. !(issue.IsPull && issue.HasMerged) {
  724. // Duplication and conflict check should apply to reopen pull request.
  725. var pr *models.PullRequest
  726. if form.Status == "reopen" && issue.IsPull {
  727. pull := issue.PullRequest
  728. pr, err = models.GetUnmergedPullRequest(pull.HeadRepoID, pull.BaseRepoID, pull.HeadBranch, pull.BaseBranch)
  729. if err != nil {
  730. if !models.IsErrPullRequestNotExist(err) {
  731. ctx.Handle(500, "GetUnmergedPullRequest", err)
  732. return
  733. }
  734. }
  735. // Regenerate patch and test conflict.
  736. if pr == nil {
  737. if err = issue.UpdatePatch(); err != nil {
  738. ctx.Handle(500, "UpdatePatch", err)
  739. return
  740. }
  741. issue.AddToTaskQueue()
  742. }
  743. }
  744. if pr != nil {
  745. ctx.Flash.Info(ctx.Tr("repo.pulls.open_unmerged_pull_exists", pr.Index))
  746. } else {
  747. issue.Repo = ctx.Repo.Repository
  748. if err = issue.ChangeStatus(ctx.User, form.Status == "close"); err != nil {
  749. log.Error(4, "ChangeStatus: %v", err)
  750. } else {
  751. log.Trace("Issue[%d] status changed to closed: %v", issue.ID, issue.IsClosed)
  752. }
  753. }
  754. }
  755. // Redirect to comment hashtag if there is any actual content.
  756. typeName := "issues"
  757. if issue.IsPull {
  758. typeName = "pulls"
  759. }
  760. if comment != nil {
  761. ctx.Redirect(fmt.Sprintf("%s/%s/%d#%s", ctx.Repo.RepoLink, typeName, issue.Index, comment.HashTag()))
  762. } else {
  763. ctx.Redirect(fmt.Sprintf("%s/%s/%d", ctx.Repo.RepoLink, typeName, issue.Index))
  764. }
  765. }()
  766. // Fix #321: Allow empty comments, as long as we have attachments.
  767. if len(form.Content) == 0 && len(attachments) == 0 {
  768. return
  769. }
  770. comment, err = models.CreateIssueComment(ctx.User, ctx.Repo.Repository, issue, form.Content, attachments)
  771. if err != nil {
  772. ctx.Handle(500, "CreateIssueComment", err)
  773. return
  774. }
  775. notifyWatchersAndMentions(ctx, &models.Issue{
  776. ID: issue.ID,
  777. Index: issue.Index,
  778. Name: issue.Name,
  779. Content: form.Content,
  780. })
  781. if ctx.Written() {
  782. return
  783. }
  784. log.Trace("Comment created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID)
  785. }
  786. func UpdateCommentContent(ctx *middleware.Context) {
  787. comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
  788. if err != nil {
  789. if models.IsErrCommentNotExist(err) {
  790. ctx.Error(404, "GetCommentByID")
  791. } else {
  792. ctx.Handle(500, "GetCommentByID", err)
  793. }
  794. return
  795. }
  796. if !ctx.IsSigned || (ctx.User.Id != comment.PosterID && !ctx.Repo.IsAdmin()) {
  797. ctx.Error(403)
  798. return
  799. } else if comment.Type != models.COMMENT_TYPE_COMMENT {
  800. ctx.Error(204)
  801. return
  802. }
  803. comment.Content = ctx.Query("content")
  804. if len(comment.Content) == 0 {
  805. ctx.JSON(200, map[string]interface{}{
  806. "content": "",
  807. })
  808. return
  809. }
  810. if err := models.UpdateComment(comment); err != nil {
  811. ctx.Handle(500, "UpdateComment", err)
  812. return
  813. }
  814. ctx.JSON(200, map[string]interface{}{
  815. "content": string(base.RenderMarkdown([]byte(comment.Content), ctx.Query("context"), ctx.Repo.Repository.ComposeMetas())),
  816. })
  817. }
  818. func Labels(ctx *middleware.Context) {
  819. ctx.Data["Title"] = ctx.Tr("repo.labels")
  820. ctx.Data["PageIsIssueList"] = true
  821. ctx.Data["PageIsLabels"] = true
  822. ctx.Data["RequireMinicolors"] = true
  823. ctx.HTML(200, LABELS)
  824. }
  825. func NewLabel(ctx *middleware.Context, form auth.CreateLabelForm) {
  826. ctx.Data["Title"] = ctx.Tr("repo.labels")
  827. ctx.Data["PageIsLabels"] = true
  828. if ctx.HasError() {
  829. ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
  830. ctx.Redirect(ctx.Repo.RepoLink + "/labels")
  831. return
  832. }
  833. l := &models.Label{
  834. RepoID: ctx.Repo.Repository.ID,
  835. Name: form.Title,
  836. Color: form.Color,
  837. }
  838. if err := models.NewLabel(l); err != nil {
  839. ctx.Handle(500, "NewLabel", err)
  840. return
  841. }
  842. ctx.Redirect(ctx.Repo.RepoLink + "/labels")
  843. }
  844. func UpdateLabel(ctx *middleware.Context, form auth.CreateLabelForm) {
  845. l, err := models.GetLabelByID(form.ID)
  846. if err != nil {
  847. switch {
  848. case models.IsErrLabelNotExist(err):
  849. ctx.Error(404)
  850. default:
  851. ctx.Handle(500, "UpdateLabel", err)
  852. }
  853. return
  854. }
  855. fmt.Println(form.Title, form.Color)
  856. l.Name = form.Title
  857. l.Color = form.Color
  858. if err := models.UpdateLabel(l); err != nil {
  859. ctx.Handle(500, "UpdateLabel", err)
  860. return
  861. }
  862. ctx.Redirect(ctx.Repo.RepoLink + "/labels")
  863. }
  864. func DeleteLabel(ctx *middleware.Context) {
  865. if err := models.DeleteLabel(ctx.Repo.Repository.ID, ctx.QueryInt64("id")); err != nil {
  866. ctx.Flash.Error("DeleteLabel: " + err.Error())
  867. } else {
  868. ctx.Flash.Success(ctx.Tr("repo.issues.label_deletion_success"))
  869. }
  870. ctx.JSON(200, map[string]interface{}{
  871. "redirect": ctx.Repo.RepoLink + "/labels",
  872. })
  873. return
  874. }
  875. func Milestones(ctx *middleware.Context) {
  876. ctx.Data["Title"] = ctx.Tr("repo.milestones")
  877. ctx.Data["PageIsIssueList"] = true
  878. ctx.Data["PageIsMilestones"] = true
  879. isShowClosed := ctx.Query("state") == "closed"
  880. openCount, closedCount := models.MilestoneStats(ctx.Repo.Repository.ID)
  881. ctx.Data["OpenCount"] = openCount
  882. ctx.Data["ClosedCount"] = closedCount
  883. page := ctx.QueryInt("page")
  884. if page <= 1 {
  885. page = 1
  886. }
  887. var total int
  888. if !isShowClosed {
  889. total = int(openCount)
  890. } else {
  891. total = int(closedCount)
  892. }
  893. ctx.Data["Page"] = paginater.New(total, setting.IssuePagingNum, page, 5)
  894. miles, err := models.GetMilestones(ctx.Repo.Repository.ID, page, isShowClosed)
  895. if err != nil {
  896. ctx.Handle(500, "GetMilestones", err)
  897. return
  898. }
  899. for _, m := range miles {
  900. m.RenderedContent = string(base.RenderMarkdown([]byte(m.Content), ctx.Repo.RepoLink, ctx.Repo.Repository.ComposeMetas()))
  901. m.CalOpenIssues()
  902. }
  903. ctx.Data["Milestones"] = miles
  904. if isShowClosed {
  905. ctx.Data["State"] = "closed"
  906. } else {
  907. ctx.Data["State"] = "open"
  908. }
  909. ctx.Data["IsShowClosed"] = isShowClosed
  910. ctx.HTML(200, MILESTONE)
  911. }
  912. func NewMilestone(ctx *middleware.Context) {
  913. ctx.Data["Title"] = ctx.Tr("repo.milestones.new")
  914. ctx.Data["PageIsIssueList"] = true
  915. ctx.Data["PageIsMilestones"] = true
  916. ctx.Data["RequireDatetimepicker"] = true
  917. ctx.Data["DateLang"] = setting.DateLang(ctx.Locale.Language())
  918. ctx.HTML(200, MILESTONE_NEW)
  919. }
  920. func NewMilestonePost(ctx *middleware.Context, form auth.CreateMilestoneForm) {
  921. ctx.Data["Title"] = ctx.Tr("repo.milestones.new")
  922. ctx.Data["PageIsIssueList"] = true
  923. ctx.Data["PageIsMilestones"] = true
  924. ctx.Data["RequireDatetimepicker"] = true
  925. ctx.Data["DateLang"] = setting.DateLang(ctx.Locale.Language())
  926. if ctx.HasError() {
  927. ctx.HTML(200, MILESTONE_NEW)
  928. return
  929. }
  930. if len(form.Deadline) == 0 {
  931. form.Deadline = "9999-12-31"
  932. }
  933. deadline, err := time.Parse("2006-01-02", form.Deadline)
  934. if err != nil {
  935. ctx.Data["Err_Deadline"] = true
  936. ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), MILESTONE_NEW, &form)
  937. return
  938. }
  939. if err = models.NewMilestone(&models.Milestone{
  940. RepoID: ctx.Repo.Repository.ID,
  941. Name: form.Title,
  942. Content: form.Content,
  943. Deadline: deadline,
  944. }); err != nil {
  945. ctx.Handle(500, "NewMilestone", err)
  946. return
  947. }
  948. ctx.Flash.Success(ctx.Tr("repo.milestones.create_success", form.Title))
  949. ctx.Redirect(ctx.Repo.RepoLink + "/milestones")
  950. }
  951. func EditMilestone(ctx *middleware.Context) {
  952. ctx.Data["Title"] = ctx.Tr("repo.milestones.edit")
  953. ctx.Data["PageIsMilestones"] = true
  954. ctx.Data["PageIsEditMilestone"] = true
  955. ctx.Data["RequireDatetimepicker"] = true
  956. ctx.Data["DateLang"] = setting.DateLang(ctx.Locale.Language())
  957. m, err := models.GetMilestoneByID(ctx.ParamsInt64(":id"))
  958. if err != nil {
  959. if models.IsErrMilestoneNotExist(err) {
  960. ctx.Handle(404, "GetMilestoneByID", nil)
  961. } else {
  962. ctx.Handle(500, "GetMilestoneByID", err)
  963. }
  964. return
  965. }
  966. ctx.Data["title"] = m.Name
  967. ctx.Data["content"] = m.Content
  968. if len(m.DeadlineString) > 0 {
  969. ctx.Data["deadline"] = m.DeadlineString
  970. }
  971. ctx.HTML(200, MILESTONE_NEW)
  972. }
  973. func EditMilestonePost(ctx *middleware.Context, form auth.CreateMilestoneForm) {
  974. ctx.Data["Title"] = ctx.Tr("repo.milestones.edit")
  975. ctx.Data["PageIsMilestones"] = true
  976. ctx.Data["PageIsEditMilestone"] = true
  977. ctx.Data["RequireDatetimepicker"] = true
  978. ctx.Data["DateLang"] = setting.DateLang(ctx.Locale.Language())
  979. if ctx.HasError() {
  980. ctx.HTML(200, MILESTONE_NEW)
  981. return
  982. }
  983. if len(form.Deadline) == 0 {
  984. form.Deadline = "9999-12-31"
  985. }
  986. deadline, err := time.Parse("2006-01-02", form.Deadline)
  987. if err != nil {
  988. ctx.Data["Err_Deadline"] = true
  989. ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), MILESTONE_NEW, &form)
  990. return
  991. }
  992. m, err := models.GetMilestoneByID(ctx.ParamsInt64(":id"))
  993. if err != nil {
  994. if models.IsErrMilestoneNotExist(err) {
  995. ctx.Handle(404, "GetMilestoneByID", nil)
  996. } else {
  997. ctx.Handle(500, "GetMilestoneByID", err)
  998. }
  999. return
  1000. }
  1001. m.Name = form.Title
  1002. m.Content = form.Content
  1003. m.Deadline = deadline
  1004. if err = models.UpdateMilestone(m); err != nil {
  1005. ctx.Handle(500, "UpdateMilestone", err)
  1006. return
  1007. }
  1008. ctx.Flash.Success(ctx.Tr("repo.milestones.edit_success", m.Name))
  1009. ctx.Redirect(ctx.Repo.RepoLink + "/milestones")
  1010. }
  1011. func ChangeMilestonStatus(ctx *middleware.Context) {
  1012. m, err := models.GetMilestoneByID(ctx.ParamsInt64(":id"))
  1013. if err != nil {
  1014. if models.IsErrMilestoneNotExist(err) {
  1015. ctx.Handle(404, "GetMilestoneByID", err)
  1016. } else {
  1017. ctx.Handle(500, "GetMilestoneByID", err)
  1018. }
  1019. return
  1020. }
  1021. switch ctx.Params(":action") {
  1022. case "open":
  1023. if m.IsClosed {
  1024. if err = models.ChangeMilestoneStatus(m, false); err != nil {
  1025. ctx.Handle(500, "ChangeMilestoneStatus", err)
  1026. return
  1027. }
  1028. }
  1029. ctx.Redirect(ctx.Repo.RepoLink + "/milestones?state=open")
  1030. case "close":
  1031. if !m.IsClosed {
  1032. m.ClosedDate = time.Now()
  1033. if err = models.ChangeMilestoneStatus(m, true); err != nil {
  1034. ctx.Handle(500, "ChangeMilestoneStatus", err)
  1035. return
  1036. }
  1037. }
  1038. ctx.Redirect(ctx.Repo.RepoLink + "/milestones?state=closed")
  1039. default:
  1040. ctx.Redirect(ctx.Repo.RepoLink + "/milestones")
  1041. }
  1042. }
  1043. func DeleteMilestone(ctx *middleware.Context) {
  1044. if err := models.DeleteMilestoneByID(ctx.QueryInt64("id")); err != nil {
  1045. ctx.Flash.Error("DeleteMilestoneByID: " + err.Error())
  1046. } else {
  1047. ctx.Flash.Success(ctx.Tr("repo.milestones.deletion_success"))
  1048. }
  1049. ctx.JSON(200, map[string]interface{}{
  1050. "redirect": ctx.Repo.RepoLink + "/milestones",
  1051. })
  1052. }