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

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