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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961
  1. // Copyright 2018 The Gitea Authors.
  2. // Copyright 2016 The Gogs Authors.
  3. // All rights reserved.
  4. // Use of this source code is governed by a MIT-style
  5. // license that can be found in the LICENSE file.
  6. package models
  7. import (
  8. "fmt"
  9. "strings"
  10. "code.gitea.io/gitea/modules/git"
  11. "code.gitea.io/gitea/modules/log"
  12. "code.gitea.io/gitea/modules/markup/markdown"
  13. "code.gitea.io/gitea/modules/references"
  14. "code.gitea.io/gitea/modules/structs"
  15. api "code.gitea.io/gitea/modules/structs"
  16. "code.gitea.io/gitea/modules/timeutil"
  17. "github.com/unknwon/com"
  18. "xorm.io/builder"
  19. "xorm.io/xorm"
  20. )
  21. // CommentType defines whether a comment is just a simple comment, an action (like close) or a reference.
  22. type CommentType int
  23. // define unknown comment type
  24. const (
  25. CommentTypeUnknown CommentType = -1
  26. )
  27. // Enumerate all the comment types
  28. const (
  29. // Plain comment, can be associated with a commit (CommitID > 0) and a line (LineNum > 0)
  30. CommentTypeComment CommentType = iota
  31. CommentTypeReopen
  32. CommentTypeClose
  33. // References.
  34. CommentTypeIssueRef
  35. // Reference from a commit (not part of a pull request)
  36. CommentTypeCommitRef
  37. // Reference from a comment
  38. CommentTypeCommentRef
  39. // Reference from a pull request
  40. CommentTypePullRef
  41. // Labels changed
  42. CommentTypeLabel
  43. // Milestone changed
  44. CommentTypeMilestone
  45. // Assignees changed
  46. CommentTypeAssignees
  47. // Change Title
  48. CommentTypeChangeTitle
  49. // Delete Branch
  50. CommentTypeDeleteBranch
  51. // Start a stopwatch for time tracking
  52. CommentTypeStartTracking
  53. // Stop a stopwatch for time tracking
  54. CommentTypeStopTracking
  55. // Add time manual for time tracking
  56. CommentTypeAddTimeManual
  57. // Cancel a stopwatch for time tracking
  58. CommentTypeCancelTracking
  59. // Added a due date
  60. CommentTypeAddedDeadline
  61. // Modified the due date
  62. CommentTypeModifiedDeadline
  63. // Removed a due date
  64. CommentTypeRemovedDeadline
  65. // Dependency added
  66. CommentTypeAddDependency
  67. //Dependency removed
  68. CommentTypeRemoveDependency
  69. // Comment a line of code
  70. CommentTypeCode
  71. // Reviews a pull request by giving general feedback
  72. CommentTypeReview
  73. // Lock an issue, giving only collaborators access
  74. CommentTypeLock
  75. // Unlocks a previously locked issue
  76. CommentTypeUnlock
  77. // Change pull request's target branch
  78. CommentTypeChangeTargetBranch
  79. // Delete time manual for time tracking
  80. CommentTypeDeleteTimeManual
  81. )
  82. // CommentTag defines comment tag type
  83. type CommentTag int
  84. // Enumerate all the comment tag types
  85. const (
  86. CommentTagNone CommentTag = iota
  87. CommentTagPoster
  88. CommentTagWriter
  89. CommentTagOwner
  90. )
  91. // Comment represents a comment in commit and issue page.
  92. type Comment struct {
  93. ID int64 `xorm:"pk autoincr"`
  94. Type CommentType `xorm:"INDEX"`
  95. PosterID int64 `xorm:"INDEX"`
  96. Poster *User `xorm:"-"`
  97. OriginalAuthor string
  98. OriginalAuthorID int64
  99. IssueID int64 `xorm:"INDEX"`
  100. Issue *Issue `xorm:"-"`
  101. LabelID int64
  102. Label *Label `xorm:"-"`
  103. OldMilestoneID int64
  104. MilestoneID int64
  105. OldMilestone *Milestone `xorm:"-"`
  106. Milestone *Milestone `xorm:"-"`
  107. AssigneeID int64
  108. RemovedAssignee bool
  109. Assignee *User `xorm:"-"`
  110. OldTitle string
  111. NewTitle string
  112. OldRef string
  113. NewRef string
  114. DependentIssueID int64
  115. DependentIssue *Issue `xorm:"-"`
  116. CommitID int64
  117. Line int64 // - previous line / + proposed line
  118. TreePath string
  119. Content string `xorm:"TEXT"`
  120. RenderedContent string `xorm:"-"`
  121. // Path represents the 4 lines of code cemented by this comment
  122. Patch string `xorm:"TEXT"`
  123. CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
  124. UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
  125. // Reference issue in commit message
  126. CommitSHA string `xorm:"VARCHAR(40)"`
  127. Attachments []*Attachment `xorm:"-"`
  128. Reactions ReactionList `xorm:"-"`
  129. // For view issue page.
  130. ShowTag CommentTag `xorm:"-"`
  131. Review *Review `xorm:"-"`
  132. ReviewID int64 `xorm:"index"`
  133. Invalidated bool
  134. // Reference an issue or pull from another comment, issue or PR
  135. // All information is about the origin of the reference
  136. RefRepoID int64 `xorm:"index"` // Repo where the referencing
  137. RefIssueID int64 `xorm:"index"`
  138. RefCommentID int64 `xorm:"index"` // 0 if origin is Issue title or content (or PR's)
  139. RefAction references.XRefAction `xorm:"SMALLINT"` // What hapens if RefIssueID resolves
  140. RefIsPull bool
  141. RefRepo *Repository `xorm:"-"`
  142. RefIssue *Issue `xorm:"-"`
  143. RefComment *Comment `xorm:"-"`
  144. }
  145. // LoadIssue loads issue from database
  146. func (c *Comment) LoadIssue() (err error) {
  147. return c.loadIssue(x)
  148. }
  149. func (c *Comment) loadIssue(e Engine) (err error) {
  150. if c.Issue != nil {
  151. return nil
  152. }
  153. c.Issue, err = getIssueByID(e, c.IssueID)
  154. return
  155. }
  156. func (c *Comment) loadPoster(e Engine) (err error) {
  157. if c.PosterID <= 0 || c.Poster != nil {
  158. return nil
  159. }
  160. c.Poster, err = getUserByID(e, c.PosterID)
  161. if err != nil {
  162. if IsErrUserNotExist(err) {
  163. c.PosterID = -1
  164. c.Poster = NewGhostUser()
  165. } else {
  166. log.Error("getUserByID[%d]: %v", c.ID, err)
  167. }
  168. }
  169. return err
  170. }
  171. // AfterDelete is invoked from XORM after the object is deleted.
  172. func (c *Comment) AfterDelete() {
  173. if c.ID <= 0 {
  174. return
  175. }
  176. _, err := DeleteAttachmentsByComment(c.ID, true)
  177. if err != nil {
  178. log.Info("Could not delete files for comment %d on issue #%d: %s", c.ID, c.IssueID, err)
  179. }
  180. }
  181. // HTMLURL formats a URL-string to the issue-comment
  182. func (c *Comment) HTMLURL() string {
  183. err := c.LoadIssue()
  184. if err != nil { // Silently dropping errors :unamused:
  185. log.Error("LoadIssue(%d): %v", c.IssueID, err)
  186. return ""
  187. }
  188. err = c.Issue.loadRepo(x)
  189. if err != nil { // Silently dropping errors :unamused:
  190. log.Error("loadRepo(%d): %v", c.Issue.RepoID, err)
  191. return ""
  192. }
  193. if c.Type == CommentTypeCode {
  194. if c.ReviewID == 0 {
  195. return fmt.Sprintf("%s/files#%s", c.Issue.HTMLURL(), c.HashTag())
  196. }
  197. if c.Review == nil {
  198. if err := c.LoadReview(); err != nil {
  199. log.Warn("LoadReview(%d): %v", c.ReviewID, err)
  200. return fmt.Sprintf("%s/files#%s", c.Issue.HTMLURL(), c.HashTag())
  201. }
  202. }
  203. if c.Review.Type <= ReviewTypePending {
  204. return fmt.Sprintf("%s/files#%s", c.Issue.HTMLURL(), c.HashTag())
  205. }
  206. }
  207. return fmt.Sprintf("%s#%s", c.Issue.HTMLURL(), c.HashTag())
  208. }
  209. // IssueURL formats a URL-string to the issue
  210. func (c *Comment) IssueURL() string {
  211. err := c.LoadIssue()
  212. if err != nil { // Silently dropping errors :unamused:
  213. log.Error("LoadIssue(%d): %v", c.IssueID, err)
  214. return ""
  215. }
  216. if c.Issue.IsPull {
  217. return ""
  218. }
  219. err = c.Issue.loadRepo(x)
  220. if err != nil { // Silently dropping errors :unamused:
  221. log.Error("loadRepo(%d): %v", c.Issue.RepoID, err)
  222. return ""
  223. }
  224. return c.Issue.HTMLURL()
  225. }
  226. // PRURL formats a URL-string to the pull-request
  227. func (c *Comment) PRURL() string {
  228. err := c.LoadIssue()
  229. if err != nil { // Silently dropping errors :unamused:
  230. log.Error("LoadIssue(%d): %v", c.IssueID, err)
  231. return ""
  232. }
  233. err = c.Issue.loadRepo(x)
  234. if err != nil { // Silently dropping errors :unamused:
  235. log.Error("loadRepo(%d): %v", c.Issue.RepoID, err)
  236. return ""
  237. }
  238. if !c.Issue.IsPull {
  239. return ""
  240. }
  241. return c.Issue.HTMLURL()
  242. }
  243. // APIFormat converts a Comment to the api.Comment format
  244. func (c *Comment) APIFormat() *api.Comment {
  245. return &api.Comment{
  246. ID: c.ID,
  247. Poster: c.Poster.APIFormat(),
  248. HTMLURL: c.HTMLURL(),
  249. IssueURL: c.IssueURL(),
  250. PRURL: c.PRURL(),
  251. Body: c.Content,
  252. Created: c.CreatedUnix.AsTime(),
  253. Updated: c.UpdatedUnix.AsTime(),
  254. }
  255. }
  256. // CommentHashTag returns unique hash tag for comment id.
  257. func CommentHashTag(id int64) string {
  258. return fmt.Sprintf("issuecomment-%d", id)
  259. }
  260. // HashTag returns unique hash tag for comment.
  261. func (c *Comment) HashTag() string {
  262. return CommentHashTag(c.ID)
  263. }
  264. // EventTag returns unique event hash tag for comment.
  265. func (c *Comment) EventTag() string {
  266. return "event-" + com.ToStr(c.ID)
  267. }
  268. // LoadLabel if comment.Type is CommentTypeLabel, then load Label
  269. func (c *Comment) LoadLabel() error {
  270. var label Label
  271. has, err := x.ID(c.LabelID).Get(&label)
  272. if err != nil {
  273. return err
  274. } else if has {
  275. c.Label = &label
  276. } else {
  277. // Ignore Label is deleted, but not clear this table
  278. log.Warn("Commit %d cannot load label %d", c.ID, c.LabelID)
  279. }
  280. return nil
  281. }
  282. // LoadMilestone if comment.Type is CommentTypeMilestone, then load milestone
  283. func (c *Comment) LoadMilestone() error {
  284. if c.OldMilestoneID > 0 {
  285. var oldMilestone Milestone
  286. has, err := x.ID(c.OldMilestoneID).Get(&oldMilestone)
  287. if err != nil {
  288. return err
  289. } else if has {
  290. c.OldMilestone = &oldMilestone
  291. }
  292. }
  293. if c.MilestoneID > 0 {
  294. var milestone Milestone
  295. has, err := x.ID(c.MilestoneID).Get(&milestone)
  296. if err != nil {
  297. return err
  298. } else if has {
  299. c.Milestone = &milestone
  300. }
  301. }
  302. return nil
  303. }
  304. // LoadPoster loads comment poster
  305. func (c *Comment) LoadPoster() error {
  306. return c.loadPoster(x)
  307. }
  308. // LoadAttachments loads attachments
  309. func (c *Comment) LoadAttachments() error {
  310. if len(c.Attachments) > 0 {
  311. return nil
  312. }
  313. var err error
  314. c.Attachments, err = getAttachmentsByCommentID(x, c.ID)
  315. if err != nil {
  316. log.Error("getAttachmentsByCommentID[%d]: %v", c.ID, err)
  317. }
  318. return nil
  319. }
  320. // UpdateAttachments update attachments by UUIDs for the comment
  321. func (c *Comment) UpdateAttachments(uuids []string) error {
  322. sess := x.NewSession()
  323. defer sess.Close()
  324. if err := sess.Begin(); err != nil {
  325. return err
  326. }
  327. attachments, err := getAttachmentsByUUIDs(sess, uuids)
  328. if err != nil {
  329. return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %v", uuids, err)
  330. }
  331. for i := 0; i < len(attachments); i++ {
  332. attachments[i].IssueID = c.IssueID
  333. attachments[i].CommentID = c.ID
  334. if err := updateAttachment(sess, attachments[i]); err != nil {
  335. return fmt.Errorf("update attachment [id: %d]: %v", attachments[i].ID, err)
  336. }
  337. }
  338. return sess.Commit()
  339. }
  340. // LoadAssigneeUser if comment.Type is CommentTypeAssignees, then load assignees
  341. func (c *Comment) LoadAssigneeUser() error {
  342. var err error
  343. if c.AssigneeID > 0 {
  344. c.Assignee, err = getUserByID(x, c.AssigneeID)
  345. if err != nil {
  346. if !IsErrUserNotExist(err) {
  347. return err
  348. }
  349. c.Assignee = NewGhostUser()
  350. }
  351. }
  352. return nil
  353. }
  354. // LoadDepIssueDetails loads Dependent Issue Details
  355. func (c *Comment) LoadDepIssueDetails() (err error) {
  356. if c.DependentIssueID <= 0 || c.DependentIssue != nil {
  357. return nil
  358. }
  359. c.DependentIssue, err = getIssueByID(x, c.DependentIssueID)
  360. return err
  361. }
  362. func (c *Comment) loadReactions(e Engine) (err error) {
  363. if c.Reactions != nil {
  364. return nil
  365. }
  366. c.Reactions, err = findReactions(e, FindReactionsOptions{
  367. IssueID: c.IssueID,
  368. CommentID: c.ID,
  369. })
  370. if err != nil {
  371. return err
  372. }
  373. // Load reaction user data
  374. if _, err := c.Reactions.LoadUsers(); err != nil {
  375. return err
  376. }
  377. return nil
  378. }
  379. // LoadReactions loads comment reactions
  380. func (c *Comment) LoadReactions() error {
  381. return c.loadReactions(x)
  382. }
  383. func (c *Comment) loadReview(e Engine) (err error) {
  384. if c.Review == nil {
  385. if c.Review, err = getReviewByID(e, c.ReviewID); err != nil {
  386. return err
  387. }
  388. }
  389. c.Review.Issue = c.Issue
  390. return nil
  391. }
  392. // LoadReview loads the associated review
  393. func (c *Comment) LoadReview() error {
  394. return c.loadReview(x)
  395. }
  396. func (c *Comment) checkInvalidation(doer *User, repo *git.Repository, branch string) error {
  397. // FIXME differentiate between previous and proposed line
  398. commit, err := repo.LineBlame(branch, repo.Path, c.TreePath, uint(c.UnsignedLine()))
  399. if err != nil && strings.Contains(err.Error(), "fatal: no such path") {
  400. c.Invalidated = true
  401. return UpdateComment(c, doer)
  402. }
  403. if err != nil {
  404. return err
  405. }
  406. if c.CommitSHA != "" && c.CommitSHA != commit.ID.String() {
  407. c.Invalidated = true
  408. return UpdateComment(c, doer)
  409. }
  410. return nil
  411. }
  412. // CheckInvalidation checks if the line of code comment got changed by another commit.
  413. // If the line got changed the comment is going to be invalidated.
  414. func (c *Comment) CheckInvalidation(repo *git.Repository, doer *User, branch string) error {
  415. return c.checkInvalidation(doer, repo, branch)
  416. }
  417. // DiffSide returns "previous" if Comment.Line is a LOC of the previous changes and "proposed" if it is a LOC of the proposed changes.
  418. func (c *Comment) DiffSide() string {
  419. if c.Line < 0 {
  420. return "previous"
  421. }
  422. return "proposed"
  423. }
  424. // UnsignedLine returns the LOC of the code comment without + or -
  425. func (c *Comment) UnsignedLine() uint64 {
  426. if c.Line < 0 {
  427. return uint64(c.Line * -1)
  428. }
  429. return uint64(c.Line)
  430. }
  431. // CodeCommentURL returns the url to a comment in code
  432. func (c *Comment) CodeCommentURL() string {
  433. err := c.LoadIssue()
  434. if err != nil { // Silently dropping errors :unamused:
  435. log.Error("LoadIssue(%d): %v", c.IssueID, err)
  436. return ""
  437. }
  438. err = c.Issue.loadRepo(x)
  439. if err != nil { // Silently dropping errors :unamused:
  440. log.Error("loadRepo(%d): %v", c.Issue.RepoID, err)
  441. return ""
  442. }
  443. return fmt.Sprintf("%s/files#%s", c.Issue.HTMLURL(), c.HashTag())
  444. }
  445. func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err error) {
  446. var LabelID int64
  447. if opts.Label != nil {
  448. LabelID = opts.Label.ID
  449. }
  450. comment := &Comment{
  451. Type: opts.Type,
  452. PosterID: opts.Doer.ID,
  453. Poster: opts.Doer,
  454. IssueID: opts.Issue.ID,
  455. LabelID: LabelID,
  456. OldMilestoneID: opts.OldMilestoneID,
  457. MilestoneID: opts.MilestoneID,
  458. RemovedAssignee: opts.RemovedAssignee,
  459. AssigneeID: opts.AssigneeID,
  460. CommitID: opts.CommitID,
  461. CommitSHA: opts.CommitSHA,
  462. Line: opts.LineNum,
  463. Content: opts.Content,
  464. OldTitle: opts.OldTitle,
  465. NewTitle: opts.NewTitle,
  466. OldRef: opts.OldRef,
  467. NewRef: opts.NewRef,
  468. DependentIssueID: opts.DependentIssueID,
  469. TreePath: opts.TreePath,
  470. ReviewID: opts.ReviewID,
  471. Patch: opts.Patch,
  472. RefRepoID: opts.RefRepoID,
  473. RefIssueID: opts.RefIssueID,
  474. RefCommentID: opts.RefCommentID,
  475. RefAction: opts.RefAction,
  476. RefIsPull: opts.RefIsPull,
  477. }
  478. if _, err = e.Insert(comment); err != nil {
  479. return nil, err
  480. }
  481. if err = opts.Repo.getOwner(e); err != nil {
  482. return nil, err
  483. }
  484. if err = updateCommentInfos(e, opts, comment); err != nil {
  485. return nil, err
  486. }
  487. if err = comment.addCrossReferences(e, opts.Doer, false); err != nil {
  488. return nil, err
  489. }
  490. return comment, nil
  491. }
  492. func updateCommentInfos(e *xorm.Session, opts *CreateCommentOptions, comment *Comment) (err error) {
  493. // Check comment type.
  494. switch opts.Type {
  495. case CommentTypeCode:
  496. if comment.ReviewID != 0 {
  497. if comment.Review == nil {
  498. if err := comment.loadReview(e); err != nil {
  499. return err
  500. }
  501. }
  502. if comment.Review.Type <= ReviewTypePending {
  503. return nil
  504. }
  505. }
  506. fallthrough
  507. case CommentTypeComment:
  508. if _, err = e.Exec("UPDATE `issue` SET num_comments=num_comments+1 WHERE id=?", opts.Issue.ID); err != nil {
  509. return err
  510. }
  511. // Check attachments
  512. attachments, err := getAttachmentsByUUIDs(e, opts.Attachments)
  513. if err != nil {
  514. return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %v", opts.Attachments, err)
  515. }
  516. for i := range attachments {
  517. attachments[i].IssueID = opts.Issue.ID
  518. attachments[i].CommentID = comment.ID
  519. // No assign value could be 0, so ignore AllCols().
  520. if _, err = e.ID(attachments[i].ID).Update(attachments[i]); err != nil {
  521. return fmt.Errorf("update attachment [%d]: %v", attachments[i].ID, err)
  522. }
  523. }
  524. case CommentTypeReopen, CommentTypeClose:
  525. if err = opts.Issue.updateClosedNum(e); err != nil {
  526. return err
  527. }
  528. }
  529. // update the issue's updated_unix column
  530. return updateIssueCols(e, opts.Issue, "updated_unix")
  531. }
  532. func createDeadlineComment(e *xorm.Session, doer *User, issue *Issue, newDeadlineUnix timeutil.TimeStamp) (*Comment, error) {
  533. var content string
  534. var commentType CommentType
  535. // newDeadline = 0 means deleting
  536. if newDeadlineUnix == 0 {
  537. commentType = CommentTypeRemovedDeadline
  538. content = issue.DeadlineUnix.Format("2006-01-02")
  539. } else if issue.DeadlineUnix == 0 {
  540. // Check if the new date was added or modified
  541. // If the actual deadline is 0 => deadline added
  542. commentType = CommentTypeAddedDeadline
  543. content = newDeadlineUnix.Format("2006-01-02")
  544. } else { // Otherwise modified
  545. commentType = CommentTypeModifiedDeadline
  546. content = newDeadlineUnix.Format("2006-01-02") + "|" + issue.DeadlineUnix.Format("2006-01-02")
  547. }
  548. if err := issue.loadRepo(e); err != nil {
  549. return nil, err
  550. }
  551. var opts = &CreateCommentOptions{
  552. Type: commentType,
  553. Doer: doer,
  554. Repo: issue.Repo,
  555. Issue: issue,
  556. Content: content,
  557. }
  558. comment, err := createComment(e, opts)
  559. if err != nil {
  560. return nil, err
  561. }
  562. return comment, nil
  563. }
  564. // Creates issue dependency comment
  565. func createIssueDependencyComment(e *xorm.Session, doer *User, issue *Issue, dependentIssue *Issue, add bool) (err error) {
  566. cType := CommentTypeAddDependency
  567. if !add {
  568. cType = CommentTypeRemoveDependency
  569. }
  570. if err = issue.loadRepo(e); err != nil {
  571. return
  572. }
  573. // Make two comments, one in each issue
  574. var opts = &CreateCommentOptions{
  575. Type: cType,
  576. Doer: doer,
  577. Repo: issue.Repo,
  578. Issue: issue,
  579. DependentIssueID: dependentIssue.ID,
  580. }
  581. if _, err = createComment(e, opts); err != nil {
  582. return
  583. }
  584. opts = &CreateCommentOptions{
  585. Type: cType,
  586. Doer: doer,
  587. Repo: issue.Repo,
  588. Issue: dependentIssue,
  589. DependentIssueID: issue.ID,
  590. }
  591. _, err = createComment(e, opts)
  592. return
  593. }
  594. // CreateCommentOptions defines options for creating comment
  595. type CreateCommentOptions struct {
  596. Type CommentType
  597. Doer *User
  598. Repo *Repository
  599. Issue *Issue
  600. Label *Label
  601. DependentIssueID int64
  602. OldMilestoneID int64
  603. MilestoneID int64
  604. AssigneeID int64
  605. RemovedAssignee bool
  606. OldTitle string
  607. NewTitle string
  608. OldRef string
  609. NewRef string
  610. CommitID int64
  611. CommitSHA string
  612. Patch string
  613. LineNum int64
  614. TreePath string
  615. ReviewID int64
  616. Content string
  617. Attachments []string // UUIDs of attachments
  618. RefRepoID int64
  619. RefIssueID int64
  620. RefCommentID int64
  621. RefAction references.XRefAction
  622. RefIsPull bool
  623. }
  624. // CreateComment creates comment of issue or commit.
  625. func CreateComment(opts *CreateCommentOptions) (comment *Comment, err error) {
  626. sess := x.NewSession()
  627. defer sess.Close()
  628. if err = sess.Begin(); err != nil {
  629. return nil, err
  630. }
  631. comment, err = createComment(sess, opts)
  632. if err != nil {
  633. return nil, err
  634. }
  635. if err = sess.Commit(); err != nil {
  636. return nil, err
  637. }
  638. return comment, nil
  639. }
  640. // CreateRefComment creates a commit reference comment to issue.
  641. func CreateRefComment(doer *User, repo *Repository, issue *Issue, content, commitSHA string) error {
  642. if len(commitSHA) == 0 {
  643. return fmt.Errorf("cannot create reference with empty commit SHA")
  644. }
  645. // Check if same reference from same commit has already existed.
  646. has, err := x.Get(&Comment{
  647. Type: CommentTypeCommitRef,
  648. IssueID: issue.ID,
  649. CommitSHA: commitSHA,
  650. })
  651. if err != nil {
  652. return fmt.Errorf("check reference comment: %v", err)
  653. } else if has {
  654. return nil
  655. }
  656. _, err = CreateComment(&CreateCommentOptions{
  657. Type: CommentTypeCommitRef,
  658. Doer: doer,
  659. Repo: repo,
  660. Issue: issue,
  661. CommitSHA: commitSHA,
  662. Content: content,
  663. })
  664. return err
  665. }
  666. // GetCommentByID returns the comment by given ID.
  667. func GetCommentByID(id int64) (*Comment, error) {
  668. return getCommentByID(x, id)
  669. }
  670. func getCommentByID(e Engine, id int64) (*Comment, error) {
  671. c := new(Comment)
  672. has, err := e.ID(id).Get(c)
  673. if err != nil {
  674. return nil, err
  675. } else if !has {
  676. return nil, ErrCommentNotExist{id, 0}
  677. }
  678. return c, nil
  679. }
  680. // FindCommentsOptions describes the conditions to Find comments
  681. type FindCommentsOptions struct {
  682. RepoID int64
  683. IssueID int64
  684. ReviewID int64
  685. Since int64
  686. Type CommentType
  687. }
  688. func (opts *FindCommentsOptions) toConds() builder.Cond {
  689. var cond = builder.NewCond()
  690. if opts.RepoID > 0 {
  691. cond = cond.And(builder.Eq{"issue.repo_id": opts.RepoID})
  692. }
  693. if opts.IssueID > 0 {
  694. cond = cond.And(builder.Eq{"comment.issue_id": opts.IssueID})
  695. }
  696. if opts.ReviewID > 0 {
  697. cond = cond.And(builder.Eq{"comment.review_id": opts.ReviewID})
  698. }
  699. if opts.Since > 0 {
  700. cond = cond.And(builder.Gte{"comment.updated_unix": opts.Since})
  701. }
  702. if opts.Type != CommentTypeUnknown {
  703. cond = cond.And(builder.Eq{"comment.type": opts.Type})
  704. }
  705. return cond
  706. }
  707. func findComments(e Engine, opts FindCommentsOptions) ([]*Comment, error) {
  708. comments := make([]*Comment, 0, 10)
  709. sess := e.Where(opts.toConds())
  710. if opts.RepoID > 0 {
  711. sess.Join("INNER", "issue", "issue.id = comment.issue_id")
  712. }
  713. return comments, sess.
  714. Asc("comment.created_unix").
  715. Asc("comment.id").
  716. Find(&comments)
  717. }
  718. // FindComments returns all comments according options
  719. func FindComments(opts FindCommentsOptions) ([]*Comment, error) {
  720. return findComments(x, opts)
  721. }
  722. // UpdateComment updates information of comment.
  723. func UpdateComment(c *Comment, doer *User) error {
  724. sess := x.NewSession()
  725. defer sess.Close()
  726. if err := sess.Begin(); err != nil {
  727. return err
  728. }
  729. if _, err := sess.ID(c.ID).AllCols().Update(c); err != nil {
  730. return err
  731. }
  732. if err := c.loadIssue(sess); err != nil {
  733. return err
  734. }
  735. if err := c.addCrossReferences(sess, doer, true); err != nil {
  736. return err
  737. }
  738. if err := sess.Commit(); err != nil {
  739. return fmt.Errorf("Commit: %v", err)
  740. }
  741. return nil
  742. }
  743. // DeleteComment deletes the comment
  744. func DeleteComment(comment *Comment, doer *User) error {
  745. sess := x.NewSession()
  746. defer sess.Close()
  747. if err := sess.Begin(); err != nil {
  748. return err
  749. }
  750. if _, err := sess.Delete(&Comment{
  751. ID: comment.ID,
  752. }); err != nil {
  753. return err
  754. }
  755. if comment.Type == CommentTypeComment {
  756. if _, err := sess.Exec("UPDATE `issue` SET num_comments = num_comments - 1 WHERE id = ?", comment.IssueID); err != nil {
  757. return err
  758. }
  759. }
  760. if _, err := sess.Where("comment_id = ?", comment.ID).Cols("is_deleted").Update(&Action{IsDeleted: true}); err != nil {
  761. return err
  762. }
  763. if err := comment.neuterCrossReferences(sess); err != nil {
  764. return err
  765. }
  766. return sess.Commit()
  767. }
  768. // CodeComments represents comments on code by using this structure: FILENAME -> LINE (+ == proposed; - == previous) -> COMMENTS
  769. type CodeComments map[string]map[int64][]*Comment
  770. func fetchCodeComments(e Engine, issue *Issue, currentUser *User) (CodeComments, error) {
  771. return fetchCodeCommentsByReview(e, issue, currentUser, nil)
  772. }
  773. func fetchCodeCommentsByReview(e Engine, issue *Issue, currentUser *User, review *Review) (CodeComments, error) {
  774. pathToLineToComment := make(CodeComments)
  775. if review == nil {
  776. review = &Review{ID: 0}
  777. }
  778. //Find comments
  779. opts := FindCommentsOptions{
  780. Type: CommentTypeCode,
  781. IssueID: issue.ID,
  782. ReviewID: review.ID,
  783. }
  784. conds := opts.toConds()
  785. if review.ID == 0 {
  786. conds = conds.And(builder.Eq{"invalidated": false})
  787. }
  788. var comments []*Comment
  789. if err := e.Where(conds).
  790. Asc("comment.created_unix").
  791. Asc("comment.id").
  792. Find(&comments); err != nil {
  793. return nil, err
  794. }
  795. if err := issue.loadRepo(e); err != nil {
  796. return nil, err
  797. }
  798. if err := CommentList(comments).loadPosters(e); err != nil {
  799. return nil, err
  800. }
  801. // Find all reviews by ReviewID
  802. reviews := make(map[int64]*Review)
  803. var ids = make([]int64, 0, len(comments))
  804. for _, comment := range comments {
  805. if comment.ReviewID != 0 {
  806. ids = append(ids, comment.ReviewID)
  807. }
  808. }
  809. if err := e.In("id", ids).Find(&reviews); err != nil {
  810. return nil, err
  811. }
  812. for _, comment := range comments {
  813. if re, ok := reviews[comment.ReviewID]; ok && re != nil {
  814. // If the review is pending only the author can see the comments (except the review is set)
  815. if review.ID == 0 {
  816. if re.Type == ReviewTypePending &&
  817. (currentUser == nil || currentUser.ID != re.ReviewerID) {
  818. continue
  819. }
  820. }
  821. comment.Review = re
  822. }
  823. comment.RenderedContent = string(markdown.Render([]byte(comment.Content), issue.Repo.Link(),
  824. issue.Repo.ComposeMetas()))
  825. if pathToLineToComment[comment.TreePath] == nil {
  826. pathToLineToComment[comment.TreePath] = make(map[int64][]*Comment)
  827. }
  828. pathToLineToComment[comment.TreePath][comment.Line] = append(pathToLineToComment[comment.TreePath][comment.Line], comment)
  829. }
  830. return pathToLineToComment, nil
  831. }
  832. // FetchCodeComments will return a 2d-map: ["Path"]["Line"] = Comments at line
  833. func FetchCodeComments(issue *Issue, currentUser *User) (CodeComments, error) {
  834. return fetchCodeComments(x, issue, currentUser)
  835. }
  836. // UpdateCommentsMigrationsByType updates comments' migrations information via given git service type and original id and poster id
  837. func UpdateCommentsMigrationsByType(tp structs.GitServiceType, originalAuthorID string, posterID int64) error {
  838. _, err := x.Table("comment").
  839. Where(builder.In("issue_id",
  840. builder.Select("issue.id").
  841. From("issue").
  842. InnerJoin("repository", "issue.repo_id = repository.id").
  843. Where(builder.Eq{
  844. "repository.original_service_type": tp,
  845. }),
  846. )).
  847. And("comment.original_author_id = ?", originalAuthorID).
  848. Update(map[string]interface{}{
  849. "poster_id": posterID,
  850. "original_author": "",
  851. "original_author_id": 0,
  852. })
  853. return err
  854. }