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

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