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

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