issue.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  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. "fmt"
  7. "net/url"
  8. "strings"
  9. "github.com/Unknwon/com"
  10. "github.com/go-martini/martini"
  11. "github.com/gogits/gogs/models"
  12. "github.com/gogits/gogs/modules/auth"
  13. "github.com/gogits/gogs/modules/base"
  14. "github.com/gogits/gogs/modules/log"
  15. "github.com/gogits/gogs/modules/mailer"
  16. "github.com/gogits/gogs/modules/middleware"
  17. )
  18. func Issues(ctx *middleware.Context) {
  19. ctx.Data["Title"] = "Issues"
  20. ctx.Data["IsRepoToolbarIssues"] = true
  21. ctx.Data["IsRepoToolbarIssuesList"] = true
  22. viewType := ctx.Query("type")
  23. types := []string{"assigned", "created_by", "mentioned"}
  24. if !com.IsSliceContainsStr(types, viewType) {
  25. viewType = "all"
  26. }
  27. isShowClosed := ctx.Query("state") == "closed"
  28. if viewType != "all" {
  29. if !ctx.IsSigned {
  30. ctx.SetCookie("redirect_to", "/"+url.QueryEscape(ctx.Req.RequestURI))
  31. ctx.Redirect("/user/login")
  32. return
  33. }
  34. }
  35. var assigneeId, posterId int64
  36. var filterMode int
  37. switch viewType {
  38. case "assigned":
  39. assigneeId = ctx.User.Id
  40. filterMode = models.FM_ASSIGN
  41. case "created_by":
  42. posterId = ctx.User.Id
  43. filterMode = models.FM_CREATE
  44. case "mentioned":
  45. filterMode = models.FM_MENTION
  46. }
  47. mid, _ := base.StrTo(ctx.Query("milestone")).Int64()
  48. page, _ := base.StrTo(ctx.Query("page")).Int()
  49. // Get issues.
  50. issues, err := models.GetIssues(assigneeId, ctx.Repo.Repository.Id, posterId, mid, page,
  51. isShowClosed, ctx.Query("labels"), ctx.Query("sortType"))
  52. if err != nil {
  53. ctx.Handle(500, "issue.Issues(GetIssues): %v", err)
  54. return
  55. }
  56. // Get issue-user pairs.
  57. pairs, err := models.GetIssueUserPairs(ctx.Repo.Repository.Id, posterId, isShowClosed)
  58. if err != nil {
  59. ctx.Handle(500, "issue.Issues(GetIssueUserPairs): %v", err)
  60. return
  61. }
  62. // Get posters.
  63. for i := range issues {
  64. idx := models.PairsContains(pairs, issues[i].Id)
  65. if filterMode == models.FM_MENTION && (idx == -1 || !pairs[idx].IsMentioned) {
  66. continue
  67. }
  68. if idx > -1 {
  69. issues[i].IsRead = pairs[idx].IsRead
  70. } else {
  71. issues[i].IsRead = true
  72. }
  73. if err = issues[i].GetPoster(); err != nil {
  74. ctx.Handle(500, "issue.Issues(GetPoster): %v", err)
  75. return
  76. }
  77. }
  78. var uid int64 = -1
  79. if ctx.User != nil {
  80. uid = ctx.User.Id
  81. }
  82. issueStats := models.GetIssueStats(ctx.Repo.Repository.Id, uid, isShowClosed, filterMode)
  83. ctx.Data["IssueStats"] = issueStats
  84. ctx.Data["ViewType"] = viewType
  85. ctx.Data["Issues"] = issues
  86. ctx.Data["IsShowClosed"] = isShowClosed
  87. if isShowClosed {
  88. ctx.Data["State"] = "closed"
  89. ctx.Data["ShowCount"] = issueStats.ClosedCount
  90. } else {
  91. ctx.Data["ShowCount"] = issueStats.OpenCount
  92. }
  93. ctx.HTML(200, "issue/list")
  94. }
  95. func CreateIssue(ctx *middleware.Context, params martini.Params) {
  96. ctx.Data["Title"] = "Create issue"
  97. ctx.Data["IsRepoToolbarIssues"] = true
  98. ctx.Data["IsRepoToolbarIssuesList"] = false
  99. us, err := models.GetCollaborators(strings.TrimPrefix(ctx.Repo.RepoLink, "/"))
  100. if err != nil {
  101. ctx.Handle(500, "issue.CreateIssue(GetCollaborators)", err)
  102. return
  103. }
  104. ctx.Data["Collaborators"] = us
  105. ctx.HTML(200, "issue/create")
  106. }
  107. func CreateIssuePost(ctx *middleware.Context, params martini.Params, form auth.CreateIssueForm) {
  108. ctx.Data["Title"] = "Create issue"
  109. ctx.Data["IsRepoToolbarIssues"] = true
  110. ctx.Data["IsRepoToolbarIssuesList"] = false
  111. us, err := models.GetCollaborators(strings.TrimPrefix(ctx.Repo.RepoLink, "/"))
  112. if err != nil {
  113. ctx.Handle(500, "issue.CreateIssue(GetCollaborators)", err)
  114. return
  115. }
  116. ctx.Data["Collaborators"] = us
  117. if ctx.HasError() {
  118. ctx.HTML(200, "issue/create")
  119. return
  120. }
  121. issue := &models.Issue{
  122. Index: int64(ctx.Repo.Repository.NumIssues) + 1,
  123. Name: form.IssueName,
  124. RepoId: ctx.Repo.Repository.Id,
  125. PosterId: ctx.User.Id,
  126. MilestoneId: form.MilestoneId,
  127. AssigneeId: form.AssigneeId,
  128. Labels: form.Labels,
  129. Content: form.Content,
  130. }
  131. if err := models.NewIssue(issue); err != nil {
  132. ctx.Handle(500, "issue.CreateIssue(NewIssue)", err)
  133. return
  134. } else if err := models.NewIssueUserPairs(issue.RepoId, issue.Id, ctx.Repo.Owner.Id,
  135. ctx.User.Id, form.AssigneeId, ctx.Repo.Repository.Name); err != nil {
  136. ctx.Handle(500, "issue.CreateIssue(NewIssueUserPairs)", err)
  137. return
  138. }
  139. // Update mentions.
  140. ms := base.MentionPattern.FindAllString(issue.Content, -1)
  141. if len(ms) > 0 {
  142. for i := range ms {
  143. ms[i] = ms[i][1:]
  144. }
  145. ids := models.GetUserIdsByNames(ms)
  146. if err := models.UpdateIssueUserPairsByMentions(ids, issue.Id); err != nil {
  147. ctx.Handle(500, "issue.CreateIssue(UpdateIssueUserPairsByMentions)", err)
  148. return
  149. }
  150. }
  151. // Notify watchers.
  152. if err := models.NotifyWatchers(&models.Action{ActUserId: ctx.User.Id, ActUserName: ctx.User.Name, ActEmail: ctx.User.Email,
  153. OpType: models.OP_CREATE_ISSUE, Content: fmt.Sprintf("%d|%s", issue.Index, issue.Name),
  154. RepoId: ctx.Repo.Repository.Id, RepoName: ctx.Repo.Repository.Name, RefName: ""}); err != nil {
  155. ctx.Handle(500, "issue.CreateIssue(NotifyWatchers)", err)
  156. return
  157. }
  158. // Mail watchers and mentions.
  159. if base.Service.NotifyMail {
  160. tos, err := mailer.SendIssueNotifyMail(ctx.User, ctx.Repo.Owner, ctx.Repo.Repository, issue)
  161. if err != nil {
  162. ctx.Handle(500, "issue.CreateIssue(SendIssueNotifyMail)", err)
  163. return
  164. }
  165. tos = append(tos, ctx.User.LowerName)
  166. newTos := make([]string, 0, len(ms))
  167. for _, m := range ms {
  168. if com.IsSliceContainsStr(tos, m) {
  169. continue
  170. }
  171. newTos = append(newTos, m)
  172. }
  173. if err = mailer.SendIssueMentionMail(ctx.Render, ctx.User, ctx.Repo.Owner,
  174. ctx.Repo.Repository, issue, models.GetUserEmailsByNames(newTos)); err != nil {
  175. ctx.Handle(500, "issue.CreateIssue(SendIssueMentionMail)", err)
  176. return
  177. }
  178. }
  179. log.Trace("%d Issue created: %d", ctx.Repo.Repository.Id, issue.Id)
  180. ctx.Redirect(fmt.Sprintf("/%s/%s/issues/%d", params["username"], params["reponame"], issue.Index))
  181. }
  182. func ViewIssue(ctx *middleware.Context, params martini.Params) {
  183. idx, _ := base.StrTo(params["index"]).Int64()
  184. if idx == 0 {
  185. ctx.Handle(404, "issue.ViewIssue", nil)
  186. return
  187. }
  188. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.Id, idx)
  189. if err != nil {
  190. if err == models.ErrIssueNotExist {
  191. ctx.Handle(404, "issue.ViewIssue", err)
  192. } else {
  193. ctx.Handle(200, "issue.ViewIssue", err)
  194. }
  195. return
  196. }
  197. // Update issue-user.
  198. if err = models.UpdateIssueUserPairByRead(ctx.User.Id, issue.Id); err != nil {
  199. ctx.Handle(500, "issue.ViewIssue(UpdateIssueUserPairByRead): %v", err)
  200. return
  201. }
  202. // Get poster and Assignee.
  203. if err = issue.GetPoster(); err != nil {
  204. ctx.Handle(500, "issue.ViewIssue(GetPoster): %v", err)
  205. return
  206. } else if err = issue.GetAssignee(); err != nil {
  207. ctx.Handle(500, "issue.ViewIssue(GetAssignee): %v", err)
  208. return
  209. }
  210. issue.RenderedContent = string(base.RenderMarkdown([]byte(issue.Content), ctx.Repo.RepoLink))
  211. // Get comments.
  212. comments, err := models.GetIssueComments(issue.Id)
  213. if err != nil {
  214. ctx.Handle(500, "issue.ViewIssue(GetIssueComments): %v", err)
  215. return
  216. }
  217. // Get posters.
  218. for i := range comments {
  219. u, err := models.GetUserById(comments[i].PosterId)
  220. if err != nil {
  221. ctx.Handle(500, "issue.ViewIssue(get poster of comment): %v", err)
  222. return
  223. }
  224. comments[i].Poster = u
  225. comments[i].Content = string(base.RenderMarkdown([]byte(comments[i].Content), ctx.Repo.RepoLink))
  226. }
  227. ctx.Data["Title"] = issue.Name
  228. ctx.Data["Issue"] = issue
  229. ctx.Data["Comments"] = comments
  230. ctx.Data["IsIssueOwner"] = ctx.Repo.IsOwner || (ctx.IsSigned && issue.PosterId == ctx.User.Id)
  231. ctx.Data["IsRepoToolbarIssues"] = true
  232. ctx.Data["IsRepoToolbarIssuesList"] = false
  233. ctx.HTML(200, "issue/view")
  234. }
  235. func UpdateIssue(ctx *middleware.Context, params martini.Params, form auth.CreateIssueForm) {
  236. index, err := base.StrTo(params["index"]).Int()
  237. if err != nil {
  238. ctx.Handle(404, "issue.UpdateIssue", err)
  239. return
  240. }
  241. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.Id, int64(index))
  242. if err != nil {
  243. if err == models.ErrIssueNotExist {
  244. ctx.Handle(404, "issue.UpdateIssue", err)
  245. } else {
  246. ctx.Handle(200, "issue.UpdateIssue(get issue)", err)
  247. }
  248. return
  249. }
  250. if ctx.User.Id != issue.PosterId && !ctx.Repo.IsOwner {
  251. ctx.Handle(404, "issue.UpdateIssue", nil)
  252. return
  253. }
  254. issue.Name = form.IssueName
  255. issue.MilestoneId = form.MilestoneId
  256. issue.AssigneeId = form.AssigneeId
  257. issue.Labels = form.Labels
  258. issue.Content = form.Content
  259. if err = models.UpdateIssue(issue); err != nil {
  260. ctx.Handle(200, "issue.UpdateIssue(update issue)", err)
  261. return
  262. }
  263. ctx.JSON(200, map[string]interface{}{
  264. "ok": true,
  265. "title": issue.Name,
  266. "content": string(base.RenderMarkdown([]byte(issue.Content), ctx.Repo.RepoLink)),
  267. })
  268. }
  269. func Comment(ctx *middleware.Context, params martini.Params) {
  270. index, err := base.StrTo(ctx.Query("issueIndex")).Int64()
  271. if err != nil {
  272. ctx.Handle(404, "issue.Comment(get index)", err)
  273. return
  274. }
  275. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.Id, index)
  276. if err != nil {
  277. if err == models.ErrIssueNotExist {
  278. ctx.Handle(404, "issue.Comment", err)
  279. } else {
  280. ctx.Handle(200, "issue.Comment(get issue)", err)
  281. }
  282. return
  283. }
  284. // TODO: check collaborators
  285. // Check if issue owner changes the status of issue.
  286. var newStatus string
  287. if ctx.Repo.IsOwner || issue.PosterId == ctx.User.Id {
  288. newStatus = ctx.Query("change_status")
  289. }
  290. if len(newStatus) > 0 {
  291. if (strings.Contains(newStatus, "Reopen") && issue.IsClosed) ||
  292. (strings.Contains(newStatus, "Close") && !issue.IsClosed) {
  293. issue.IsClosed = !issue.IsClosed
  294. if err = models.UpdateIssue(issue); err != nil {
  295. ctx.Handle(500, "issue.Comment(UpdateIssue)", err)
  296. return
  297. } else if err = models.UpdateIssueUserPairsByStatus(issue.Id, issue.IsClosed); err != nil {
  298. ctx.Handle(500, "issue.Comment(UpdateIssueUserPairsByStatus)", err)
  299. return
  300. }
  301. cmtType := models.IT_CLOSE
  302. if !issue.IsClosed {
  303. cmtType = models.IT_REOPEN
  304. }
  305. if err = models.CreateComment(ctx.User.Id, ctx.Repo.Repository.Id, issue.Id, 0, 0, cmtType, ""); err != nil {
  306. ctx.Handle(200, "issue.Comment(create status change comment)", err)
  307. return
  308. }
  309. log.Trace("%s Issue(%d) status changed: %v", ctx.Req.RequestURI, issue.Id, !issue.IsClosed)
  310. }
  311. }
  312. var ms []string
  313. content := ctx.Query("content")
  314. if len(content) > 0 {
  315. switch params["action"] {
  316. case "new":
  317. if err = models.CreateComment(ctx.User.Id, ctx.Repo.Repository.Id, issue.Id, 0, 0, models.IT_PLAIN, content); err != nil {
  318. ctx.Handle(500, "issue.Comment(create comment)", err)
  319. return
  320. }
  321. // Update mentions.
  322. ms = base.MentionPattern.FindAllString(issue.Content, -1)
  323. if len(ms) > 0 {
  324. for i := range ms {
  325. ms[i] = ms[i][1:]
  326. }
  327. ids := models.GetUserIdsByNames(ms)
  328. if err := models.UpdateIssueUserPairsByMentions(ids, issue.Id); err != nil {
  329. ctx.Handle(500, "issue.CreateIssue(UpdateIssueUserPairsByMentions)", err)
  330. return
  331. }
  332. }
  333. log.Trace("%s Comment created: %d", ctx.Req.RequestURI, issue.Id)
  334. default:
  335. ctx.Handle(404, "issue.Comment", err)
  336. return
  337. }
  338. }
  339. // Notify watchers.
  340. if err = models.NotifyWatchers(&models.Action{ActUserId: ctx.User.Id, ActUserName: ctx.User.Name, ActEmail: ctx.User.Email,
  341. OpType: models.OP_COMMENT_ISSUE, Content: fmt.Sprintf("%d|%s", issue.Index, strings.Split(content, "\n")[0]),
  342. RepoId: ctx.Repo.Repository.Id, RepoName: ctx.Repo.Repository.Name, RefName: ""}); err != nil {
  343. ctx.Handle(500, "issue.CreateIssue(NotifyWatchers)", err)
  344. return
  345. }
  346. // Mail watchers and mentions.
  347. if base.Service.NotifyMail {
  348. issue.Content = content
  349. tos, err := mailer.SendIssueNotifyMail(ctx.User, ctx.Repo.Owner, ctx.Repo.Repository, issue)
  350. if err != nil {
  351. ctx.Handle(500, "issue.Comment(SendIssueNotifyMail)", err)
  352. return
  353. }
  354. tos = append(tos, ctx.User.LowerName)
  355. newTos := make([]string, 0, len(ms))
  356. for _, m := range ms {
  357. if com.IsSliceContainsStr(tos, m) {
  358. continue
  359. }
  360. newTos = append(newTos, m)
  361. }
  362. if err = mailer.SendIssueMentionMail(ctx.Render, ctx.User, ctx.Repo.Owner,
  363. ctx.Repo.Repository, issue, models.GetUserEmailsByNames(newTos)); err != nil {
  364. ctx.Handle(500, "issue.Comment(SendIssueMentionMail)", err)
  365. return
  366. }
  367. }
  368. ctx.Redirect(fmt.Sprintf("%s/issues/%d", ctx.Repo.RepoLink, index))
  369. }
  370. func Milestones(ctx *middleware.Context) {
  371. ctx.Data["Title"] = "Milestones"
  372. ctx.Data["IsRepoToolbarIssues"] = true
  373. ctx.Data["IsRepoToolbarIssuesList"] = true
  374. ctx.HTML(200, "issue/milestone")
  375. }