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

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