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