issue.go 29 KB

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