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

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