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.

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