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