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 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681
  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. "time"
  9. "github.com/Unknwon/com"
  10. "github.com/go-xorm/builder"
  11. "github.com/go-xorm/xorm"
  12. api "code.gitea.io/sdk/gitea"
  13. "code.gitea.io/gitea/modules/log"
  14. "code.gitea.io/gitea/modules/markdown"
  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. )
  47. // CommentTag defines comment tag type
  48. type CommentTag int
  49. // Enumerate all the comment tag types
  50. const (
  51. CommentTagNone CommentTag = iota
  52. CommentTagPoster
  53. CommentTagWriter
  54. CommentTagOwner
  55. )
  56. // Comment represents a comment in commit and issue page.
  57. type Comment struct {
  58. ID int64 `xorm:"pk autoincr"`
  59. Type CommentType
  60. PosterID int64 `xorm:"INDEX"`
  61. Poster *User `xorm:"-"`
  62. IssueID int64 `xorm:"INDEX"`
  63. LabelID int64
  64. Label *Label `xorm:"-"`
  65. OldMilestoneID int64
  66. MilestoneID int64
  67. OldMilestone *Milestone `xorm:"-"`
  68. Milestone *Milestone `xorm:"-"`
  69. OldAssigneeID int64
  70. AssigneeID int64
  71. Assignee *User `xorm:"-"`
  72. OldAssignee *User `xorm:"-"`
  73. OldTitle string
  74. NewTitle string
  75. CommitID int64
  76. Line int64
  77. Content string `xorm:"TEXT"`
  78. RenderedContent string `xorm:"-"`
  79. Created time.Time `xorm:"-"`
  80. CreatedUnix int64 `xorm:"INDEX"`
  81. Updated time.Time `xorm:"-"`
  82. UpdatedUnix int64 `xorm:"INDEX"`
  83. // Reference issue in commit message
  84. CommitSHA string `xorm:"VARCHAR(40)"`
  85. Attachments []*Attachment `xorm:"-"`
  86. // For view issue page.
  87. ShowTag CommentTag `xorm:"-"`
  88. }
  89. // BeforeInsert will be invoked by XORM before inserting a record
  90. // representing this object.
  91. func (c *Comment) BeforeInsert() {
  92. c.CreatedUnix = time.Now().Unix()
  93. c.UpdatedUnix = c.CreatedUnix
  94. }
  95. // BeforeUpdate is invoked from XORM before updating this object.
  96. func (c *Comment) BeforeUpdate() {
  97. c.UpdatedUnix = time.Now().Unix()
  98. }
  99. // AfterSet is invoked from XORM after setting the value of a field of this object.
  100. func (c *Comment) AfterSet(colName string, _ xorm.Cell) {
  101. var err error
  102. switch colName {
  103. case "id":
  104. c.Attachments, err = GetAttachmentsByCommentID(c.ID)
  105. if err != nil {
  106. log.Error(3, "GetAttachmentsByCommentID[%d]: %v", c.ID, err)
  107. }
  108. case "poster_id":
  109. c.Poster, err = GetUserByID(c.PosterID)
  110. if err != nil {
  111. if IsErrUserNotExist(err) {
  112. c.PosterID = -1
  113. c.Poster = NewGhostUser()
  114. } else {
  115. log.Error(3, "GetUserByID[%d]: %v", c.ID, err)
  116. }
  117. }
  118. case "created_unix":
  119. c.Created = time.Unix(c.CreatedUnix, 0).Local()
  120. case "updated_unix":
  121. c.Updated = time.Unix(c.UpdatedUnix, 0).Local()
  122. }
  123. }
  124. // AfterDelete is invoked from XORM after the object is deleted.
  125. func (c *Comment) AfterDelete() {
  126. _, err := DeleteAttachmentsByComment(c.ID, true)
  127. if err != nil {
  128. log.Info("Could not delete files for comment %d on issue #%d: %s", c.ID, c.IssueID, err)
  129. }
  130. }
  131. // HTMLURL formats a URL-string to the issue-comment
  132. func (c *Comment) HTMLURL() string {
  133. issue, err := GetIssueByID(c.IssueID)
  134. if err != nil { // Silently dropping errors :unamused:
  135. log.Error(4, "GetIssueByID(%d): %v", c.IssueID, err)
  136. return ""
  137. }
  138. return fmt.Sprintf("%s#%s", issue.HTMLURL(), c.HashTag())
  139. }
  140. // IssueURL formats a URL-string to the issue
  141. func (c *Comment) IssueURL() string {
  142. issue, err := GetIssueByID(c.IssueID)
  143. if err != nil { // Silently dropping errors :unamused:
  144. log.Error(4, "GetIssueByID(%d): %v", c.IssueID, err)
  145. return ""
  146. }
  147. if issue.IsPull {
  148. return ""
  149. }
  150. return issue.HTMLURL()
  151. }
  152. // PRURL formats a URL-string to the pull-request
  153. func (c *Comment) PRURL() string {
  154. issue, err := GetIssueByID(c.IssueID)
  155. if err != nil { // Silently dropping errors :unamused:
  156. log.Error(4, "GetIssueByID(%d): %v", c.IssueID, err)
  157. return ""
  158. }
  159. if !issue.IsPull {
  160. return ""
  161. }
  162. return issue.HTMLURL()
  163. }
  164. // APIFormat converts a Comment to the api.Comment format
  165. func (c *Comment) APIFormat() *api.Comment {
  166. return &api.Comment{
  167. ID: c.ID,
  168. Poster: c.Poster.APIFormat(),
  169. HTMLURL: c.HTMLURL(),
  170. IssueURL: c.IssueURL(),
  171. PRURL: c.PRURL(),
  172. Body: c.Content,
  173. Created: c.Created,
  174. Updated: c.Updated,
  175. }
  176. }
  177. // HashTag returns unique hash tag for comment.
  178. func (c *Comment) HashTag() string {
  179. return "issuecomment-" + com.ToStr(c.ID)
  180. }
  181. // EventTag returns unique event hash tag for comment.
  182. func (c *Comment) EventTag() string {
  183. return "event-" + com.ToStr(c.ID)
  184. }
  185. // LoadLabel if comment.Type is CommentTypeLabel, then load Label
  186. func (c *Comment) LoadLabel() error {
  187. var label Label
  188. has, err := x.ID(c.LabelID).Get(&label)
  189. if err != nil {
  190. return err
  191. } else if has {
  192. c.Label = &label
  193. } else {
  194. // Ignore Label is deleted, but not clear this table
  195. log.Warn("Commit %d cannot load label %d", c.ID, c.LabelID)
  196. }
  197. return nil
  198. }
  199. // LoadMilestone if comment.Type is CommentTypeMilestone, then load milestone
  200. func (c *Comment) LoadMilestone() error {
  201. if c.OldMilestoneID > 0 {
  202. var oldMilestone Milestone
  203. has, err := x.ID(c.OldMilestoneID).Get(&oldMilestone)
  204. if err != nil {
  205. return err
  206. } else if has {
  207. c.OldMilestone = &oldMilestone
  208. }
  209. }
  210. if c.MilestoneID > 0 {
  211. var milestone Milestone
  212. has, err := x.ID(c.MilestoneID).Get(&milestone)
  213. if err != nil {
  214. return err
  215. } else if has {
  216. c.Milestone = &milestone
  217. }
  218. }
  219. return nil
  220. }
  221. // LoadAssignees if comment.Type is CommentTypeAssignees, then load assignees
  222. func (c *Comment) LoadAssignees() error {
  223. var err error
  224. if c.OldAssigneeID > 0 {
  225. c.OldAssignee, err = getUserByID(x, c.OldAssigneeID)
  226. if err != nil {
  227. return err
  228. }
  229. }
  230. if c.AssigneeID > 0 {
  231. c.Assignee, err = getUserByID(x, c.AssigneeID)
  232. if err != nil {
  233. return err
  234. }
  235. }
  236. return nil
  237. }
  238. // MailParticipants sends new comment emails to repository watchers
  239. // and mentioned people.
  240. func (c *Comment) MailParticipants(e Engine, opType ActionType, issue *Issue) (err error) {
  241. mentions := markdown.FindAllMentions(c.Content)
  242. if err = UpdateIssueMentions(e, c.IssueID, mentions); err != nil {
  243. return fmt.Errorf("UpdateIssueMentions [%d]: %v", c.IssueID, err)
  244. }
  245. switch opType {
  246. case ActionCommentIssue:
  247. issue.Content = c.Content
  248. case ActionCloseIssue:
  249. issue.Content = fmt.Sprintf("Closed #%d", issue.Index)
  250. case ActionReopenIssue:
  251. issue.Content = fmt.Sprintf("Reopened #%d", issue.Index)
  252. }
  253. if err = mailIssueCommentToParticipants(e, issue, c.Poster, c, mentions); err != nil {
  254. log.Error(4, "mailIssueCommentToParticipants: %v", err)
  255. }
  256. return nil
  257. }
  258. func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err error) {
  259. var LabelID int64
  260. if opts.Label != nil {
  261. LabelID = opts.Label.ID
  262. }
  263. comment := &Comment{
  264. Type: opts.Type,
  265. PosterID: opts.Doer.ID,
  266. Poster: opts.Doer,
  267. IssueID: opts.Issue.ID,
  268. LabelID: LabelID,
  269. OldMilestoneID: opts.OldMilestoneID,
  270. MilestoneID: opts.MilestoneID,
  271. OldAssigneeID: opts.OldAssigneeID,
  272. AssigneeID: opts.AssigneeID,
  273. CommitID: opts.CommitID,
  274. CommitSHA: opts.CommitSHA,
  275. Line: opts.LineNum,
  276. Content: opts.Content,
  277. OldTitle: opts.OldTitle,
  278. NewTitle: opts.NewTitle,
  279. }
  280. if _, err = e.Insert(comment); err != nil {
  281. return nil, err
  282. }
  283. if err = opts.Repo.getOwner(e); err != nil {
  284. return nil, err
  285. }
  286. // Compose comment action, could be plain comment, close or reopen issue/pull request.
  287. // This object will be used to notify watchers in the end of function.
  288. act := &Action{
  289. ActUserID: opts.Doer.ID,
  290. ActUser: opts.Doer,
  291. Content: fmt.Sprintf("%d|%s", opts.Issue.Index, strings.Split(opts.Content, "\n")[0]),
  292. RepoID: opts.Repo.ID,
  293. Repo: opts.Repo,
  294. Comment: comment,
  295. CommentID: comment.ID,
  296. IsPrivate: opts.Repo.IsPrivate,
  297. }
  298. // Check comment type.
  299. switch opts.Type {
  300. case CommentTypeComment:
  301. act.OpType = ActionCommentIssue
  302. if _, err = e.Exec("UPDATE `issue` SET num_comments=num_comments+1 WHERE id=?", opts.Issue.ID); err != nil {
  303. return nil, err
  304. }
  305. // Check attachments
  306. attachments := make([]*Attachment, 0, len(opts.Attachments))
  307. for _, uuid := range opts.Attachments {
  308. attach, err := getAttachmentByUUID(e, uuid)
  309. if err != nil {
  310. if IsErrAttachmentNotExist(err) {
  311. continue
  312. }
  313. return nil, fmt.Errorf("getAttachmentByUUID [%s]: %v", uuid, err)
  314. }
  315. attachments = append(attachments, attach)
  316. }
  317. for i := range attachments {
  318. attachments[i].IssueID = opts.Issue.ID
  319. attachments[i].CommentID = comment.ID
  320. // No assign value could be 0, so ignore AllCols().
  321. if _, err = e.Id(attachments[i].ID).Update(attachments[i]); err != nil {
  322. return nil, fmt.Errorf("update attachment [%d]: %v", attachments[i].ID, err)
  323. }
  324. }
  325. case CommentTypeReopen:
  326. act.OpType = ActionReopenIssue
  327. if opts.Issue.IsPull {
  328. act.OpType = ActionReopenPullRequest
  329. }
  330. if opts.Issue.IsPull {
  331. _, err = e.Exec("UPDATE `repository` SET num_closed_pulls=num_closed_pulls-1 WHERE id=?", opts.Repo.ID)
  332. } else {
  333. _, err = e.Exec("UPDATE `repository` SET num_closed_issues=num_closed_issues-1 WHERE id=?", opts.Repo.ID)
  334. }
  335. if err != nil {
  336. return nil, err
  337. }
  338. case CommentTypeClose:
  339. act.OpType = ActionCloseIssue
  340. if opts.Issue.IsPull {
  341. act.OpType = ActionClosePullRequest
  342. }
  343. if opts.Issue.IsPull {
  344. _, err = e.Exec("UPDATE `repository` SET num_closed_pulls=num_closed_pulls+1 WHERE id=?", opts.Repo.ID)
  345. } else {
  346. _, err = e.Exec("UPDATE `repository` SET num_closed_issues=num_closed_issues+1 WHERE id=?", opts.Repo.ID)
  347. }
  348. if err != nil {
  349. return nil, err
  350. }
  351. }
  352. // update the issue's updated_unix column
  353. if err = updateIssueCols(e, opts.Issue); err != nil {
  354. return nil, err
  355. }
  356. // Notify watchers for whatever action comes in, ignore if no action type.
  357. if act.OpType > 0 {
  358. if err = notifyWatchers(e, act); err != nil {
  359. log.Error(4, "notifyWatchers: %v", err)
  360. }
  361. if err = comment.MailParticipants(e, act.OpType, opts.Issue); err != nil {
  362. log.Error(4, "MailParticipants: %v", err)
  363. }
  364. }
  365. return comment, nil
  366. }
  367. func createStatusComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue) (*Comment, error) {
  368. cmtType := CommentTypeClose
  369. if !issue.IsClosed {
  370. cmtType = CommentTypeReopen
  371. }
  372. return createComment(e, &CreateCommentOptions{
  373. Type: cmtType,
  374. Doer: doer,
  375. Repo: repo,
  376. Issue: issue,
  377. })
  378. }
  379. func createLabelComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue, label *Label, add bool) (*Comment, error) {
  380. var content string
  381. if add {
  382. content = "1"
  383. }
  384. return createComment(e, &CreateCommentOptions{
  385. Type: CommentTypeLabel,
  386. Doer: doer,
  387. Repo: repo,
  388. Issue: issue,
  389. Label: label,
  390. Content: content,
  391. })
  392. }
  393. func createMilestoneComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue, oldMilestoneID, milestoneID int64) (*Comment, error) {
  394. return createComment(e, &CreateCommentOptions{
  395. Type: CommentTypeMilestone,
  396. Doer: doer,
  397. Repo: repo,
  398. Issue: issue,
  399. OldMilestoneID: oldMilestoneID,
  400. MilestoneID: milestoneID,
  401. })
  402. }
  403. func createAssigneeComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue, oldAssigneeID, assigneeID int64) (*Comment, error) {
  404. return createComment(e, &CreateCommentOptions{
  405. Type: CommentTypeAssignees,
  406. Doer: doer,
  407. Repo: repo,
  408. Issue: issue,
  409. OldAssigneeID: oldAssigneeID,
  410. AssigneeID: assigneeID,
  411. })
  412. }
  413. func createChangeTitleComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue, oldTitle, newTitle string) (*Comment, error) {
  414. return createComment(e, &CreateCommentOptions{
  415. Type: CommentTypeChangeTitle,
  416. Doer: doer,
  417. Repo: repo,
  418. Issue: issue,
  419. OldTitle: oldTitle,
  420. NewTitle: newTitle,
  421. })
  422. }
  423. func createDeleteBranchComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue, branchName string) (*Comment, error) {
  424. return createComment(e, &CreateCommentOptions{
  425. Type: CommentTypeDeleteBranch,
  426. Doer: doer,
  427. Repo: repo,
  428. Issue: issue,
  429. CommitSHA: branchName,
  430. })
  431. }
  432. // CreateCommentOptions defines options for creating comment
  433. type CreateCommentOptions struct {
  434. Type CommentType
  435. Doer *User
  436. Repo *Repository
  437. Issue *Issue
  438. Label *Label
  439. OldMilestoneID int64
  440. MilestoneID int64
  441. OldAssigneeID int64
  442. AssigneeID int64
  443. OldTitle string
  444. NewTitle string
  445. CommitID int64
  446. CommitSHA string
  447. LineNum int64
  448. Content string
  449. Attachments []string // UUIDs of attachments
  450. }
  451. // CreateComment creates comment of issue or commit.
  452. func CreateComment(opts *CreateCommentOptions) (comment *Comment, err error) {
  453. sess := x.NewSession()
  454. defer sess.Close()
  455. if err = sess.Begin(); err != nil {
  456. return nil, err
  457. }
  458. comment, err = createComment(sess, opts)
  459. if err != nil {
  460. return nil, err
  461. }
  462. return comment, sess.Commit()
  463. }
  464. // CreateIssueComment creates a plain issue comment.
  465. func CreateIssueComment(doer *User, repo *Repository, issue *Issue, content string, attachments []string) (*Comment, error) {
  466. return CreateComment(&CreateCommentOptions{
  467. Type: CommentTypeComment,
  468. Doer: doer,
  469. Repo: repo,
  470. Issue: issue,
  471. Content: content,
  472. Attachments: attachments,
  473. })
  474. }
  475. // CreateRefComment creates a commit reference comment to issue.
  476. func CreateRefComment(doer *User, repo *Repository, issue *Issue, content, commitSHA string) error {
  477. if len(commitSHA) == 0 {
  478. return fmt.Errorf("cannot create reference with empty commit SHA")
  479. }
  480. // Check if same reference from same commit has already existed.
  481. has, err := x.Get(&Comment{
  482. Type: CommentTypeCommitRef,
  483. IssueID: issue.ID,
  484. CommitSHA: commitSHA,
  485. })
  486. if err != nil {
  487. return fmt.Errorf("check reference comment: %v", err)
  488. } else if has {
  489. return nil
  490. }
  491. _, err = CreateComment(&CreateCommentOptions{
  492. Type: CommentTypeCommitRef,
  493. Doer: doer,
  494. Repo: repo,
  495. Issue: issue,
  496. CommitSHA: commitSHA,
  497. Content: content,
  498. })
  499. return err
  500. }
  501. // GetCommentByID returns the comment by given ID.
  502. func GetCommentByID(id int64) (*Comment, error) {
  503. c := new(Comment)
  504. has, err := x.Id(id).Get(c)
  505. if err != nil {
  506. return nil, err
  507. } else if !has {
  508. return nil, ErrCommentNotExist{id, 0}
  509. }
  510. return c, nil
  511. }
  512. // FindCommentsOptions describes the conditions to Find comments
  513. type FindCommentsOptions struct {
  514. RepoID int64
  515. IssueID int64
  516. Since int64
  517. Type CommentType
  518. }
  519. func (opts *FindCommentsOptions) toConds() builder.Cond {
  520. var cond = builder.NewCond()
  521. if opts.RepoID > 0 {
  522. cond = cond.And(builder.Eq{"issue.repo_id": opts.RepoID})
  523. }
  524. if opts.IssueID > 0 {
  525. cond = cond.And(builder.Eq{"comment.issue_id": opts.IssueID})
  526. }
  527. if opts.Since > 0 {
  528. cond = cond.And(builder.Gte{"comment.updated_unix": opts.Since})
  529. }
  530. if opts.Type != CommentTypeUnknown {
  531. cond = cond.And(builder.Eq{"comment.type": opts.Type})
  532. }
  533. return cond
  534. }
  535. func findComments(e Engine, opts FindCommentsOptions) ([]*Comment, error) {
  536. comments := make([]*Comment, 0, 10)
  537. sess := e.Where(opts.toConds())
  538. if opts.RepoID > 0 {
  539. sess.Join("INNER", "issue", "issue.id = comment.issue_id")
  540. }
  541. return comments, sess.
  542. Asc("comment.created_unix").
  543. Find(&comments)
  544. }
  545. // FindComments returns all comments according options
  546. func FindComments(opts FindCommentsOptions) ([]*Comment, error) {
  547. return findComments(x, opts)
  548. }
  549. // GetCommentsByIssueID returns all comments of an issue.
  550. func GetCommentsByIssueID(issueID int64) ([]*Comment, error) {
  551. return findComments(x, FindCommentsOptions{
  552. IssueID: issueID,
  553. Type: CommentTypeUnknown,
  554. })
  555. }
  556. // GetCommentsByIssueIDSince returns a list of comments of an issue since a given time point.
  557. func GetCommentsByIssueIDSince(issueID, since int64) ([]*Comment, error) {
  558. return findComments(x, FindCommentsOptions{
  559. IssueID: issueID,
  560. Type: CommentTypeUnknown,
  561. Since: since,
  562. })
  563. }
  564. // GetCommentsByRepoIDSince returns a list of comments for all issues in a repo since a given time point.
  565. func GetCommentsByRepoIDSince(repoID, since int64) ([]*Comment, error) {
  566. return findComments(x, FindCommentsOptions{
  567. RepoID: repoID,
  568. Type: CommentTypeUnknown,
  569. Since: since,
  570. })
  571. }
  572. // UpdateComment updates information of comment.
  573. func UpdateComment(c *Comment) error {
  574. _, err := x.Id(c.ID).AllCols().Update(c)
  575. return err
  576. }
  577. // DeleteComment deletes the comment
  578. func DeleteComment(comment *Comment) error {
  579. sess := x.NewSession()
  580. defer sess.Close()
  581. if err := sess.Begin(); err != nil {
  582. return err
  583. }
  584. if _, err := sess.Delete(&Comment{
  585. ID: comment.ID,
  586. }); err != nil {
  587. return err
  588. }
  589. if comment.Type == CommentTypeComment {
  590. if _, err := sess.Exec("UPDATE `issue` SET num_comments = num_comments - 1 WHERE id = ?", comment.IssueID); err != nil {
  591. return err
  592. }
  593. }
  594. if _, err := sess.Where("comment_id = ?", comment.ID).Cols("is_deleted").Update(&Action{IsDeleted: true}); err != nil {
  595. return err
  596. }
  597. return sess.Commit()
  598. }