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.

issue_comment.go 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821
  1. // Copyright 2016 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 models
  5. import (
  6. "fmt"
  7. "strings"
  8. "github.com/Unknwon/com"
  9. "github.com/go-xorm/builder"
  10. "github.com/go-xorm/xorm"
  11. api "code.gitea.io/sdk/gitea"
  12. "code.gitea.io/gitea/modules/log"
  13. "code.gitea.io/gitea/modules/markup"
  14. "code.gitea.io/gitea/modules/util"
  15. )
  16. // CommentType defines whether a comment is just a simple comment, an action (like close) or a reference.
  17. type CommentType int
  18. // define unknown comment type
  19. const (
  20. CommentTypeUnknown CommentType = -1
  21. )
  22. // Enumerate all the comment types
  23. const (
  24. // Plain comment, can be associated with a commit (CommitID > 0) and a line (LineNum > 0)
  25. CommentTypeComment CommentType = iota
  26. CommentTypeReopen
  27. CommentTypeClose
  28. // References.
  29. CommentTypeIssueRef
  30. // Reference from a commit (not part of a pull request)
  31. CommentTypeCommitRef
  32. // Reference from a comment
  33. CommentTypeCommentRef
  34. // Reference from a pull request
  35. CommentTypePullRef
  36. // Labels changed
  37. CommentTypeLabel
  38. // Milestone changed
  39. CommentTypeMilestone
  40. // Assignees changed
  41. CommentTypeAssignees
  42. // Change Title
  43. CommentTypeChangeTitle
  44. // Delete Branch
  45. CommentTypeDeleteBranch
  46. // Start a stopwatch for time tracking
  47. CommentTypeStartTracking
  48. // Stop a stopwatch for time tracking
  49. CommentTypeStopTracking
  50. // Add time manual for time tracking
  51. CommentTypeAddTimeManual
  52. // Cancel a stopwatch for time tracking
  53. CommentTypeCancelTracking
  54. // Added a due date
  55. CommentTypeAddedDeadline
  56. // Modified the due date
  57. CommentTypeModifiedDeadline
  58. // Removed a due date
  59. CommentTypeRemovedDeadline
  60. )
  61. // CommentTag defines comment tag type
  62. type CommentTag int
  63. // Enumerate all the comment tag types
  64. const (
  65. CommentTagNone CommentTag = iota
  66. CommentTagPoster
  67. CommentTagWriter
  68. CommentTagOwner
  69. )
  70. // Comment represents a comment in commit and issue page.
  71. type Comment struct {
  72. ID int64 `xorm:"pk autoincr"`
  73. Type CommentType
  74. PosterID int64 `xorm:"INDEX"`
  75. Poster *User `xorm:"-"`
  76. IssueID int64 `xorm:"INDEX"`
  77. Issue *Issue `xorm:"-"`
  78. LabelID int64
  79. Label *Label `xorm:"-"`
  80. OldMilestoneID int64
  81. MilestoneID int64
  82. OldMilestone *Milestone `xorm:"-"`
  83. Milestone *Milestone `xorm:"-"`
  84. AssigneeID int64
  85. RemovedAssignee bool
  86. Assignee *User `xorm:"-"`
  87. OldTitle string
  88. NewTitle string
  89. CommitID int64
  90. Line int64
  91. Content string `xorm:"TEXT"`
  92. RenderedContent string `xorm:"-"`
  93. CreatedUnix util.TimeStamp `xorm:"INDEX created"`
  94. UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
  95. // Reference issue in commit message
  96. CommitSHA string `xorm:"VARCHAR(40)"`
  97. Attachments []*Attachment `xorm:"-"`
  98. Reactions ReactionList `xorm:"-"`
  99. // For view issue page.
  100. ShowTag CommentTag `xorm:"-"`
  101. }
  102. // LoadIssue loads issue from database
  103. func (c *Comment) LoadIssue() (err error) {
  104. if c.Issue != nil {
  105. return nil
  106. }
  107. c.Issue, err = GetIssueByID(c.IssueID)
  108. return
  109. }
  110. // AfterLoad is invoked from XORM after setting the values of all fields of this object.
  111. func (c *Comment) AfterLoad(session *xorm.Session) {
  112. var err error
  113. c.Attachments, err = getAttachmentsByCommentID(session, c.ID)
  114. if err != nil {
  115. log.Error(3, "getAttachmentsByCommentID[%d]: %v", c.ID, err)
  116. }
  117. c.Poster, err = getUserByID(session, c.PosterID)
  118. if err != nil {
  119. if IsErrUserNotExist(err) {
  120. c.PosterID = -1
  121. c.Poster = NewGhostUser()
  122. } else {
  123. log.Error(3, "getUserByID[%d]: %v", c.ID, err)
  124. }
  125. }
  126. }
  127. // AfterDelete is invoked from XORM after the object is deleted.
  128. func (c *Comment) AfterDelete() {
  129. if c.ID <= 0 {
  130. return
  131. }
  132. _, err := DeleteAttachmentsByComment(c.ID, true)
  133. if err != nil {
  134. log.Info("Could not delete files for comment %d on issue #%d: %s", c.ID, c.IssueID, err)
  135. }
  136. }
  137. // HTMLURL formats a URL-string to the issue-comment
  138. func (c *Comment) HTMLURL() string {
  139. err := c.LoadIssue()
  140. if err != nil { // Silently dropping errors :unamused:
  141. log.Error(4, "LoadIssue(%d): %v", c.IssueID, err)
  142. return ""
  143. }
  144. return fmt.Sprintf("%s#%s", c.Issue.HTMLURL(), c.HashTag())
  145. }
  146. // IssueURL formats a URL-string to the issue
  147. func (c *Comment) IssueURL() string {
  148. err := c.LoadIssue()
  149. if err != nil { // Silently dropping errors :unamused:
  150. log.Error(4, "LoadIssue(%d): %v", c.IssueID, err)
  151. return ""
  152. }
  153. if c.Issue.IsPull {
  154. return ""
  155. }
  156. return c.Issue.HTMLURL()
  157. }
  158. // PRURL formats a URL-string to the pull-request
  159. func (c *Comment) PRURL() string {
  160. err := c.LoadIssue()
  161. if err != nil { // Silently dropping errors :unamused:
  162. log.Error(4, "LoadIssue(%d): %v", c.IssueID, err)
  163. return ""
  164. }
  165. if !c.Issue.IsPull {
  166. return ""
  167. }
  168. return c.Issue.HTMLURL()
  169. }
  170. // APIFormat converts a Comment to the api.Comment format
  171. func (c *Comment) APIFormat() *api.Comment {
  172. return &api.Comment{
  173. ID: c.ID,
  174. Poster: c.Poster.APIFormat(),
  175. HTMLURL: c.HTMLURL(),
  176. IssueURL: c.IssueURL(),
  177. PRURL: c.PRURL(),
  178. Body: c.Content,
  179. Created: c.CreatedUnix.AsTime(),
  180. Updated: c.UpdatedUnix.AsTime(),
  181. }
  182. }
  183. // CommentHashTag returns unique hash tag for comment id.
  184. func CommentHashTag(id int64) string {
  185. return fmt.Sprintf("issuecomment-%d", id)
  186. }
  187. // HashTag returns unique hash tag for comment.
  188. func (c *Comment) HashTag() string {
  189. return CommentHashTag(c.ID)
  190. }
  191. // EventTag returns unique event hash tag for comment.
  192. func (c *Comment) EventTag() string {
  193. return "event-" + com.ToStr(c.ID)
  194. }
  195. // LoadLabel if comment.Type is CommentTypeLabel, then load Label
  196. func (c *Comment) LoadLabel() error {
  197. var label Label
  198. has, err := x.ID(c.LabelID).Get(&label)
  199. if err != nil {
  200. return err
  201. } else if has {
  202. c.Label = &label
  203. } else {
  204. // Ignore Label is deleted, but not clear this table
  205. log.Warn("Commit %d cannot load label %d", c.ID, c.LabelID)
  206. }
  207. return nil
  208. }
  209. // LoadMilestone if comment.Type is CommentTypeMilestone, then load milestone
  210. func (c *Comment) LoadMilestone() error {
  211. if c.OldMilestoneID > 0 {
  212. var oldMilestone Milestone
  213. has, err := x.ID(c.OldMilestoneID).Get(&oldMilestone)
  214. if err != nil {
  215. return err
  216. } else if has {
  217. c.OldMilestone = &oldMilestone
  218. }
  219. }
  220. if c.MilestoneID > 0 {
  221. var milestone Milestone
  222. has, err := x.ID(c.MilestoneID).Get(&milestone)
  223. if err != nil {
  224. return err
  225. } else if has {
  226. c.Milestone = &milestone
  227. }
  228. }
  229. return nil
  230. }
  231. // LoadAssigneeUser if comment.Type is CommentTypeAssignees, then load assignees
  232. func (c *Comment) LoadAssigneeUser() error {
  233. var err error
  234. if c.AssigneeID > 0 {
  235. c.Assignee, err = getUserByID(x, c.AssigneeID)
  236. if err != nil {
  237. if !IsErrUserNotExist(err) {
  238. return err
  239. }
  240. c.Assignee = NewGhostUser()
  241. }
  242. }
  243. return nil
  244. }
  245. // MailParticipants sends new comment emails to repository watchers
  246. // and mentioned people.
  247. func (c *Comment) MailParticipants(e Engine, opType ActionType, issue *Issue) (err error) {
  248. mentions := markup.FindAllMentions(c.Content)
  249. if err = UpdateIssueMentions(e, c.IssueID, mentions); err != nil {
  250. return fmt.Errorf("UpdateIssueMentions [%d]: %v", c.IssueID, err)
  251. }
  252. content := c.Content
  253. switch opType {
  254. case ActionCloseIssue:
  255. content = fmt.Sprintf("Closed #%d", issue.Index)
  256. case ActionReopenIssue:
  257. content = fmt.Sprintf("Reopened #%d", issue.Index)
  258. }
  259. if err = mailIssueCommentToParticipants(e, issue, c.Poster, content, c, mentions); err != nil {
  260. log.Error(4, "mailIssueCommentToParticipants: %v", err)
  261. }
  262. return nil
  263. }
  264. func (c *Comment) loadReactions(e Engine) (err error) {
  265. if c.Reactions != nil {
  266. return nil
  267. }
  268. c.Reactions, err = findReactions(e, FindReactionsOptions{
  269. IssueID: c.IssueID,
  270. CommentID: c.ID,
  271. })
  272. if err != nil {
  273. return err
  274. }
  275. // Load reaction user data
  276. if _, err := c.Reactions.LoadUsers(); err != nil {
  277. return err
  278. }
  279. return nil
  280. }
  281. // LoadReactions loads comment reactions
  282. func (c *Comment) LoadReactions() error {
  283. return c.loadReactions(x)
  284. }
  285. func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err error) {
  286. var LabelID int64
  287. if opts.Label != nil {
  288. LabelID = opts.Label.ID
  289. }
  290. comment := &Comment{
  291. Type: opts.Type,
  292. PosterID: opts.Doer.ID,
  293. Poster: opts.Doer,
  294. IssueID: opts.Issue.ID,
  295. LabelID: LabelID,
  296. OldMilestoneID: opts.OldMilestoneID,
  297. MilestoneID: opts.MilestoneID,
  298. RemovedAssignee: opts.RemovedAssignee,
  299. AssigneeID: opts.AssigneeID,
  300. CommitID: opts.CommitID,
  301. CommitSHA: opts.CommitSHA,
  302. Line: opts.LineNum,
  303. Content: opts.Content,
  304. OldTitle: opts.OldTitle,
  305. NewTitle: opts.NewTitle,
  306. }
  307. if _, err = e.Insert(comment); err != nil {
  308. return nil, err
  309. }
  310. if err = opts.Repo.getOwner(e); err != nil {
  311. return nil, err
  312. }
  313. // Compose comment action, could be plain comment, close or reopen issue/pull request.
  314. // This object will be used to notify watchers in the end of function.
  315. act := &Action{
  316. ActUserID: opts.Doer.ID,
  317. ActUser: opts.Doer,
  318. Content: fmt.Sprintf("%d|%s", opts.Issue.Index, strings.Split(opts.Content, "\n")[0]),
  319. RepoID: opts.Repo.ID,
  320. Repo: opts.Repo,
  321. Comment: comment,
  322. CommentID: comment.ID,
  323. IsPrivate: opts.Repo.IsPrivate,
  324. }
  325. // Check comment type.
  326. switch opts.Type {
  327. case CommentTypeComment:
  328. act.OpType = ActionCommentIssue
  329. if _, err = e.Exec("UPDATE `issue` SET num_comments=num_comments+1 WHERE id=?", opts.Issue.ID); err != nil {
  330. return nil, err
  331. }
  332. // Check attachments
  333. attachments := make([]*Attachment, 0, len(opts.Attachments))
  334. for _, uuid := range opts.Attachments {
  335. attach, err := getAttachmentByUUID(e, uuid)
  336. if err != nil {
  337. if IsErrAttachmentNotExist(err) {
  338. continue
  339. }
  340. return nil, fmt.Errorf("getAttachmentByUUID [%s]: %v", uuid, err)
  341. }
  342. attachments = append(attachments, attach)
  343. }
  344. for i := range attachments {
  345. attachments[i].IssueID = opts.Issue.ID
  346. attachments[i].CommentID = comment.ID
  347. // No assign value could be 0, so ignore AllCols().
  348. if _, err = e.ID(attachments[i].ID).Update(attachments[i]); err != nil {
  349. return nil, fmt.Errorf("update attachment [%d]: %v", attachments[i].ID, err)
  350. }
  351. }
  352. case CommentTypeReopen:
  353. act.OpType = ActionReopenIssue
  354. if opts.Issue.IsPull {
  355. act.OpType = ActionReopenPullRequest
  356. }
  357. if opts.Issue.IsPull {
  358. _, err = e.Exec("UPDATE `repository` SET num_closed_pulls=num_closed_pulls-1 WHERE id=?", opts.Repo.ID)
  359. } else {
  360. _, err = e.Exec("UPDATE `repository` SET num_closed_issues=num_closed_issues-1 WHERE id=?", opts.Repo.ID)
  361. }
  362. if err != nil {
  363. return nil, err
  364. }
  365. case CommentTypeClose:
  366. act.OpType = ActionCloseIssue
  367. if opts.Issue.IsPull {
  368. act.OpType = ActionClosePullRequest
  369. }
  370. if opts.Issue.IsPull {
  371. _, err = e.Exec("UPDATE `repository` SET num_closed_pulls=num_closed_pulls+1 WHERE id=?", opts.Repo.ID)
  372. } else {
  373. _, err = e.Exec("UPDATE `repository` SET num_closed_issues=num_closed_issues+1 WHERE id=?", opts.Repo.ID)
  374. }
  375. if err != nil {
  376. return nil, err
  377. }
  378. }
  379. // update the issue's updated_unix column
  380. if err = updateIssueCols(e, opts.Issue, "updated_unix"); err != nil {
  381. return nil, err
  382. }
  383. // Notify watchers for whatever action comes in, ignore if no action type.
  384. if act.OpType > 0 {
  385. if err = notifyWatchers(e, act); err != nil {
  386. log.Error(4, "notifyWatchers: %v", err)
  387. }
  388. if err = comment.MailParticipants(e, act.OpType, opts.Issue); err != nil {
  389. log.Error(4, "MailParticipants: %v", err)
  390. }
  391. }
  392. return comment, nil
  393. }
  394. func createStatusComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue) (*Comment, error) {
  395. cmtType := CommentTypeClose
  396. if !issue.IsClosed {
  397. cmtType = CommentTypeReopen
  398. }
  399. return createComment(e, &CreateCommentOptions{
  400. Type: cmtType,
  401. Doer: doer,
  402. Repo: repo,
  403. Issue: issue,
  404. })
  405. }
  406. func createLabelComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue, label *Label, add bool) (*Comment, error) {
  407. var content string
  408. if add {
  409. content = "1"
  410. }
  411. return createComment(e, &CreateCommentOptions{
  412. Type: CommentTypeLabel,
  413. Doer: doer,
  414. Repo: repo,
  415. Issue: issue,
  416. Label: label,
  417. Content: content,
  418. })
  419. }
  420. func createMilestoneComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue, oldMilestoneID, milestoneID int64) (*Comment, error) {
  421. return createComment(e, &CreateCommentOptions{
  422. Type: CommentTypeMilestone,
  423. Doer: doer,
  424. Repo: repo,
  425. Issue: issue,
  426. OldMilestoneID: oldMilestoneID,
  427. MilestoneID: milestoneID,
  428. })
  429. }
  430. func createAssigneeComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue, assigneeID int64, removedAssignee bool) (*Comment, error) {
  431. return createComment(e, &CreateCommentOptions{
  432. Type: CommentTypeAssignees,
  433. Doer: doer,
  434. Repo: repo,
  435. Issue: issue,
  436. RemovedAssignee: removedAssignee,
  437. AssigneeID: assigneeID,
  438. })
  439. }
  440. func createDeadlineComment(e *xorm.Session, doer *User, issue *Issue, newDeadlineUnix util.TimeStamp) (*Comment, error) {
  441. var content string
  442. var commentType CommentType
  443. // newDeadline = 0 means deleting
  444. if newDeadlineUnix == 0 {
  445. commentType = CommentTypeRemovedDeadline
  446. content = issue.DeadlineUnix.Format("2006-01-02")
  447. } else if issue.DeadlineUnix == 0 {
  448. // Check if the new date was added or modified
  449. // If the actual deadline is 0 => deadline added
  450. commentType = CommentTypeAddedDeadline
  451. content = newDeadlineUnix.Format("2006-01-02")
  452. } else { // Otherwise modified
  453. commentType = CommentTypeModifiedDeadline
  454. content = newDeadlineUnix.Format("2006-01-02") + "|" + issue.DeadlineUnix.Format("2006-01-02")
  455. }
  456. return createComment(e, &CreateCommentOptions{
  457. Type: commentType,
  458. Doer: doer,
  459. Repo: issue.Repo,
  460. Issue: issue,
  461. Content: content,
  462. })
  463. }
  464. func createChangeTitleComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue, oldTitle, newTitle string) (*Comment, error) {
  465. return createComment(e, &CreateCommentOptions{
  466. Type: CommentTypeChangeTitle,
  467. Doer: doer,
  468. Repo: repo,
  469. Issue: issue,
  470. OldTitle: oldTitle,
  471. NewTitle: newTitle,
  472. })
  473. }
  474. func createDeleteBranchComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue, branchName string) (*Comment, error) {
  475. return createComment(e, &CreateCommentOptions{
  476. Type: CommentTypeDeleteBranch,
  477. Doer: doer,
  478. Repo: repo,
  479. Issue: issue,
  480. CommitSHA: branchName,
  481. })
  482. }
  483. // CreateCommentOptions defines options for creating comment
  484. type CreateCommentOptions struct {
  485. Type CommentType
  486. Doer *User
  487. Repo *Repository
  488. Issue *Issue
  489. Label *Label
  490. OldMilestoneID int64
  491. MilestoneID int64
  492. AssigneeID int64
  493. RemovedAssignee bool
  494. OldTitle string
  495. NewTitle string
  496. CommitID int64
  497. CommitSHA string
  498. LineNum int64
  499. Content string
  500. Attachments []string // UUIDs of attachments
  501. }
  502. // CreateComment creates comment of issue or commit.
  503. func CreateComment(opts *CreateCommentOptions) (comment *Comment, err error) {
  504. sess := x.NewSession()
  505. defer sess.Close()
  506. if err = sess.Begin(); err != nil {
  507. return nil, err
  508. }
  509. comment, err = createComment(sess, opts)
  510. if err != nil {
  511. return nil, err
  512. }
  513. if err = sess.Commit(); err != nil {
  514. return nil, err
  515. }
  516. if opts.Type == CommentTypeComment {
  517. UpdateIssueIndexer(opts.Issue.ID)
  518. }
  519. return comment, nil
  520. }
  521. // CreateIssueComment creates a plain issue comment.
  522. func CreateIssueComment(doer *User, repo *Repository, issue *Issue, content string, attachments []string) (*Comment, error) {
  523. comment, err := CreateComment(&CreateCommentOptions{
  524. Type: CommentTypeComment,
  525. Doer: doer,
  526. Repo: repo,
  527. Issue: issue,
  528. Content: content,
  529. Attachments: attachments,
  530. })
  531. if err != nil {
  532. return nil, fmt.Errorf("CreateComment: %v", err)
  533. }
  534. mode, _ := AccessLevel(doer.ID, repo)
  535. if err = PrepareWebhooks(repo, HookEventIssueComment, &api.IssueCommentPayload{
  536. Action: api.HookIssueCommentCreated,
  537. Issue: issue.APIFormat(),
  538. Comment: comment.APIFormat(),
  539. Repository: repo.APIFormat(mode),
  540. Sender: doer.APIFormat(),
  541. }); err != nil {
  542. log.Error(2, "PrepareWebhooks [comment_id: %d]: %v", comment.ID, err)
  543. } else {
  544. go HookQueue.Add(repo.ID)
  545. }
  546. return comment, nil
  547. }
  548. // CreateRefComment creates a commit reference comment to issue.
  549. func CreateRefComment(doer *User, repo *Repository, issue *Issue, content, commitSHA string) error {
  550. if len(commitSHA) == 0 {
  551. return fmt.Errorf("cannot create reference with empty commit SHA")
  552. }
  553. // Check if same reference from same commit has already existed.
  554. has, err := x.Get(&Comment{
  555. Type: CommentTypeCommitRef,
  556. IssueID: issue.ID,
  557. CommitSHA: commitSHA,
  558. })
  559. if err != nil {
  560. return fmt.Errorf("check reference comment: %v", err)
  561. } else if has {
  562. return nil
  563. }
  564. _, err = CreateComment(&CreateCommentOptions{
  565. Type: CommentTypeCommitRef,
  566. Doer: doer,
  567. Repo: repo,
  568. Issue: issue,
  569. CommitSHA: commitSHA,
  570. Content: content,
  571. })
  572. return err
  573. }
  574. // GetCommentByID returns the comment by given ID.
  575. func GetCommentByID(id int64) (*Comment, error) {
  576. c := new(Comment)
  577. has, err := x.ID(id).Get(c)
  578. if err != nil {
  579. return nil, err
  580. } else if !has {
  581. return nil, ErrCommentNotExist{id, 0}
  582. }
  583. return c, nil
  584. }
  585. // FindCommentsOptions describes the conditions to Find comments
  586. type FindCommentsOptions struct {
  587. RepoID int64
  588. IssueID int64
  589. Since int64
  590. Type CommentType
  591. }
  592. func (opts *FindCommentsOptions) toConds() builder.Cond {
  593. var cond = builder.NewCond()
  594. if opts.RepoID > 0 {
  595. cond = cond.And(builder.Eq{"issue.repo_id": opts.RepoID})
  596. }
  597. if opts.IssueID > 0 {
  598. cond = cond.And(builder.Eq{"comment.issue_id": opts.IssueID})
  599. }
  600. if opts.Since > 0 {
  601. cond = cond.And(builder.Gte{"comment.updated_unix": opts.Since})
  602. }
  603. if opts.Type != CommentTypeUnknown {
  604. cond = cond.And(builder.Eq{"comment.type": opts.Type})
  605. }
  606. return cond
  607. }
  608. func findComments(e Engine, opts FindCommentsOptions) ([]*Comment, error) {
  609. comments := make([]*Comment, 0, 10)
  610. sess := e.Where(opts.toConds())
  611. if opts.RepoID > 0 {
  612. sess.Join("INNER", "issue", "issue.id = comment.issue_id")
  613. }
  614. return comments, sess.
  615. Asc("comment.created_unix").
  616. Asc("comment.id").
  617. Find(&comments)
  618. }
  619. // FindComments returns all comments according options
  620. func FindComments(opts FindCommentsOptions) ([]*Comment, error) {
  621. return findComments(x, opts)
  622. }
  623. // GetCommentsByIssueID returns all comments of an issue.
  624. func GetCommentsByIssueID(issueID int64) ([]*Comment, error) {
  625. return findComments(x, FindCommentsOptions{
  626. IssueID: issueID,
  627. Type: CommentTypeUnknown,
  628. })
  629. }
  630. // GetCommentsByIssueIDSince returns a list of comments of an issue since a given time point.
  631. func GetCommentsByIssueIDSince(issueID, since int64) ([]*Comment, error) {
  632. return findComments(x, FindCommentsOptions{
  633. IssueID: issueID,
  634. Type: CommentTypeUnknown,
  635. Since: since,
  636. })
  637. }
  638. // GetCommentsByRepoIDSince returns a list of comments for all issues in a repo since a given time point.
  639. func GetCommentsByRepoIDSince(repoID, since int64) ([]*Comment, error) {
  640. return findComments(x, FindCommentsOptions{
  641. RepoID: repoID,
  642. Type: CommentTypeUnknown,
  643. Since: since,
  644. })
  645. }
  646. // UpdateComment updates information of comment.
  647. func UpdateComment(doer *User, c *Comment, oldContent string) error {
  648. if _, err := x.ID(c.ID).AllCols().Update(c); err != nil {
  649. return err
  650. } else if c.Type == CommentTypeComment {
  651. UpdateIssueIndexer(c.IssueID)
  652. }
  653. if err := c.LoadIssue(); err != nil {
  654. return err
  655. }
  656. if err := c.Issue.LoadAttributes(); err != nil {
  657. return err
  658. }
  659. mode, _ := AccessLevel(doer.ID, c.Issue.Repo)
  660. if err := PrepareWebhooks(c.Issue.Repo, HookEventIssueComment, &api.IssueCommentPayload{
  661. Action: api.HookIssueCommentEdited,
  662. Issue: c.Issue.APIFormat(),
  663. Comment: c.APIFormat(),
  664. Changes: &api.ChangesPayload{
  665. Body: &api.ChangesFromPayload{
  666. From: oldContent,
  667. },
  668. },
  669. Repository: c.Issue.Repo.APIFormat(mode),
  670. Sender: doer.APIFormat(),
  671. }); err != nil {
  672. log.Error(2, "PrepareWebhooks [comment_id: %d]: %v", c.ID, err)
  673. } else {
  674. go HookQueue.Add(c.Issue.Repo.ID)
  675. }
  676. return nil
  677. }
  678. // DeleteComment deletes the comment
  679. func DeleteComment(doer *User, comment *Comment) error {
  680. sess := x.NewSession()
  681. defer sess.Close()
  682. if err := sess.Begin(); err != nil {
  683. return err
  684. }
  685. if _, err := sess.Delete(&Comment{
  686. ID: comment.ID,
  687. }); err != nil {
  688. return err
  689. }
  690. if comment.Type == CommentTypeComment {
  691. if _, err := sess.Exec("UPDATE `issue` SET num_comments = num_comments - 1 WHERE id = ?", comment.IssueID); err != nil {
  692. return err
  693. }
  694. }
  695. if _, err := sess.Where("comment_id = ?", comment.ID).Cols("is_deleted").Update(&Action{IsDeleted: true}); err != nil {
  696. return err
  697. }
  698. if err := sess.Commit(); err != nil {
  699. return err
  700. } else if comment.Type == CommentTypeComment {
  701. UpdateIssueIndexer(comment.IssueID)
  702. }
  703. if err := comment.LoadIssue(); err != nil {
  704. return err
  705. }
  706. if err := comment.Issue.LoadAttributes(); err != nil {
  707. return err
  708. }
  709. mode, _ := AccessLevel(doer.ID, comment.Issue.Repo)
  710. if err := PrepareWebhooks(comment.Issue.Repo, HookEventIssueComment, &api.IssueCommentPayload{
  711. Action: api.HookIssueCommentDeleted,
  712. Issue: comment.Issue.APIFormat(),
  713. Comment: comment.APIFormat(),
  714. Repository: comment.Issue.Repo.APIFormat(mode),
  715. Sender: doer.APIFormat(),
  716. }); err != nil {
  717. log.Error(2, "PrepareWebhooks [comment_id: %d]: %v", comment.ID, err)
  718. } else {
  719. go HookQueue.Add(comment.Issue.Repo.ID)
  720. }
  721. return nil
  722. }