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