Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

issue_comment.go 26KB

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