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

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