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

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