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