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

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