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

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