issue.go 32 KB

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