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_comment.go 17 kB

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