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

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