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 29 kB

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