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

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