Fork to maintain patches against the official gitea for https://code.ceondo.com https://github.com/go-gitea/gitea

issue.go 33KB

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