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