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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987
  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. // APIURL formats a API-string to the issue-comment
  210. func (c *Comment) APIURL() 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. err = c.Issue.loadRepo(x)
  217. if err != nil { // Silently dropping errors :unamused:
  218. log.Error("loadRepo(%d): %v", c.Issue.RepoID, err)
  219. return ""
  220. }
  221. return fmt.Sprintf("%s/issues/comments/%d", c.Issue.Repo.APIURL(), c.ID)
  222. }
  223. // IssueURL formats a URL-string to the issue
  224. func (c *Comment) IssueURL() string {
  225. err := c.LoadIssue()
  226. if err != nil { // Silently dropping errors :unamused:
  227. log.Error("LoadIssue(%d): %v", c.IssueID, err)
  228. return ""
  229. }
  230. if c.Issue.IsPull {
  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. return c.Issue.HTMLURL()
  239. }
  240. // PRURL formats a URL-string to the pull-request
  241. func (c *Comment) PRURL() string {
  242. err := c.LoadIssue()
  243. if err != nil { // Silently dropping errors :unamused:
  244. log.Error("LoadIssue(%d): %v", c.IssueID, err)
  245. return ""
  246. }
  247. err = c.Issue.loadRepo(x)
  248. if err != nil { // Silently dropping errors :unamused:
  249. log.Error("loadRepo(%d): %v", c.Issue.RepoID, err)
  250. return ""
  251. }
  252. if !c.Issue.IsPull {
  253. return ""
  254. }
  255. return c.Issue.HTMLURL()
  256. }
  257. // APIFormat converts a Comment to the api.Comment format
  258. func (c *Comment) APIFormat() *api.Comment {
  259. return &api.Comment{
  260. ID: c.ID,
  261. Poster: c.Poster.APIFormat(),
  262. HTMLURL: c.HTMLURL(),
  263. IssueURL: c.IssueURL(),
  264. PRURL: c.PRURL(),
  265. Body: c.Content,
  266. Created: c.CreatedUnix.AsTime(),
  267. Updated: c.UpdatedUnix.AsTime(),
  268. }
  269. }
  270. // CommentHashTag returns unique hash tag for comment id.
  271. func CommentHashTag(id int64) string {
  272. return fmt.Sprintf("issuecomment-%d", id)
  273. }
  274. // HashTag returns unique hash tag for comment.
  275. func (c *Comment) HashTag() string {
  276. return CommentHashTag(c.ID)
  277. }
  278. // EventTag returns unique event hash tag for comment.
  279. func (c *Comment) EventTag() string {
  280. return "event-" + com.ToStr(c.ID)
  281. }
  282. // LoadLabel if comment.Type is CommentTypeLabel, then load Label
  283. func (c *Comment) LoadLabel() error {
  284. var label Label
  285. has, err := x.ID(c.LabelID).Get(&label)
  286. if err != nil {
  287. return err
  288. } else if has {
  289. c.Label = &label
  290. } else {
  291. // Ignore Label is deleted, but not clear this table
  292. log.Warn("Commit %d cannot load label %d", c.ID, c.LabelID)
  293. }
  294. return nil
  295. }
  296. // LoadMilestone if comment.Type is CommentTypeMilestone, then load milestone
  297. func (c *Comment) LoadMilestone() error {
  298. if c.OldMilestoneID > 0 {
  299. var oldMilestone Milestone
  300. has, err := x.ID(c.OldMilestoneID).Get(&oldMilestone)
  301. if err != nil {
  302. return err
  303. } else if has {
  304. c.OldMilestone = &oldMilestone
  305. }
  306. }
  307. if c.MilestoneID > 0 {
  308. var milestone Milestone
  309. has, err := x.ID(c.MilestoneID).Get(&milestone)
  310. if err != nil {
  311. return err
  312. } else if has {
  313. c.Milestone = &milestone
  314. }
  315. }
  316. return nil
  317. }
  318. // LoadPoster loads comment poster
  319. func (c *Comment) LoadPoster() error {
  320. return c.loadPoster(x)
  321. }
  322. // LoadAttachments loads attachments
  323. func (c *Comment) LoadAttachments() error {
  324. if len(c.Attachments) > 0 {
  325. return nil
  326. }
  327. var err error
  328. c.Attachments, err = getAttachmentsByCommentID(x, c.ID)
  329. if err != nil {
  330. log.Error("getAttachmentsByCommentID[%d]: %v", c.ID, err)
  331. }
  332. return nil
  333. }
  334. // UpdateAttachments update attachments by UUIDs for the comment
  335. func (c *Comment) UpdateAttachments(uuids []string) error {
  336. sess := x.NewSession()
  337. defer sess.Close()
  338. if err := sess.Begin(); err != nil {
  339. return err
  340. }
  341. attachments, err := getAttachmentsByUUIDs(sess, uuids)
  342. if err != nil {
  343. return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %v", uuids, err)
  344. }
  345. for i := 0; i < len(attachments); i++ {
  346. attachments[i].IssueID = c.IssueID
  347. attachments[i].CommentID = c.ID
  348. if err := updateAttachment(sess, attachments[i]); err != nil {
  349. return fmt.Errorf("update attachment [id: %d]: %v", attachments[i].ID, err)
  350. }
  351. }
  352. return sess.Commit()
  353. }
  354. // LoadAssigneeUser if comment.Type is CommentTypeAssignees, then load assignees
  355. func (c *Comment) LoadAssigneeUser() error {
  356. var err error
  357. if c.AssigneeID > 0 {
  358. c.Assignee, err = getUserByID(x, c.AssigneeID)
  359. if err != nil {
  360. if !IsErrUserNotExist(err) {
  361. return err
  362. }
  363. c.Assignee = NewGhostUser()
  364. }
  365. }
  366. return nil
  367. }
  368. // LoadDepIssueDetails loads Dependent Issue Details
  369. func (c *Comment) LoadDepIssueDetails() (err error) {
  370. if c.DependentIssueID <= 0 || c.DependentIssue != nil {
  371. return nil
  372. }
  373. c.DependentIssue, err = getIssueByID(x, c.DependentIssueID)
  374. return err
  375. }
  376. func (c *Comment) loadReactions(e Engine, repo *Repository) (err error) {
  377. if c.Reactions != nil {
  378. return nil
  379. }
  380. c.Reactions, err = findReactions(e, FindReactionsOptions{
  381. IssueID: c.IssueID,
  382. CommentID: c.ID,
  383. })
  384. if err != nil {
  385. return err
  386. }
  387. // Load reaction user data
  388. if _, err := c.Reactions.loadUsers(e, repo); err != nil {
  389. return err
  390. }
  391. return nil
  392. }
  393. // LoadReactions loads comment reactions
  394. func (c *Comment) LoadReactions(repo *Repository) error {
  395. return c.loadReactions(x, repo)
  396. }
  397. func (c *Comment) loadReview(e Engine) (err error) {
  398. if c.Review == nil {
  399. if c.Review, err = getReviewByID(e, c.ReviewID); err != nil {
  400. return err
  401. }
  402. }
  403. c.Review.Issue = c.Issue
  404. return nil
  405. }
  406. // LoadReview loads the associated review
  407. func (c *Comment) LoadReview() error {
  408. return c.loadReview(x)
  409. }
  410. func (c *Comment) checkInvalidation(doer *User, repo *git.Repository, branch string) error {
  411. // FIXME differentiate between previous and proposed line
  412. commit, err := repo.LineBlame(branch, repo.Path, c.TreePath, uint(c.UnsignedLine()))
  413. if err != nil && strings.Contains(err.Error(), "fatal: no such path") {
  414. c.Invalidated = true
  415. return UpdateComment(c, doer)
  416. }
  417. if err != nil {
  418. return err
  419. }
  420. if c.CommitSHA != "" && c.CommitSHA != commit.ID.String() {
  421. c.Invalidated = true
  422. return UpdateComment(c, doer)
  423. }
  424. return nil
  425. }
  426. // CheckInvalidation checks if the line of code comment got changed by another commit.
  427. // If the line got changed the comment is going to be invalidated.
  428. func (c *Comment) CheckInvalidation(repo *git.Repository, doer *User, branch string) error {
  429. return c.checkInvalidation(doer, repo, branch)
  430. }
  431. // DiffSide returns "previous" if Comment.Line is a LOC of the previous changes and "proposed" if it is a LOC of the proposed changes.
  432. func (c *Comment) DiffSide() string {
  433. if c.Line < 0 {
  434. return "previous"
  435. }
  436. return "proposed"
  437. }
  438. // UnsignedLine returns the LOC of the code comment without + or -
  439. func (c *Comment) UnsignedLine() uint64 {
  440. if c.Line < 0 {
  441. return uint64(c.Line * -1)
  442. }
  443. return uint64(c.Line)
  444. }
  445. // CodeCommentURL returns the url to a comment in code
  446. func (c *Comment) CodeCommentURL() string {
  447. err := c.LoadIssue()
  448. if err != nil { // Silently dropping errors :unamused:
  449. log.Error("LoadIssue(%d): %v", c.IssueID, err)
  450. return ""
  451. }
  452. err = c.Issue.loadRepo(x)
  453. if err != nil { // Silently dropping errors :unamused:
  454. log.Error("loadRepo(%d): %v", c.Issue.RepoID, err)
  455. return ""
  456. }
  457. return fmt.Sprintf("%s/files#%s", c.Issue.HTMLURL(), c.HashTag())
  458. }
  459. func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err error) {
  460. var LabelID int64
  461. if opts.Label != nil {
  462. LabelID = opts.Label.ID
  463. }
  464. comment := &Comment{
  465. Type: opts.Type,
  466. PosterID: opts.Doer.ID,
  467. Poster: opts.Doer,
  468. IssueID: opts.Issue.ID,
  469. LabelID: LabelID,
  470. OldMilestoneID: opts.OldMilestoneID,
  471. MilestoneID: opts.MilestoneID,
  472. RemovedAssignee: opts.RemovedAssignee,
  473. AssigneeID: opts.AssigneeID,
  474. CommitID: opts.CommitID,
  475. CommitSHA: opts.CommitSHA,
  476. Line: opts.LineNum,
  477. Content: opts.Content,
  478. OldTitle: opts.OldTitle,
  479. NewTitle: opts.NewTitle,
  480. OldRef: opts.OldRef,
  481. NewRef: opts.NewRef,
  482. DependentIssueID: opts.DependentIssueID,
  483. TreePath: opts.TreePath,
  484. ReviewID: opts.ReviewID,
  485. Patch: opts.Patch,
  486. RefRepoID: opts.RefRepoID,
  487. RefIssueID: opts.RefIssueID,
  488. RefCommentID: opts.RefCommentID,
  489. RefAction: opts.RefAction,
  490. RefIsPull: opts.RefIsPull,
  491. }
  492. if _, err = e.Insert(comment); err != nil {
  493. return nil, err
  494. }
  495. if err = opts.Repo.getOwner(e); err != nil {
  496. return nil, err
  497. }
  498. if err = updateCommentInfos(e, opts, comment); err != nil {
  499. return nil, err
  500. }
  501. if err = comment.addCrossReferences(e, opts.Doer, false); err != nil {
  502. return nil, err
  503. }
  504. return comment, nil
  505. }
  506. func updateCommentInfos(e *xorm.Session, opts *CreateCommentOptions, comment *Comment) (err error) {
  507. // Check comment type.
  508. switch opts.Type {
  509. case CommentTypeCode:
  510. if comment.ReviewID != 0 {
  511. if comment.Review == nil {
  512. if err := comment.loadReview(e); err != nil {
  513. return err
  514. }
  515. }
  516. if comment.Review.Type <= ReviewTypePending {
  517. return nil
  518. }
  519. }
  520. fallthrough
  521. case CommentTypeComment:
  522. if _, err = e.Exec("UPDATE `issue` SET num_comments=num_comments+1 WHERE id=?", opts.Issue.ID); err != nil {
  523. return err
  524. }
  525. // Check attachments
  526. attachments, err := getAttachmentsByUUIDs(e, opts.Attachments)
  527. if err != nil {
  528. return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %v", opts.Attachments, err)
  529. }
  530. for i := range attachments {
  531. attachments[i].IssueID = opts.Issue.ID
  532. attachments[i].CommentID = comment.ID
  533. // No assign value could be 0, so ignore AllCols().
  534. if _, err = e.ID(attachments[i].ID).Update(attachments[i]); err != nil {
  535. return fmt.Errorf("update attachment [%d]: %v", attachments[i].ID, err)
  536. }
  537. }
  538. case CommentTypeReopen, CommentTypeClose:
  539. if err = opts.Issue.updateClosedNum(e); err != nil {
  540. return err
  541. }
  542. }
  543. // update the issue's updated_unix column
  544. return updateIssueCols(e, opts.Issue, "updated_unix")
  545. }
  546. func createDeadlineComment(e *xorm.Session, doer *User, issue *Issue, newDeadlineUnix timeutil.TimeStamp) (*Comment, error) {
  547. var content string
  548. var commentType CommentType
  549. // newDeadline = 0 means deleting
  550. if newDeadlineUnix == 0 {
  551. commentType = CommentTypeRemovedDeadline
  552. content = issue.DeadlineUnix.Format("2006-01-02")
  553. } else if issue.DeadlineUnix == 0 {
  554. // Check if the new date was added or modified
  555. // If the actual deadline is 0 => deadline added
  556. commentType = CommentTypeAddedDeadline
  557. content = newDeadlineUnix.Format("2006-01-02")
  558. } else { // Otherwise modified
  559. commentType = CommentTypeModifiedDeadline
  560. content = newDeadlineUnix.Format("2006-01-02") + "|" + issue.DeadlineUnix.Format("2006-01-02")
  561. }
  562. if err := issue.loadRepo(e); err != nil {
  563. return nil, err
  564. }
  565. var opts = &CreateCommentOptions{
  566. Type: commentType,
  567. Doer: doer,
  568. Repo: issue.Repo,
  569. Issue: issue,
  570. Content: content,
  571. }
  572. comment, err := createComment(e, opts)
  573. if err != nil {
  574. return nil, err
  575. }
  576. return comment, nil
  577. }
  578. // Creates issue dependency comment
  579. func createIssueDependencyComment(e *xorm.Session, doer *User, issue *Issue, dependentIssue *Issue, add bool) (err error) {
  580. cType := CommentTypeAddDependency
  581. if !add {
  582. cType = CommentTypeRemoveDependency
  583. }
  584. if err = issue.loadRepo(e); err != nil {
  585. return
  586. }
  587. // Make two comments, one in each issue
  588. var opts = &CreateCommentOptions{
  589. Type: cType,
  590. Doer: doer,
  591. Repo: issue.Repo,
  592. Issue: issue,
  593. DependentIssueID: dependentIssue.ID,
  594. }
  595. if _, err = createComment(e, opts); err != nil {
  596. return
  597. }
  598. opts = &CreateCommentOptions{
  599. Type: cType,
  600. Doer: doer,
  601. Repo: issue.Repo,
  602. Issue: dependentIssue,
  603. DependentIssueID: issue.ID,
  604. }
  605. _, err = createComment(e, opts)
  606. return
  607. }
  608. // CreateCommentOptions defines options for creating comment
  609. type CreateCommentOptions struct {
  610. Type CommentType
  611. Doer *User
  612. Repo *Repository
  613. Issue *Issue
  614. Label *Label
  615. DependentIssueID int64
  616. OldMilestoneID int64
  617. MilestoneID int64
  618. AssigneeID int64
  619. RemovedAssignee bool
  620. OldTitle string
  621. NewTitle string
  622. OldRef string
  623. NewRef string
  624. CommitID int64
  625. CommitSHA string
  626. Patch string
  627. LineNum int64
  628. TreePath string
  629. ReviewID int64
  630. Content string
  631. Attachments []string // UUIDs of attachments
  632. RefRepoID int64
  633. RefIssueID int64
  634. RefCommentID int64
  635. RefAction references.XRefAction
  636. RefIsPull bool
  637. }
  638. // CreateComment creates comment of issue or commit.
  639. func CreateComment(opts *CreateCommentOptions) (comment *Comment, err error) {
  640. sess := x.NewSession()
  641. defer sess.Close()
  642. if err = sess.Begin(); err != nil {
  643. return nil, err
  644. }
  645. comment, err = createComment(sess, opts)
  646. if err != nil {
  647. return nil, err
  648. }
  649. if err = sess.Commit(); err != nil {
  650. return nil, err
  651. }
  652. return comment, nil
  653. }
  654. // CreateRefComment creates a commit reference comment to issue.
  655. func CreateRefComment(doer *User, repo *Repository, issue *Issue, content, commitSHA string) error {
  656. if len(commitSHA) == 0 {
  657. return fmt.Errorf("cannot create reference with empty commit SHA")
  658. }
  659. // Check if same reference from same commit has already existed.
  660. has, err := x.Get(&Comment{
  661. Type: CommentTypeCommitRef,
  662. IssueID: issue.ID,
  663. CommitSHA: commitSHA,
  664. })
  665. if err != nil {
  666. return fmt.Errorf("check reference comment: %v", err)
  667. } else if has {
  668. return nil
  669. }
  670. _, err = CreateComment(&CreateCommentOptions{
  671. Type: CommentTypeCommitRef,
  672. Doer: doer,
  673. Repo: repo,
  674. Issue: issue,
  675. CommitSHA: commitSHA,
  676. Content: content,
  677. })
  678. return err
  679. }
  680. // GetCommentByID returns the comment by given ID.
  681. func GetCommentByID(id int64) (*Comment, error) {
  682. return getCommentByID(x, id)
  683. }
  684. func getCommentByID(e Engine, id int64) (*Comment, error) {
  685. c := new(Comment)
  686. has, err := e.ID(id).Get(c)
  687. if err != nil {
  688. return nil, err
  689. } else if !has {
  690. return nil, ErrCommentNotExist{id, 0}
  691. }
  692. return c, nil
  693. }
  694. // FindCommentsOptions describes the conditions to Find comments
  695. type FindCommentsOptions struct {
  696. ListOptions
  697. RepoID int64
  698. IssueID int64
  699. ReviewID int64
  700. Since int64
  701. Before int64
  702. Type CommentType
  703. }
  704. func (opts *FindCommentsOptions) toConds() builder.Cond {
  705. var cond = builder.NewCond()
  706. if opts.RepoID > 0 {
  707. cond = cond.And(builder.Eq{"issue.repo_id": opts.RepoID})
  708. }
  709. if opts.IssueID > 0 {
  710. cond = cond.And(builder.Eq{"comment.issue_id": opts.IssueID})
  711. }
  712. if opts.ReviewID > 0 {
  713. cond = cond.And(builder.Eq{"comment.review_id": opts.ReviewID})
  714. }
  715. if opts.Since > 0 {
  716. cond = cond.And(builder.Gte{"comment.updated_unix": opts.Since})
  717. }
  718. if opts.Before > 0 {
  719. cond = cond.And(builder.Lte{"comment.updated_unix": opts.Before})
  720. }
  721. if opts.Type != CommentTypeUnknown {
  722. cond = cond.And(builder.Eq{"comment.type": opts.Type})
  723. }
  724. return cond
  725. }
  726. func findComments(e Engine, opts FindCommentsOptions) ([]*Comment, error) {
  727. comments := make([]*Comment, 0, 10)
  728. sess := e.Where(opts.toConds())
  729. if opts.RepoID > 0 {
  730. sess.Join("INNER", "issue", "issue.id = comment.issue_id")
  731. }
  732. if opts.Page != 0 {
  733. sess = opts.setSessionPagination(sess)
  734. }
  735. return comments, sess.
  736. Asc("comment.created_unix").
  737. Asc("comment.id").
  738. Find(&comments)
  739. }
  740. // FindComments returns all comments according options
  741. func FindComments(opts FindCommentsOptions) ([]*Comment, error) {
  742. return findComments(x, opts)
  743. }
  744. // UpdateComment updates information of comment.
  745. func UpdateComment(c *Comment, doer *User) error {
  746. sess := x.NewSession()
  747. defer sess.Close()
  748. if err := sess.Begin(); err != nil {
  749. return err
  750. }
  751. if _, err := sess.ID(c.ID).AllCols().Update(c); err != nil {
  752. return err
  753. }
  754. if err := c.loadIssue(sess); err != nil {
  755. return err
  756. }
  757. if err := c.addCrossReferences(sess, doer, true); err != nil {
  758. return err
  759. }
  760. if err := sess.Commit(); err != nil {
  761. return fmt.Errorf("Commit: %v", err)
  762. }
  763. return nil
  764. }
  765. // DeleteComment deletes the comment
  766. func DeleteComment(comment *Comment, doer *User) error {
  767. sess := x.NewSession()
  768. defer sess.Close()
  769. if err := sess.Begin(); err != nil {
  770. return err
  771. }
  772. if _, err := sess.Delete(&Comment{
  773. ID: comment.ID,
  774. }); err != nil {
  775. return err
  776. }
  777. if comment.Type == CommentTypeComment {
  778. if _, err := sess.Exec("UPDATE `issue` SET num_comments = num_comments - 1 WHERE id = ?", comment.IssueID); err != nil {
  779. return err
  780. }
  781. }
  782. if _, err := sess.Where("comment_id = ?", comment.ID).Cols("is_deleted").Update(&Action{IsDeleted: true}); err != nil {
  783. return err
  784. }
  785. if err := comment.neuterCrossReferences(sess); err != nil {
  786. return err
  787. }
  788. return sess.Commit()
  789. }
  790. // CodeComments represents comments on code by using this structure: FILENAME -> LINE (+ == proposed; - == previous) -> COMMENTS
  791. type CodeComments map[string]map[int64][]*Comment
  792. func fetchCodeComments(e Engine, issue *Issue, currentUser *User) (CodeComments, error) {
  793. return fetchCodeCommentsByReview(e, issue, currentUser, nil)
  794. }
  795. func fetchCodeCommentsByReview(e Engine, issue *Issue, currentUser *User, review *Review) (CodeComments, error) {
  796. pathToLineToComment := make(CodeComments)
  797. if review == nil {
  798. review = &Review{ID: 0}
  799. }
  800. //Find comments
  801. opts := FindCommentsOptions{
  802. Type: CommentTypeCode,
  803. IssueID: issue.ID,
  804. ReviewID: review.ID,
  805. }
  806. conds := opts.toConds()
  807. if review.ID == 0 {
  808. conds = conds.And(builder.Eq{"invalidated": false})
  809. }
  810. var comments []*Comment
  811. if err := e.Where(conds).
  812. Asc("comment.created_unix").
  813. Asc("comment.id").
  814. Find(&comments); err != nil {
  815. return nil, err
  816. }
  817. if err := issue.loadRepo(e); err != nil {
  818. return nil, err
  819. }
  820. if err := CommentList(comments).loadPosters(e); err != nil {
  821. return nil, err
  822. }
  823. // Find all reviews by ReviewID
  824. reviews := make(map[int64]*Review)
  825. var ids = make([]int64, 0, len(comments))
  826. for _, comment := range comments {
  827. if comment.ReviewID != 0 {
  828. ids = append(ids, comment.ReviewID)
  829. }
  830. }
  831. if err := e.In("id", ids).Find(&reviews); err != nil {
  832. return nil, err
  833. }
  834. for _, comment := range comments {
  835. if re, ok := reviews[comment.ReviewID]; ok && re != nil {
  836. // If the review is pending only the author can see the comments (except the review is set)
  837. if review.ID == 0 {
  838. if re.Type == ReviewTypePending &&
  839. (currentUser == nil || currentUser.ID != re.ReviewerID) {
  840. continue
  841. }
  842. }
  843. comment.Review = re
  844. }
  845. comment.RenderedContent = string(markdown.Render([]byte(comment.Content), issue.Repo.Link(),
  846. issue.Repo.ComposeMetas()))
  847. if pathToLineToComment[comment.TreePath] == nil {
  848. pathToLineToComment[comment.TreePath] = make(map[int64][]*Comment)
  849. }
  850. pathToLineToComment[comment.TreePath][comment.Line] = append(pathToLineToComment[comment.TreePath][comment.Line], comment)
  851. }
  852. return pathToLineToComment, nil
  853. }
  854. // FetchCodeComments will return a 2d-map: ["Path"]["Line"] = Comments at line
  855. func FetchCodeComments(issue *Issue, currentUser *User) (CodeComments, error) {
  856. return fetchCodeComments(x, issue, currentUser)
  857. }
  858. // UpdateCommentsMigrationsByType updates comments' migrations information via given git service type and original id and poster id
  859. func UpdateCommentsMigrationsByType(tp structs.GitServiceType, originalAuthorID string, posterID int64) error {
  860. _, err := x.Table("comment").
  861. Where(builder.In("issue_id",
  862. builder.Select("issue.id").
  863. From("issue").
  864. InnerJoin("repository", "issue.repo_id = repository.id").
  865. Where(builder.Eq{
  866. "repository.original_service_type": tp,
  867. }),
  868. )).
  869. And("comment.original_author_id = ?", originalAuthorID).
  870. Update(map[string]interface{}{
  871. "poster_id": posterID,
  872. "original_author": "",
  873. "original_author_id": 0,
  874. })
  875. return err
  876. }