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