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
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
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
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
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198
  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. )
  517. // Render comments.
  518. for _, comment = range issue.Comments {
  519. if comment.Type == models.COMMENT_TYPE_COMMENT {
  520. comment.RenderedContent = string(base.RenderMarkdown([]byte(comment.Content), ctx.Repo.RepoLink,
  521. ctx.Repo.Repository.ComposeMetas()))
  522. // Check tag.
  523. tag, ok = marked[comment.PosterID]
  524. if ok {
  525. comment.ShowTag = tag
  526. continue
  527. }
  528. if repo.IsOwnedBy(comment.PosterID) ||
  529. (repo.Owner.IsOrganization() && repo.Owner.IsOwnedBy(comment.PosterID)) {
  530. comment.ShowTag = models.COMMENT_TAG_OWNER
  531. } else if comment.Poster.IsAdminOfRepo(repo) {
  532. comment.ShowTag = models.COMMENT_TAG_ADMIN
  533. } else if comment.PosterID == issue.PosterID {
  534. comment.ShowTag = models.COMMENT_TAG_POSTER
  535. }
  536. marked[comment.PosterID] = comment.ShowTag
  537. }
  538. }
  539. ctx.Data["Issue"] = issue
  540. ctx.Data["IsIssueOwner"] = ctx.Repo.IsAdmin() || (ctx.IsSigned && issue.IsPoster(ctx.User.Id))
  541. ctx.Data["SignInLink"] = setting.AppSubUrl + "/user/login"
  542. ctx.Data["RequireHighlightJS"] = true
  543. ctx.HTML(200, ISSUE_VIEW)
  544. }
  545. func getActionIssue(ctx *middleware.Context) *models.Issue {
  546. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  547. if err != nil {
  548. if models.IsErrIssueNotExist(err) {
  549. ctx.Error(404, "GetIssueByIndex")
  550. } else {
  551. ctx.Handle(500, "GetIssueByIndex", err)
  552. }
  553. return nil
  554. }
  555. return issue
  556. }
  557. func UpdateIssueTitle(ctx *middleware.Context) {
  558. issue := getActionIssue(ctx)
  559. if ctx.Written() {
  560. return
  561. }
  562. if !ctx.IsSigned || (ctx.User.Id != issue.PosterID && !ctx.Repo.IsAdmin()) {
  563. ctx.Error(403)
  564. return
  565. }
  566. issue.Name = ctx.QueryTrim("title")
  567. if len(issue.Name) == 0 {
  568. ctx.Error(204)
  569. return
  570. }
  571. if err := models.UpdateIssue(issue); err != nil {
  572. ctx.Handle(500, "UpdateIssue", err)
  573. return
  574. }
  575. ctx.JSON(200, map[string]interface{}{
  576. "title": issue.Name,
  577. })
  578. }
  579. func UpdateIssueContent(ctx *middleware.Context) {
  580. issue := getActionIssue(ctx)
  581. if ctx.Written() {
  582. return
  583. }
  584. if !ctx.IsSigned || (ctx.User.Id != issue.PosterID && !ctx.Repo.IsAdmin()) {
  585. ctx.Error(403)
  586. return
  587. }
  588. issue.Content = ctx.Query("content")
  589. if err := models.UpdateIssue(issue); err != nil {
  590. ctx.Handle(500, "UpdateIssue", err)
  591. return
  592. }
  593. ctx.JSON(200, map[string]interface{}{
  594. "content": string(base.RenderMarkdown([]byte(issue.Content), ctx.Query("context"), ctx.Repo.Repository.ComposeMetas())),
  595. })
  596. }
  597. func UpdateIssueLabel(ctx *middleware.Context) {
  598. issue := getActionIssue(ctx)
  599. if ctx.Written() {
  600. return
  601. }
  602. if ctx.Query("action") == "clear" {
  603. if err := issue.ClearLabels(); err != nil {
  604. ctx.Handle(500, "ClearLabels", err)
  605. return
  606. }
  607. } else {
  608. isAttach := ctx.Query("action") == "attach"
  609. label, err := models.GetLabelByID(ctx.QueryInt64("id"))
  610. if err != nil {
  611. if models.IsErrLabelNotExist(err) {
  612. ctx.Error(404, "GetLabelByID")
  613. } else {
  614. ctx.Handle(500, "GetLabelByID", err)
  615. }
  616. return
  617. }
  618. if isAttach && !issue.HasLabel(label.ID) {
  619. if err = issue.AddLabel(label); err != nil {
  620. ctx.Handle(500, "AddLabel", err)
  621. return
  622. }
  623. } else if !isAttach && issue.HasLabel(label.ID) {
  624. if err = issue.RemoveLabel(label); err != nil {
  625. ctx.Handle(500, "RemoveLabel", err)
  626. return
  627. }
  628. }
  629. }
  630. ctx.JSON(200, map[string]interface{}{
  631. "ok": true,
  632. })
  633. }
  634. func UpdateIssueMilestone(ctx *middleware.Context) {
  635. issue := getActionIssue(ctx)
  636. if ctx.Written() {
  637. return
  638. }
  639. oldMid := issue.MilestoneID
  640. mid := ctx.QueryInt64("id")
  641. if oldMid == mid {
  642. ctx.JSON(200, map[string]interface{}{
  643. "ok": true,
  644. })
  645. return
  646. }
  647. // Not check for invalid milestone id and give responsibility to owners.
  648. issue.MilestoneID = mid
  649. if err := models.ChangeMilestoneAssign(oldMid, issue); err != nil {
  650. ctx.Handle(500, "ChangeMilestoneAssign", err)
  651. return
  652. }
  653. ctx.JSON(200, map[string]interface{}{
  654. "ok": true,
  655. })
  656. }
  657. func UpdateIssueAssignee(ctx *middleware.Context) {
  658. issue := getActionIssue(ctx)
  659. if ctx.Written() {
  660. return
  661. }
  662. aid := ctx.QueryInt64("id")
  663. if issue.AssigneeID == aid {
  664. ctx.JSON(200, map[string]interface{}{
  665. "ok": true,
  666. })
  667. return
  668. }
  669. // Not check for invalid assignee id and give responsibility to owners.
  670. issue.AssigneeID = aid
  671. if err := models.UpdateIssueUserByAssignee(issue); err != nil {
  672. ctx.Handle(500, "UpdateIssueUserByAssignee: %v", err)
  673. return
  674. }
  675. ctx.JSON(200, map[string]interface{}{
  676. "ok": true,
  677. })
  678. }
  679. func NewComment(ctx *middleware.Context, form auth.CreateCommentForm) {
  680. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  681. if err != nil {
  682. if models.IsErrIssueNotExist(err) {
  683. ctx.Handle(404, "GetIssueByIndex", err)
  684. } else {
  685. ctx.Handle(500, "GetIssueByIndex", err)
  686. }
  687. return
  688. }
  689. if issue.IsPull {
  690. if err = issue.GetPullRequest(); err != nil {
  691. ctx.Handle(500, "GetPullRequest", err)
  692. return
  693. }
  694. }
  695. var attachments []string
  696. if setting.AttachmentEnabled {
  697. attachments = form.Attachments
  698. }
  699. if ctx.HasError() {
  700. ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
  701. ctx.Redirect(fmt.Sprintf("%s/issues/%d", ctx.Repo.RepoLink, issue.Index))
  702. return
  703. }
  704. var comment *models.Comment
  705. defer func() {
  706. // Check if issue admin/poster changes the status of issue.
  707. if (ctx.Repo.IsAdmin() || (ctx.IsSigned && issue.IsPoster(ctx.User.Id))) &&
  708. (form.Status == "reopen" || form.Status == "close") &&
  709. !(issue.IsPull && issue.HasMerged) {
  710. // Duplication and conflict check should apply to reopen pull request.
  711. var pr *models.PullRequest
  712. if form.Status == "reopen" && issue.IsPull {
  713. pull := issue.PullRequest
  714. pr, err = models.GetUnmergedPullRequest(pull.HeadRepoID, pull.BaseRepoID, pull.HeadBranch, pull.BaseBranch)
  715. if err != nil {
  716. if !models.IsErrPullRequestNotExist(err) {
  717. ctx.Handle(500, "GetUnmergedPullRequest", err)
  718. return
  719. }
  720. }
  721. // Regenerate patch and test conflict.
  722. if pr == nil {
  723. if err = issue.UpdatePatch(); err != nil {
  724. ctx.Handle(500, "UpdatePatch", err)
  725. return
  726. }
  727. issue.AddToTaskQueue()
  728. }
  729. }
  730. if pr != nil {
  731. ctx.Flash.Info(ctx.Tr("repo.pulls.open_unmerged_pull_exists", pr.Index))
  732. } else {
  733. issue.Repo = ctx.Repo.Repository
  734. if err = issue.ChangeStatus(ctx.User, form.Status == "close"); err != nil {
  735. log.Error(4, "ChangeStatus: %v", err)
  736. } else {
  737. log.Trace("Issue[%d] status changed to closed: %v", issue.ID, issue.IsClosed)
  738. }
  739. }
  740. }
  741. // Redirect to comment hashtag if there is any actual content.
  742. typeName := "issues"
  743. if issue.IsPull {
  744. typeName = "pulls"
  745. }
  746. if comment != nil {
  747. ctx.Redirect(fmt.Sprintf("%s/%s/%d#%s", ctx.Repo.RepoLink, typeName, issue.Index, comment.HashTag()))
  748. } else {
  749. ctx.Redirect(fmt.Sprintf("%s/%s/%d", ctx.Repo.RepoLink, typeName, issue.Index))
  750. }
  751. }()
  752. // Fix #321: Allow empty comments, as long as we have attachments.
  753. if len(form.Content) == 0 && len(attachments) == 0 {
  754. return
  755. }
  756. comment, err = models.CreateIssueComment(ctx.User, ctx.Repo.Repository, issue, form.Content, attachments)
  757. if err != nil {
  758. ctx.Handle(500, "CreateIssueComment", err)
  759. return
  760. }
  761. notifyWatchersAndMentions(ctx, &models.Issue{
  762. ID: issue.ID,
  763. Index: issue.Index,
  764. Name: issue.Name,
  765. Content: form.Content,
  766. })
  767. if ctx.Written() {
  768. return
  769. }
  770. log.Trace("Comment created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID)
  771. }
  772. func UpdateCommentContent(ctx *middleware.Context) {
  773. comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
  774. if err != nil {
  775. if models.IsErrCommentNotExist(err) {
  776. ctx.Error(404, "GetCommentByID")
  777. } else {
  778. ctx.Handle(500, "GetCommentByID", err)
  779. }
  780. return
  781. }
  782. if !ctx.IsSigned || (ctx.User.Id != comment.PosterID && !ctx.Repo.IsAdmin()) {
  783. ctx.Error(403)
  784. return
  785. } else if comment.Type != models.COMMENT_TYPE_COMMENT {
  786. ctx.Error(204)
  787. return
  788. }
  789. comment.Content = ctx.Query("content")
  790. if len(comment.Content) == 0 {
  791. ctx.JSON(200, map[string]interface{}{
  792. "content": "",
  793. })
  794. return
  795. }
  796. if err := models.UpdateComment(comment); err != nil {
  797. ctx.Handle(500, "UpdateComment", err)
  798. return
  799. }
  800. ctx.JSON(200, map[string]interface{}{
  801. "content": string(base.RenderMarkdown([]byte(comment.Content), ctx.Query("context"), ctx.Repo.Repository.ComposeMetas())),
  802. })
  803. }
  804. func Labels(ctx *middleware.Context) {
  805. ctx.Data["Title"] = ctx.Tr("repo.labels")
  806. ctx.Data["PageIsIssueList"] = true
  807. ctx.Data["PageIsLabels"] = true
  808. ctx.Data["RequireMinicolors"] = true
  809. ctx.HTML(200, LABELS)
  810. }
  811. func NewLabel(ctx *middleware.Context, form auth.CreateLabelForm) {
  812. ctx.Data["Title"] = ctx.Tr("repo.labels")
  813. ctx.Data["PageIsLabels"] = true
  814. if ctx.HasError() {
  815. ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
  816. ctx.Redirect(ctx.Repo.RepoLink + "/labels")
  817. return
  818. }
  819. l := &models.Label{
  820. RepoID: ctx.Repo.Repository.ID,
  821. Name: form.Title,
  822. Color: form.Color,
  823. }
  824. if err := models.NewLabel(l); err != nil {
  825. ctx.Handle(500, "NewLabel", err)
  826. return
  827. }
  828. ctx.Redirect(ctx.Repo.RepoLink + "/labels")
  829. }
  830. func UpdateLabel(ctx *middleware.Context, form auth.CreateLabelForm) {
  831. l, err := models.GetLabelByID(form.ID)
  832. if err != nil {
  833. switch {
  834. case models.IsErrLabelNotExist(err):
  835. ctx.Error(404)
  836. default:
  837. ctx.Handle(500, "UpdateLabel", err)
  838. }
  839. return
  840. }
  841. fmt.Println(form.Title, form.Color)
  842. l.Name = form.Title
  843. l.Color = form.Color
  844. if err := models.UpdateLabel(l); err != nil {
  845. ctx.Handle(500, "UpdateLabel", err)
  846. return
  847. }
  848. ctx.Redirect(ctx.Repo.RepoLink + "/labels")
  849. }
  850. func DeleteLabel(ctx *middleware.Context) {
  851. if err := models.DeleteLabel(ctx.Repo.Repository.ID, ctx.QueryInt64("id")); err != nil {
  852. ctx.Flash.Error("DeleteLabel: " + err.Error())
  853. } else {
  854. ctx.Flash.Success(ctx.Tr("repo.issues.label_deletion_success"))
  855. }
  856. ctx.JSON(200, map[string]interface{}{
  857. "redirect": ctx.Repo.RepoLink + "/labels",
  858. })
  859. return
  860. }
  861. func Milestones(ctx *middleware.Context) {
  862. ctx.Data["Title"] = ctx.Tr("repo.milestones")
  863. ctx.Data["PageIsIssueList"] = true
  864. ctx.Data["PageIsMilestones"] = true
  865. isShowClosed := ctx.Query("state") == "closed"
  866. openCount, closedCount := models.MilestoneStats(ctx.Repo.Repository.ID)
  867. ctx.Data["OpenCount"] = openCount
  868. ctx.Data["ClosedCount"] = closedCount
  869. page := ctx.QueryInt("page")
  870. if page <= 1 {
  871. page = 1
  872. }
  873. var total int
  874. if !isShowClosed {
  875. total = int(openCount)
  876. } else {
  877. total = int(closedCount)
  878. }
  879. ctx.Data["Page"] = paginater.New(total, setting.IssuePagingNum, page, 5)
  880. miles, err := models.GetMilestones(ctx.Repo.Repository.ID, page, isShowClosed)
  881. if err != nil {
  882. ctx.Handle(500, "GetMilestones", err)
  883. return
  884. }
  885. for _, m := range miles {
  886. m.RenderedContent = string(base.RenderMarkdown([]byte(m.Content), ctx.Repo.RepoLink, ctx.Repo.Repository.ComposeMetas()))
  887. m.CalOpenIssues()
  888. }
  889. ctx.Data["Milestones"] = miles
  890. if isShowClosed {
  891. ctx.Data["State"] = "closed"
  892. } else {
  893. ctx.Data["State"] = "open"
  894. }
  895. ctx.Data["IsShowClosed"] = isShowClosed
  896. ctx.HTML(200, MILESTONE)
  897. }
  898. func NewMilestone(ctx *middleware.Context) {
  899. ctx.Data["Title"] = ctx.Tr("repo.milestones.new")
  900. ctx.Data["PageIsIssueList"] = true
  901. ctx.Data["PageIsMilestones"] = true
  902. ctx.Data["RequireDatetimepicker"] = true
  903. ctx.Data["DateLang"] = setting.DateLang(ctx.Locale.Language())
  904. ctx.HTML(200, MILESTONE_NEW)
  905. }
  906. func NewMilestonePost(ctx *middleware.Context, form auth.CreateMilestoneForm) {
  907. ctx.Data["Title"] = ctx.Tr("repo.milestones.new")
  908. ctx.Data["PageIsIssueList"] = true
  909. ctx.Data["PageIsMilestones"] = true
  910. ctx.Data["RequireDatetimepicker"] = true
  911. ctx.Data["DateLang"] = setting.DateLang(ctx.Locale.Language())
  912. if ctx.HasError() {
  913. ctx.HTML(200, MILESTONE_NEW)
  914. return
  915. }
  916. if len(form.Deadline) == 0 {
  917. form.Deadline = "9999-12-31"
  918. }
  919. deadline, err := time.Parse("2006-01-02", form.Deadline)
  920. if err != nil {
  921. ctx.Data["Err_Deadline"] = true
  922. ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), MILESTONE_NEW, &form)
  923. return
  924. }
  925. if err = models.NewMilestone(&models.Milestone{
  926. RepoID: ctx.Repo.Repository.ID,
  927. Name: form.Title,
  928. Content: form.Content,
  929. Deadline: deadline,
  930. }); err != nil {
  931. ctx.Handle(500, "NewMilestone", err)
  932. return
  933. }
  934. ctx.Flash.Success(ctx.Tr("repo.milestones.create_success", form.Title))
  935. ctx.Redirect(ctx.Repo.RepoLink + "/milestones")
  936. }
  937. func EditMilestone(ctx *middleware.Context) {
  938. ctx.Data["Title"] = ctx.Tr("repo.milestones.edit")
  939. ctx.Data["PageIsMilestones"] = true
  940. ctx.Data["PageIsEditMilestone"] = true
  941. ctx.Data["RequireDatetimepicker"] = true
  942. ctx.Data["DateLang"] = setting.DateLang(ctx.Locale.Language())
  943. m, err := models.GetMilestoneByID(ctx.ParamsInt64(":id"))
  944. if err != nil {
  945. if models.IsErrMilestoneNotExist(err) {
  946. ctx.Handle(404, "GetMilestoneByID", nil)
  947. } else {
  948. ctx.Handle(500, "GetMilestoneByID", err)
  949. }
  950. return
  951. }
  952. ctx.Data["title"] = m.Name
  953. ctx.Data["content"] = m.Content
  954. if len(m.DeadlineString) > 0 {
  955. ctx.Data["deadline"] = m.DeadlineString
  956. }
  957. ctx.HTML(200, MILESTONE_NEW)
  958. }
  959. func EditMilestonePost(ctx *middleware.Context, form auth.CreateMilestoneForm) {
  960. ctx.Data["Title"] = ctx.Tr("repo.milestones.edit")
  961. ctx.Data["PageIsMilestones"] = true
  962. ctx.Data["PageIsEditMilestone"] = true
  963. ctx.Data["RequireDatetimepicker"] = true
  964. ctx.Data["DateLang"] = setting.DateLang(ctx.Locale.Language())
  965. if ctx.HasError() {
  966. ctx.HTML(200, MILESTONE_NEW)
  967. return
  968. }
  969. if len(form.Deadline) == 0 {
  970. form.Deadline = "9999-12-31"
  971. }
  972. deadline, err := time.Parse("2006-01-02", form.Deadline)
  973. if err != nil {
  974. ctx.Data["Err_Deadline"] = true
  975. ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), MILESTONE_NEW, &form)
  976. return
  977. }
  978. m, err := models.GetMilestoneByID(ctx.ParamsInt64(":id"))
  979. if err != nil {
  980. if models.IsErrMilestoneNotExist(err) {
  981. ctx.Handle(404, "GetMilestoneByID", nil)
  982. } else {
  983. ctx.Handle(500, "GetMilestoneByID", err)
  984. }
  985. return
  986. }
  987. m.Name = form.Title
  988. m.Content = form.Content
  989. m.Deadline = deadline
  990. if err = models.UpdateMilestone(m); err != nil {
  991. ctx.Handle(500, "UpdateMilestone", err)
  992. return
  993. }
  994. ctx.Flash.Success(ctx.Tr("repo.milestones.edit_success", m.Name))
  995. ctx.Redirect(ctx.Repo.RepoLink + "/milestones")
  996. }
  997. func ChangeMilestonStatus(ctx *middleware.Context) {
  998. m, err := models.GetMilestoneByID(ctx.ParamsInt64(":id"))
  999. if err != nil {
  1000. if models.IsErrMilestoneNotExist(err) {
  1001. ctx.Handle(404, "GetMilestoneByID", err)
  1002. } else {
  1003. ctx.Handle(500, "GetMilestoneByID", err)
  1004. }
  1005. return
  1006. }
  1007. switch ctx.Params(":action") {
  1008. case "open":
  1009. if m.IsClosed {
  1010. if err = models.ChangeMilestoneStatus(m, false); err != nil {
  1011. ctx.Handle(500, "ChangeMilestoneStatus", err)
  1012. return
  1013. }
  1014. }
  1015. ctx.Redirect(ctx.Repo.RepoLink + "/milestones?state=open")
  1016. case "close":
  1017. if !m.IsClosed {
  1018. m.ClosedDate = time.Now()
  1019. if err = models.ChangeMilestoneStatus(m, true); err != nil {
  1020. ctx.Handle(500, "ChangeMilestoneStatus", err)
  1021. return
  1022. }
  1023. }
  1024. ctx.Redirect(ctx.Repo.RepoLink + "/milestones?state=closed")
  1025. default:
  1026. ctx.Redirect(ctx.Repo.RepoLink + "/milestones")
  1027. }
  1028. }
  1029. func DeleteMilestone(ctx *middleware.Context) {
  1030. if err := models.DeleteMilestoneByID(ctx.QueryInt64("id")); err != nil {
  1031. ctx.Flash.Error("DeleteMilestoneByID: " + err.Error())
  1032. } else {
  1033. ctx.Flash.Success(ctx.Tr("repo.milestones.deletion_success"))
  1034. }
  1035. ctx.JSON(200, map[string]interface{}{
  1036. "redirect": ctx.Repo.RepoLink + "/milestones",
  1037. })
  1038. }