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