issue.go 29 KB

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