Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

issue_comment.go 32KB

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