1
0

issue.go 30 KB

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