You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

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