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 31 kB

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