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
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
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
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199
  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 || ctx.Repo.Repository.EnableExternalTracker {
  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", 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", 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", 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", err)
  188. return
  189. }
  190. // Get assignees.
  191. ctx.Data["Assignees"], err = repo.GetAssignees()
  192. if err != nil {
  193. ctx.Handle(500, "GetAssignees", 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", err)
  225. return
  226. }
  227. ctx.Data["ClosedMilestones"], err = models.GetMilestones(repo.ID, -1, true)
  228. if err != nil {
  229. ctx.Handle(500, "GetMilestones", err)
  230. return
  231. }
  232. ctx.Data["Assignees"], err = repo.GetAssignees()
  233. if err != nil {
  234. ctx.Handle(500, "GetAssignees", 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", 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", 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", 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. Title: 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.Title
  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. ctx.Data["PageIsPullConversation"] = true
  449. } else {
  450. MustEnableIssues(ctx)
  451. if ctx.Written() {
  452. return
  453. }
  454. ctx.Data["PageIsIssueList"] = true
  455. }
  456. issue.RenderedContent = string(markdown.Render([]byte(issue.Content), ctx.Repo.RepoLink,
  457. ctx.Repo.Repository.ComposeMetas()))
  458. repo := ctx.Repo.Repository
  459. // Get more information if it's a pull request.
  460. if issue.IsPull {
  461. if issue.HasMerged {
  462. ctx.Data["DisableStatusChange"] = issue.HasMerged
  463. PrepareMergedViewPullInfo(ctx, issue)
  464. } else {
  465. PrepareViewPullInfo(ctx, issue)
  466. }
  467. if ctx.Written() {
  468. return
  469. }
  470. }
  471. // Metas.
  472. // Check labels.
  473. labelIDMark := make(map[int64]bool)
  474. for i := range issue.Labels {
  475. labelIDMark[issue.Labels[i].ID] = true
  476. }
  477. labels, err := models.GetLabelsByRepoID(repo.ID)
  478. if err != nil {
  479. ctx.Handle(500, "GetLabelsByRepoID", err)
  480. return
  481. }
  482. hasSelected := false
  483. for i := range labels {
  484. if labelIDMark[labels[i].ID] {
  485. labels[i].IsChecked = true
  486. hasSelected = true
  487. }
  488. }
  489. ctx.Data["HasSelectedLabel"] = hasSelected
  490. ctx.Data["Labels"] = labels
  491. // Check milestone and assignee.
  492. if ctx.Repo.IsWriter() {
  493. RetrieveRepoMilestonesAndAssignees(ctx, repo)
  494. if ctx.Written() {
  495. return
  496. }
  497. }
  498. if ctx.IsSigned {
  499. // Update issue-user.
  500. if err = issue.ReadBy(ctx.User.ID); err != nil {
  501. ctx.Handle(500, "ReadBy", err)
  502. return
  503. }
  504. }
  505. var (
  506. tag models.CommentTag
  507. ok bool
  508. marked = make(map[int64]models.CommentTag)
  509. comment *models.Comment
  510. participants = make([]*models.User, 1, 10)
  511. )
  512. // Render comments and and fetch participants.
  513. participants[0] = issue.Poster
  514. for _, comment = range issue.Comments {
  515. if comment.Type == models.COMMENT_TYPE_COMMENT {
  516. comment.RenderedContent = string(markdown.Render([]byte(comment.Content), ctx.Repo.RepoLink,
  517. ctx.Repo.Repository.ComposeMetas()))
  518. // Check tag.
  519. tag, ok = marked[comment.PosterID]
  520. if ok {
  521. comment.ShowTag = tag
  522. continue
  523. }
  524. if repo.IsOwnedBy(comment.PosterID) ||
  525. (repo.Owner.IsOrganization() && repo.Owner.IsOwnedBy(comment.PosterID)) {
  526. comment.ShowTag = models.COMMENT_TAG_OWNER
  527. } else if comment.Poster.IsWriterOfRepo(repo) {
  528. comment.ShowTag = models.COMMENT_TAG_WRITER
  529. } else if comment.PosterID == issue.PosterID {
  530. comment.ShowTag = models.COMMENT_TAG_POSTER
  531. }
  532. marked[comment.PosterID] = comment.ShowTag
  533. isAdded := false
  534. for j := range participants {
  535. if comment.Poster == participants[j] {
  536. isAdded = true
  537. break
  538. }
  539. }
  540. if !isAdded && !issue.IsPoster(comment.Poster.ID) {
  541. participants = append(participants, comment.Poster)
  542. }
  543. }
  544. }
  545. ctx.Data["Participants"] = participants
  546. ctx.Data["NumParticipants"] = len(participants)
  547. ctx.Data["Issue"] = issue
  548. ctx.Data["IsIssueOwner"] = ctx.Repo.IsWriter() || (ctx.IsSigned && issue.IsPoster(ctx.User.ID))
  549. ctx.Data["SignInLink"] = setting.AppSubUrl + "/user/login?redirect_to=" + ctx.Data["Link"].(string)
  550. ctx.Data["RequireHighlightJS"] = true
  551. ctx.HTML(200, ISSUE_VIEW)
  552. }
  553. func getActionIssue(ctx *context.Context) *models.Issue {
  554. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  555. if err != nil {
  556. if models.IsErrIssueNotExist(err) {
  557. ctx.Error(404, "GetIssueByIndex")
  558. } else {
  559. ctx.Handle(500, "GetIssueByIndex", err)
  560. }
  561. return nil
  562. }
  563. return issue
  564. }
  565. func UpdateIssueTitle(ctx *context.Context) {
  566. issue := getActionIssue(ctx)
  567. if ctx.Written() {
  568. return
  569. }
  570. if !ctx.IsSigned || (!issue.IsPoster(ctx.User.ID) && !ctx.Repo.IsWriter()) {
  571. ctx.Error(403)
  572. return
  573. }
  574. title := ctx.QueryTrim("title")
  575. if len(title) == 0 {
  576. ctx.Error(204)
  577. return
  578. }
  579. if err := issue.ChangeTitle(ctx.User, title); err != nil {
  580. ctx.Handle(500, "ChangeTitle", err)
  581. return
  582. }
  583. ctx.JSON(200, map[string]interface{}{
  584. "title": issue.Title,
  585. })
  586. }
  587. func UpdateIssueContent(ctx *context.Context) {
  588. issue := getActionIssue(ctx)
  589. if ctx.Written() {
  590. return
  591. }
  592. if !ctx.IsSigned || (ctx.User.ID != issue.PosterID && !ctx.Repo.IsWriter()) {
  593. ctx.Error(403)
  594. return
  595. }
  596. content := ctx.Query("content")
  597. if err := issue.ChangeContent(ctx.User, content); err != nil {
  598. ctx.Handle(500, "ChangeContent", err)
  599. return
  600. }
  601. ctx.JSON(200, map[string]interface{}{
  602. "content": string(markdown.Render([]byte(issue.Content), ctx.Query("context"), ctx.Repo.Repository.ComposeMetas())),
  603. })
  604. }
  605. func UpdateIssueLabel(ctx *context.Context) {
  606. issue := getActionIssue(ctx)
  607. if ctx.Written() {
  608. return
  609. }
  610. if ctx.Query("action") == "clear" {
  611. if err := issue.ClearLabels(ctx.User); err != nil {
  612. ctx.Handle(500, "ClearLabels", err)
  613. return
  614. }
  615. } else {
  616. isAttach := ctx.Query("action") == "attach"
  617. label, err := models.GetLabelByID(ctx.QueryInt64("id"))
  618. if err != nil {
  619. if models.IsErrLabelNotExist(err) {
  620. ctx.Error(404, "GetLabelByID")
  621. } else {
  622. ctx.Handle(500, "GetLabelByID", err)
  623. }
  624. return
  625. }
  626. if isAttach && !issue.HasLabel(label.ID) {
  627. if err = issue.AddLabel(ctx.User, label); err != nil {
  628. ctx.Handle(500, "AddLabel", err)
  629. return
  630. }
  631. } else if !isAttach && issue.HasLabel(label.ID) {
  632. if err = issue.RemoveLabel(ctx.User, label); err != nil {
  633. ctx.Handle(500, "RemoveLabel", err)
  634. return
  635. }
  636. }
  637. }
  638. ctx.JSON(200, map[string]interface{}{
  639. "ok": true,
  640. })
  641. }
  642. func UpdateIssueMilestone(ctx *context.Context) {
  643. issue := getActionIssue(ctx)
  644. if ctx.Written() {
  645. return
  646. }
  647. oldMid := issue.MilestoneID
  648. mid := ctx.QueryInt64("id")
  649. if oldMid == mid {
  650. ctx.JSON(200, map[string]interface{}{
  651. "ok": true,
  652. })
  653. return
  654. }
  655. // Not check for invalid milestone id and give responsibility to owners.
  656. issue.MilestoneID = mid
  657. if err := models.ChangeMilestoneAssign(oldMid, issue); err != nil {
  658. ctx.Handle(500, "ChangeMilestoneAssign", err)
  659. return
  660. }
  661. ctx.JSON(200, map[string]interface{}{
  662. "ok": true,
  663. })
  664. }
  665. func UpdateIssueAssignee(ctx *context.Context) {
  666. issue := getActionIssue(ctx)
  667. if ctx.Written() {
  668. return
  669. }
  670. assigneeID := ctx.QueryInt64("id")
  671. if issue.AssigneeID == assigneeID {
  672. ctx.JSON(200, map[string]interface{}{
  673. "ok": true,
  674. })
  675. return
  676. }
  677. if err := issue.ChangeAssignee(ctx.User, assigneeID); err != nil {
  678. ctx.Handle(500, "ChangeAssignee", err)
  679. return
  680. }
  681. ctx.JSON(200, map[string]interface{}{
  682. "ok": true,
  683. })
  684. }
  685. func NewComment(ctx *context.Context, form auth.CreateCommentForm) {
  686. issue, err := models.GetIssueByIndex(ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  687. if err != nil {
  688. ctx.HandleError("GetIssueByIndex", models.IsErrIssueNotExist, err, 404)
  689. return
  690. }
  691. var attachments []string
  692. if setting.AttachmentEnabled {
  693. attachments = form.Attachments
  694. }
  695. if ctx.HasError() {
  696. ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
  697. ctx.Redirect(fmt.Sprintf("%s/issues/%d", ctx.Repo.RepoLink, issue.Index))
  698. return
  699. }
  700. var comment *models.Comment
  701. defer func() {
  702. // Check if issue admin/poster changes the status of issue.
  703. if (ctx.Repo.IsWriter() || (ctx.IsSigned && issue.IsPoster(ctx.User.ID))) &&
  704. (form.Status == "reopen" || form.Status == "close") &&
  705. !(issue.IsPull && issue.HasMerged) {
  706. // Duplication and conflict check should apply to reopen pull request.
  707. var pr *models.PullRequest
  708. if form.Status == "reopen" && issue.IsPull {
  709. pull := issue.PullRequest
  710. pr, err = models.GetUnmergedPullRequest(pull.HeadRepoID, pull.BaseRepoID, pull.HeadBranch, pull.BaseBranch)
  711. if err != nil {
  712. if !models.IsErrPullRequestNotExist(err) {
  713. ctx.Handle(500, "GetUnmergedPullRequest", err)
  714. return
  715. }
  716. }
  717. // Regenerate patch and test conflict.
  718. if pr == nil {
  719. if err = issue.UpdatePatch(); err != nil {
  720. ctx.Handle(500, "UpdatePatch", err)
  721. return
  722. }
  723. issue.AddToTaskQueue()
  724. }
  725. }
  726. if pr != nil {
  727. ctx.Flash.Info(ctx.Tr("repo.pulls.open_unmerged_pull_exists", pr.Index))
  728. } else {
  729. if err = issue.ChangeStatus(ctx.User, ctx.Repo.Repository, form.Status == "close"); err != nil {
  730. log.Error(4, "ChangeStatus: %v", err)
  731. } else {
  732. log.Trace("Issue [%d] status changed to closed: %v", issue.ID, issue.IsClosed)
  733. }
  734. }
  735. }
  736. // Redirect to comment hashtag if there is any actual content.
  737. typeName := "issues"
  738. if issue.IsPull {
  739. typeName = "pulls"
  740. }
  741. if comment != nil {
  742. ctx.Redirect(fmt.Sprintf("%s/%s/%d#%s", ctx.Repo.RepoLink, typeName, issue.Index, comment.HashTag()))
  743. } else {
  744. ctx.Redirect(fmt.Sprintf("%s/%s/%d", ctx.Repo.RepoLink, typeName, issue.Index))
  745. }
  746. }()
  747. // Fix #321: Allow empty comments, as long as we have attachments.
  748. if len(form.Content) == 0 && len(attachments) == 0 {
  749. return
  750. }
  751. comment, err = models.CreateIssueComment(ctx.User, ctx.Repo.Repository, issue, form.Content, attachments)
  752. if err != nil {
  753. ctx.Handle(500, "CreateIssueComment", err)
  754. return
  755. }
  756. log.Trace("Comment created: %d/%d/%d", ctx.Repo.Repository.ID, issue.ID, comment.ID)
  757. }
  758. func UpdateCommentContent(ctx *context.Context) {
  759. comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
  760. if err != nil {
  761. ctx.HandleError("GetCommentByID", models.IsErrCommentNotExist, err, 404)
  762. return
  763. }
  764. if !ctx.IsSigned || (ctx.User.ID != comment.PosterID && !ctx.Repo.IsAdmin()) {
  765. ctx.Error(403)
  766. return
  767. } else if comment.Type != models.COMMENT_TYPE_COMMENT {
  768. ctx.Error(204)
  769. return
  770. }
  771. comment.Content = ctx.Query("content")
  772. if len(comment.Content) == 0 {
  773. ctx.JSON(200, map[string]interface{}{
  774. "content": "",
  775. })
  776. return
  777. }
  778. if err = models.UpdateComment(comment); err != nil {
  779. ctx.Handle(500, "UpdateComment", err)
  780. return
  781. }
  782. ctx.JSON(200, map[string]interface{}{
  783. "content": string(markdown.Render([]byte(comment.Content), ctx.Query("context"), ctx.Repo.Repository.ComposeMetas())),
  784. })
  785. }
  786. func DeleteComment(ctx *context.Context) {
  787. comment, err := models.GetCommentByID(ctx.ParamsInt64(":id"))
  788. if err != nil {
  789. ctx.HandleError("GetCommentByID", models.IsErrCommentNotExist, err, 404)
  790. return
  791. }
  792. if !ctx.IsSigned || (ctx.User.ID != comment.PosterID && !ctx.Repo.IsAdmin()) {
  793. ctx.Error(403)
  794. return
  795. } else if comment.Type != models.COMMENT_TYPE_COMMENT {
  796. ctx.Error(204)
  797. return
  798. }
  799. if err = models.DeleteCommentByID(comment.ID); err != nil {
  800. ctx.Handle(500, "DeleteCommentByID", err)
  801. return
  802. }
  803. ctx.Status(200)
  804. }
  805. func Labels(ctx *context.Context) {
  806. ctx.Data["Title"] = ctx.Tr("repo.labels")
  807. ctx.Data["PageIsIssueList"] = true
  808. ctx.Data["PageIsLabels"] = true
  809. ctx.Data["RequireMinicolors"] = true
  810. ctx.HTML(200, LABELS)
  811. }
  812. func NewLabel(ctx *context.Context, form auth.CreateLabelForm) {
  813. ctx.Data["Title"] = ctx.Tr("repo.labels")
  814. ctx.Data["PageIsLabels"] = true
  815. if ctx.HasError() {
  816. ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
  817. ctx.Redirect(ctx.Repo.RepoLink + "/labels")
  818. return
  819. }
  820. l := &models.Label{
  821. RepoID: ctx.Repo.Repository.ID,
  822. Name: form.Title,
  823. Color: form.Color,
  824. }
  825. if err := models.NewLabel(l); err != nil {
  826. ctx.Handle(500, "NewLabel", err)
  827. return
  828. }
  829. ctx.Redirect(ctx.Repo.RepoLink + "/labels")
  830. }
  831. func UpdateLabel(ctx *context.Context, form auth.CreateLabelForm) {
  832. l, err := models.GetLabelByID(form.ID)
  833. if err != nil {
  834. switch {
  835. case models.IsErrLabelNotExist(err):
  836. ctx.Error(404)
  837. default:
  838. ctx.Handle(500, "UpdateLabel", err)
  839. }
  840. return
  841. }
  842. l.Name = form.Title
  843. l.Color = form.Color
  844. if err := models.UpdateLabel(l); err != nil {
  845. ctx.Handle(500, "UpdateLabel", err)
  846. return
  847. }
  848. ctx.Redirect(ctx.Repo.RepoLink + "/labels")
  849. }
  850. func DeleteLabel(ctx *context.Context) {
  851. if err := models.DeleteLabel(ctx.Repo.Repository.ID, ctx.QueryInt64("id")); err != nil {
  852. ctx.Flash.Error("DeleteLabel: " + err.Error())
  853. } else {
  854. ctx.Flash.Success(ctx.Tr("repo.issues.label_deletion_success"))
  855. }
  856. ctx.JSON(200, map[string]interface{}{
  857. "redirect": ctx.Repo.RepoLink + "/labels",
  858. })
  859. return
  860. }
  861. func Milestones(ctx *context.Context) {
  862. ctx.Data["Title"] = ctx.Tr("repo.milestones")
  863. ctx.Data["PageIsIssueList"] = true
  864. ctx.Data["PageIsMilestones"] = true
  865. isShowClosed := ctx.Query("state") == "closed"
  866. openCount, closedCount := models.MilestoneStats(ctx.Repo.Repository.ID)
  867. ctx.Data["OpenCount"] = openCount
  868. ctx.Data["ClosedCount"] = closedCount
  869. page := ctx.QueryInt("page")
  870. if page <= 1 {
  871. page = 1
  872. }
  873. var total int
  874. if !isShowClosed {
  875. total = int(openCount)
  876. } else {
  877. total = int(closedCount)
  878. }
  879. ctx.Data["Page"] = paginater.New(total, setting.UI.IssuePagingNum, page, 5)
  880. miles, err := models.GetMilestones(ctx.Repo.Repository.ID, page, isShowClosed)
  881. if err != nil {
  882. ctx.Handle(500, "GetMilestones", err)
  883. return
  884. }
  885. for _, m := range miles {
  886. m.RenderedContent = string(markdown.Render([]byte(m.Content), ctx.Repo.RepoLink, ctx.Repo.Repository.ComposeMetas()))
  887. }
  888. ctx.Data["Milestones"] = miles
  889. if isShowClosed {
  890. ctx.Data["State"] = "closed"
  891. } else {
  892. ctx.Data["State"] = "open"
  893. }
  894. ctx.Data["IsShowClosed"] = isShowClosed
  895. ctx.HTML(200, MILESTONE)
  896. }
  897. func NewMilestone(ctx *context.Context) {
  898. ctx.Data["Title"] = ctx.Tr("repo.milestones.new")
  899. ctx.Data["PageIsIssueList"] = true
  900. ctx.Data["PageIsMilestones"] = true
  901. ctx.Data["RequireDatetimepicker"] = true
  902. ctx.Data["DateLang"] = setting.DateLang(ctx.Locale.Language())
  903. ctx.HTML(200, MILESTONE_NEW)
  904. }
  905. func NewMilestonePost(ctx *context.Context, form auth.CreateMilestoneForm) {
  906. ctx.Data["Title"] = ctx.Tr("repo.milestones.new")
  907. ctx.Data["PageIsIssueList"] = true
  908. ctx.Data["PageIsMilestones"] = true
  909. ctx.Data["RequireDatetimepicker"] = true
  910. ctx.Data["DateLang"] = setting.DateLang(ctx.Locale.Language())
  911. if ctx.HasError() {
  912. ctx.HTML(200, MILESTONE_NEW)
  913. return
  914. }
  915. if len(form.Deadline) == 0 {
  916. form.Deadline = "9999-12-31"
  917. }
  918. deadline, err := time.ParseInLocation("2006-01-02", form.Deadline, time.Local)
  919. if err != nil {
  920. ctx.Data["Err_Deadline"] = true
  921. ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), MILESTONE_NEW, &form)
  922. return
  923. }
  924. if err = models.NewMilestone(&models.Milestone{
  925. RepoID: ctx.Repo.Repository.ID,
  926. Name: form.Title,
  927. Content: form.Content,
  928. Deadline: deadline,
  929. }); err != nil {
  930. ctx.Handle(500, "NewMilestone", err)
  931. return
  932. }
  933. ctx.Flash.Success(ctx.Tr("repo.milestones.create_success", form.Title))
  934. ctx.Redirect(ctx.Repo.RepoLink + "/milestones")
  935. }
  936. func EditMilestone(ctx *context.Context) {
  937. ctx.Data["Title"] = ctx.Tr("repo.milestones.edit")
  938. ctx.Data["PageIsMilestones"] = true
  939. ctx.Data["PageIsEditMilestone"] = true
  940. ctx.Data["RequireDatetimepicker"] = true
  941. ctx.Data["DateLang"] = setting.DateLang(ctx.Locale.Language())
  942. m, err := models.GetMilestoneByID(ctx.ParamsInt64(":id"))
  943. if err != nil {
  944. if models.IsErrMilestoneNotExist(err) {
  945. ctx.Handle(404, "GetMilestoneByID", nil)
  946. } else {
  947. ctx.Handle(500, "GetMilestoneByID", err)
  948. }
  949. return
  950. }
  951. ctx.Data["title"] = m.Name
  952. ctx.Data["content"] = m.Content
  953. if len(m.DeadlineString) > 0 {
  954. ctx.Data["deadline"] = m.DeadlineString
  955. }
  956. ctx.HTML(200, MILESTONE_NEW)
  957. }
  958. func EditMilestonePost(ctx *context.Context, form auth.CreateMilestoneForm) {
  959. ctx.Data["Title"] = ctx.Tr("repo.milestones.edit")
  960. ctx.Data["PageIsMilestones"] = true
  961. ctx.Data["PageIsEditMilestone"] = true
  962. ctx.Data["RequireDatetimepicker"] = true
  963. ctx.Data["DateLang"] = setting.DateLang(ctx.Locale.Language())
  964. if ctx.HasError() {
  965. ctx.HTML(200, MILESTONE_NEW)
  966. return
  967. }
  968. if len(form.Deadline) == 0 {
  969. form.Deadline = "9999-12-31"
  970. }
  971. deadline, err := time.ParseInLocation("2006-01-02", form.Deadline, time.Local)
  972. if err != nil {
  973. ctx.Data["Err_Deadline"] = true
  974. ctx.RenderWithErr(ctx.Tr("repo.milestones.invalid_due_date_format"), MILESTONE_NEW, &form)
  975. return
  976. }
  977. m, err := models.GetMilestoneByID(ctx.ParamsInt64(":id"))
  978. if err != nil {
  979. if models.IsErrMilestoneNotExist(err) {
  980. ctx.Handle(404, "GetMilestoneByID", nil)
  981. } else {
  982. ctx.Handle(500, "GetMilestoneByID", err)
  983. }
  984. return
  985. }
  986. m.Name = form.Title
  987. m.Content = form.Content
  988. m.Deadline = deadline
  989. if err = models.UpdateMilestone(m); err != nil {
  990. ctx.Handle(500, "UpdateMilestone", err)
  991. return
  992. }
  993. ctx.Flash.Success(ctx.Tr("repo.milestones.edit_success", m.Name))
  994. ctx.Redirect(ctx.Repo.RepoLink + "/milestones")
  995. }
  996. func ChangeMilestonStatus(ctx *context.Context) {
  997. m, err := models.GetMilestoneByID(ctx.ParamsInt64(":id"))
  998. if err != nil {
  999. if models.IsErrMilestoneNotExist(err) {
  1000. ctx.Handle(404, "GetMilestoneByID", err)
  1001. } else {
  1002. ctx.Handle(500, "GetMilestoneByID", err)
  1003. }
  1004. return
  1005. }
  1006. switch ctx.Params(":action") {
  1007. case "open":
  1008. if m.IsClosed {
  1009. if err = models.ChangeMilestoneStatus(m, false); err != nil {
  1010. ctx.Handle(500, "ChangeMilestoneStatus", err)
  1011. return
  1012. }
  1013. }
  1014. ctx.Redirect(ctx.Repo.RepoLink + "/milestones?state=open")
  1015. case "close":
  1016. if !m.IsClosed {
  1017. m.ClosedDate = time.Now()
  1018. if err = models.ChangeMilestoneStatus(m, true); err != nil {
  1019. ctx.Handle(500, "ChangeMilestoneStatus", err)
  1020. return
  1021. }
  1022. }
  1023. ctx.Redirect(ctx.Repo.RepoLink + "/milestones?state=closed")
  1024. default:
  1025. ctx.Redirect(ctx.Repo.RepoLink + "/milestones")
  1026. }
  1027. }
  1028. func DeleteMilestone(ctx *context.Context) {
  1029. if err := models.DeleteMilestoneByID(ctx.QueryInt64("id")); err != nil {
  1030. ctx.Flash.Error("DeleteMilestoneByID: " + err.Error())
  1031. } else {
  1032. ctx.Flash.Success(ctx.Tr("repo.milestones.deletion_success"))
  1033. }
  1034. ctx.JSON(200, map[string]interface{}{
  1035. "redirect": ctx.Repo.RepoLink + "/milestones",
  1036. })
  1037. }