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

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