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

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