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

issue.go 29 kB

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