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.

comment.go 34KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227
  1. // Copyright 2018 The Gitea Authors.
  2. // Copyright 2016 The Gogs Authors.
  3. // All rights reserved.
  4. // SPDX-License-Identifier: MIT
  5. package issues
  6. import (
  7. "context"
  8. "fmt"
  9. "strconv"
  10. "unicode/utf8"
  11. "code.gitea.io/gitea/models/db"
  12. git_model "code.gitea.io/gitea/models/git"
  13. "code.gitea.io/gitea/models/organization"
  14. project_model "code.gitea.io/gitea/models/project"
  15. repo_model "code.gitea.io/gitea/models/repo"
  16. user_model "code.gitea.io/gitea/models/user"
  17. "code.gitea.io/gitea/modules/git"
  18. "code.gitea.io/gitea/modules/json"
  19. "code.gitea.io/gitea/modules/log"
  20. "code.gitea.io/gitea/modules/references"
  21. "code.gitea.io/gitea/modules/structs"
  22. "code.gitea.io/gitea/modules/timeutil"
  23. "code.gitea.io/gitea/modules/util"
  24. "xorm.io/builder"
  25. "xorm.io/xorm"
  26. )
  27. // ErrCommentNotExist represents a "CommentNotExist" kind of error.
  28. type ErrCommentNotExist struct {
  29. ID int64
  30. IssueID int64
  31. }
  32. // IsErrCommentNotExist checks if an error is a ErrCommentNotExist.
  33. func IsErrCommentNotExist(err error) bool {
  34. _, ok := err.(ErrCommentNotExist)
  35. return ok
  36. }
  37. func (err ErrCommentNotExist) Error() string {
  38. return fmt.Sprintf("comment does not exist [id: %d, issue_id: %d]", err.ID, err.IssueID)
  39. }
  40. func (err ErrCommentNotExist) Unwrap() error {
  41. return util.ErrNotExist
  42. }
  43. // CommentType defines whether a comment is just a simple comment, an action (like close) or a reference.
  44. type CommentType int
  45. // define unknown comment type
  46. const (
  47. CommentTypeUnknown CommentType = -1
  48. )
  49. // Enumerate all the comment types
  50. const (
  51. // 0 Plain comment, can be associated with a commit (CommitID > 0) and a line (LineNum > 0)
  52. CommentTypeComment CommentType = iota
  53. CommentTypeReopen // 1
  54. CommentTypeClose // 2
  55. // 3 References.
  56. CommentTypeIssueRef
  57. // 4 Reference from a commit (not part of a pull request)
  58. CommentTypeCommitRef
  59. // 5 Reference from a comment
  60. CommentTypeCommentRef
  61. // 6 Reference from a pull request
  62. CommentTypePullRef
  63. // 7 Labels changed
  64. CommentTypeLabel
  65. // 8 Milestone changed
  66. CommentTypeMilestone
  67. // 9 Assignees changed
  68. CommentTypeAssignees
  69. // 10 Change Title
  70. CommentTypeChangeTitle
  71. // 11 Delete Branch
  72. CommentTypeDeleteBranch
  73. // 12 Start a stopwatch for time tracking
  74. CommentTypeStartTracking
  75. // 13 Stop a stopwatch for time tracking
  76. CommentTypeStopTracking
  77. // 14 Add time manual for time tracking
  78. CommentTypeAddTimeManual
  79. // 15 Cancel a stopwatch for time tracking
  80. CommentTypeCancelTracking
  81. // 16 Added a due date
  82. CommentTypeAddedDeadline
  83. // 17 Modified the due date
  84. CommentTypeModifiedDeadline
  85. // 18 Removed a due date
  86. CommentTypeRemovedDeadline
  87. // 19 Dependency added
  88. CommentTypeAddDependency
  89. // 20 Dependency removed
  90. CommentTypeRemoveDependency
  91. // 21 Comment a line of code
  92. CommentTypeCode
  93. // 22 Reviews a pull request by giving general feedback
  94. CommentTypeReview
  95. // 23 Lock an issue, giving only collaborators access
  96. CommentTypeLock
  97. // 24 Unlocks a previously locked issue
  98. CommentTypeUnlock
  99. // 25 Change pull request's target branch
  100. CommentTypeChangeTargetBranch
  101. // 26 Delete time manual for time tracking
  102. CommentTypeDeleteTimeManual
  103. // 27 add or remove Request from one
  104. CommentTypeReviewRequest
  105. // 28 merge pull request
  106. CommentTypeMergePull
  107. // 29 push to PR head branch
  108. CommentTypePullRequestPush
  109. // 30 Project changed
  110. CommentTypeProject
  111. // 31 Project board changed
  112. CommentTypeProjectBoard
  113. // 32 Dismiss Review
  114. CommentTypeDismissReview
  115. // 33 Change issue ref
  116. CommentTypeChangeIssueRef
  117. // 34 pr was scheduled to auto merge when checks succeed
  118. CommentTypePRScheduledToAutoMerge
  119. // 35 pr was un scheduled to auto merge when checks succeed
  120. CommentTypePRUnScheduledToAutoMerge
  121. )
  122. var commentStrings = []string{
  123. "comment",
  124. "reopen",
  125. "close",
  126. "issue_ref",
  127. "commit_ref",
  128. "comment_ref",
  129. "pull_ref",
  130. "label",
  131. "milestone",
  132. "assignees",
  133. "change_title",
  134. "delete_branch",
  135. "start_tracking",
  136. "stop_tracking",
  137. "add_time_manual",
  138. "cancel_tracking",
  139. "added_deadline",
  140. "modified_deadline",
  141. "removed_deadline",
  142. "add_dependency",
  143. "remove_dependency",
  144. "code",
  145. "review",
  146. "lock",
  147. "unlock",
  148. "change_target_branch",
  149. "delete_time_manual",
  150. "review_request",
  151. "merge_pull",
  152. "pull_push",
  153. "project",
  154. "project_board",
  155. "dismiss_review",
  156. "change_issue_ref",
  157. "pull_scheduled_merge",
  158. "pull_cancel_scheduled_merge",
  159. }
  160. func (t CommentType) String() string {
  161. return commentStrings[t]
  162. }
  163. func AsCommentType(typeName string) CommentType {
  164. for index, name := range commentStrings {
  165. if typeName == name {
  166. return CommentType(index)
  167. }
  168. }
  169. return CommentTypeUnknown
  170. }
  171. // RoleDescriptor defines comment tag type
  172. type RoleDescriptor int
  173. // Enumerate all the role tags.
  174. const (
  175. RoleDescriptorNone RoleDescriptor = iota
  176. RoleDescriptorPoster
  177. RoleDescriptorWriter
  178. RoleDescriptorOwner
  179. )
  180. // WithRole enable a specific tag on the RoleDescriptor.
  181. func (rd RoleDescriptor) WithRole(role RoleDescriptor) RoleDescriptor {
  182. return rd | (1 << role)
  183. }
  184. func stringToRoleDescriptor(role string) RoleDescriptor {
  185. switch role {
  186. case "Poster":
  187. return RoleDescriptorPoster
  188. case "Writer":
  189. return RoleDescriptorWriter
  190. case "Owner":
  191. return RoleDescriptorOwner
  192. default:
  193. return RoleDescriptorNone
  194. }
  195. }
  196. // HasRole returns if a certain role is enabled on the RoleDescriptor.
  197. func (rd RoleDescriptor) HasRole(role string) bool {
  198. roleDescriptor := stringToRoleDescriptor(role)
  199. bitValue := rd & (1 << roleDescriptor)
  200. return (bitValue > 0)
  201. }
  202. // Comment represents a comment in commit and issue page.
  203. type Comment struct {
  204. ID int64 `xorm:"pk autoincr"`
  205. Type CommentType `xorm:"INDEX"`
  206. PosterID int64 `xorm:"INDEX"`
  207. Poster *user_model.User `xorm:"-"`
  208. OriginalAuthor string
  209. OriginalAuthorID int64
  210. IssueID int64 `xorm:"INDEX"`
  211. Issue *Issue `xorm:"-"`
  212. LabelID int64
  213. Label *Label `xorm:"-"`
  214. AddedLabels []*Label `xorm:"-"`
  215. RemovedLabels []*Label `xorm:"-"`
  216. OldProjectID int64
  217. ProjectID int64
  218. OldProject *project_model.Project `xorm:"-"`
  219. Project *project_model.Project `xorm:"-"`
  220. OldMilestoneID int64
  221. MilestoneID int64
  222. OldMilestone *Milestone `xorm:"-"`
  223. Milestone *Milestone `xorm:"-"`
  224. TimeID int64
  225. Time *TrackedTime `xorm:"-"`
  226. AssigneeID int64
  227. RemovedAssignee bool
  228. Assignee *user_model.User `xorm:"-"`
  229. AssigneeTeamID int64 `xorm:"NOT NULL DEFAULT 0"`
  230. AssigneeTeam *organization.Team `xorm:"-"`
  231. ResolveDoerID int64
  232. ResolveDoer *user_model.User `xorm:"-"`
  233. OldTitle string
  234. NewTitle string
  235. OldRef string
  236. NewRef string
  237. DependentIssueID int64
  238. DependentIssue *Issue `xorm:"-"`
  239. CommitID int64
  240. Line int64 // - previous line / + proposed line
  241. TreePath string
  242. Content string `xorm:"LONGTEXT"`
  243. RenderedContent string `xorm:"-"`
  244. // Path represents the 4 lines of code cemented by this comment
  245. Patch string `xorm:"-"`
  246. PatchQuoted string `xorm:"LONGTEXT patch"`
  247. CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
  248. UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
  249. // Reference issue in commit message
  250. CommitSHA string `xorm:"VARCHAR(40)"`
  251. Attachments []*repo_model.Attachment `xorm:"-"`
  252. Reactions ReactionList `xorm:"-"`
  253. // For view issue page.
  254. ShowRole RoleDescriptor `xorm:"-"`
  255. Review *Review `xorm:"-"`
  256. ReviewID int64 `xorm:"index"`
  257. Invalidated bool
  258. // Reference an issue or pull from another comment, issue or PR
  259. // All information is about the origin of the reference
  260. RefRepoID int64 `xorm:"index"` // Repo where the referencing
  261. RefIssueID int64 `xorm:"index"`
  262. RefCommentID int64 `xorm:"index"` // 0 if origin is Issue title or content (or PR's)
  263. RefAction references.XRefAction `xorm:"SMALLINT"` // What happens if RefIssueID resolves
  264. RefIsPull bool
  265. RefRepo *repo_model.Repository `xorm:"-"`
  266. RefIssue *Issue `xorm:"-"`
  267. RefComment *Comment `xorm:"-"`
  268. Commits []*git_model.SignCommitWithStatuses `xorm:"-"`
  269. OldCommit string `xorm:"-"`
  270. NewCommit string `xorm:"-"`
  271. CommitsNum int64 `xorm:"-"`
  272. IsForcePush bool `xorm:"-"`
  273. }
  274. func init() {
  275. db.RegisterModel(new(Comment))
  276. }
  277. // PushActionContent is content of push pull comment
  278. type PushActionContent struct {
  279. IsForcePush bool `json:"is_force_push"`
  280. CommitIDs []string `json:"commit_ids"`
  281. }
  282. // LoadIssue loads the issue reference for the comment
  283. func (c *Comment) LoadIssue(ctx context.Context) (err error) {
  284. if c.Issue != nil {
  285. return nil
  286. }
  287. c.Issue, err = GetIssueByID(ctx, c.IssueID)
  288. return err
  289. }
  290. // BeforeInsert will be invoked by XORM before inserting a record
  291. func (c *Comment) BeforeInsert() {
  292. c.PatchQuoted = c.Patch
  293. if !utf8.ValidString(c.Patch) {
  294. c.PatchQuoted = strconv.Quote(c.Patch)
  295. }
  296. }
  297. // BeforeUpdate will be invoked by XORM before updating a record
  298. func (c *Comment) BeforeUpdate() {
  299. c.PatchQuoted = c.Patch
  300. if !utf8.ValidString(c.Patch) {
  301. c.PatchQuoted = strconv.Quote(c.Patch)
  302. }
  303. }
  304. // AfterLoad is invoked from XORM after setting the values of all fields of this object.
  305. func (c *Comment) AfterLoad(session *xorm.Session) {
  306. c.Patch = c.PatchQuoted
  307. if len(c.PatchQuoted) > 0 && c.PatchQuoted[0] == '"' {
  308. unquoted, err := strconv.Unquote(c.PatchQuoted)
  309. if err == nil {
  310. c.Patch = unquoted
  311. }
  312. }
  313. }
  314. // LoadPoster loads comment poster
  315. func (c *Comment) LoadPoster(ctx context.Context) (err error) {
  316. if c.PosterID <= 0 || c.Poster != nil {
  317. return nil
  318. }
  319. c.Poster, err = user_model.GetPossibleUserByID(ctx, c.PosterID)
  320. if err != nil {
  321. if user_model.IsErrUserNotExist(err) {
  322. c.PosterID = -1
  323. c.Poster = user_model.NewGhostUser()
  324. } else {
  325. log.Error("getUserByID[%d]: %v", c.ID, err)
  326. }
  327. }
  328. return err
  329. }
  330. // AfterDelete is invoked from XORM after the object is deleted.
  331. func (c *Comment) AfterDelete() {
  332. if c.ID <= 0 {
  333. return
  334. }
  335. _, err := repo_model.DeleteAttachmentsByComment(c.ID, true)
  336. if err != nil {
  337. log.Info("Could not delete files for comment %d on issue #%d: %s", c.ID, c.IssueID, err)
  338. }
  339. }
  340. // HTMLURL formats a URL-string to the issue-comment
  341. func (c *Comment) HTMLURL() string {
  342. err := c.LoadIssue(db.DefaultContext)
  343. if err != nil { // Silently dropping errors :unamused:
  344. log.Error("LoadIssue(%d): %v", c.IssueID, err)
  345. return ""
  346. }
  347. err = c.Issue.LoadRepo(db.DefaultContext)
  348. if err != nil { // Silently dropping errors :unamused:
  349. log.Error("loadRepo(%d): %v", c.Issue.RepoID, err)
  350. return ""
  351. }
  352. if c.Type == CommentTypeCode {
  353. if c.ReviewID == 0 {
  354. return fmt.Sprintf("%s/files#%s", c.Issue.HTMLURL(), c.HashTag())
  355. }
  356. if c.Review == nil {
  357. if err := c.LoadReview(); err != nil {
  358. log.Warn("LoadReview(%d): %v", c.ReviewID, err)
  359. return fmt.Sprintf("%s/files#%s", c.Issue.HTMLURL(), c.HashTag())
  360. }
  361. }
  362. if c.Review.Type <= ReviewTypePending {
  363. return fmt.Sprintf("%s/files#%s", c.Issue.HTMLURL(), c.HashTag())
  364. }
  365. }
  366. return fmt.Sprintf("%s#%s", c.Issue.HTMLURL(), c.HashTag())
  367. }
  368. // APIURL formats a API-string to the issue-comment
  369. func (c *Comment) APIURL() string {
  370. err := c.LoadIssue(db.DefaultContext)
  371. if err != nil { // Silently dropping errors :unamused:
  372. log.Error("LoadIssue(%d): %v", c.IssueID, err)
  373. return ""
  374. }
  375. err = c.Issue.LoadRepo(db.DefaultContext)
  376. if err != nil { // Silently dropping errors :unamused:
  377. log.Error("loadRepo(%d): %v", c.Issue.RepoID, err)
  378. return ""
  379. }
  380. return fmt.Sprintf("%s/issues/comments/%d", c.Issue.Repo.APIURL(), c.ID)
  381. }
  382. // IssueURL formats a URL-string to the issue
  383. func (c *Comment) IssueURL() string {
  384. err := c.LoadIssue(db.DefaultContext)
  385. if err != nil { // Silently dropping errors :unamused:
  386. log.Error("LoadIssue(%d): %v", c.IssueID, err)
  387. return ""
  388. }
  389. if c.Issue.IsPull {
  390. return ""
  391. }
  392. err = c.Issue.LoadRepo(db.DefaultContext)
  393. if err != nil { // Silently dropping errors :unamused:
  394. log.Error("loadRepo(%d): %v", c.Issue.RepoID, err)
  395. return ""
  396. }
  397. return c.Issue.HTMLURL()
  398. }
  399. // PRURL formats a URL-string to the pull-request
  400. func (c *Comment) PRURL() string {
  401. err := c.LoadIssue(db.DefaultContext)
  402. if err != nil { // Silently dropping errors :unamused:
  403. log.Error("LoadIssue(%d): %v", c.IssueID, err)
  404. return ""
  405. }
  406. err = c.Issue.LoadRepo(db.DefaultContext)
  407. if err != nil { // Silently dropping errors :unamused:
  408. log.Error("loadRepo(%d): %v", c.Issue.RepoID, err)
  409. return ""
  410. }
  411. if !c.Issue.IsPull {
  412. return ""
  413. }
  414. return c.Issue.HTMLURL()
  415. }
  416. // CommentHashTag returns unique hash tag for comment id.
  417. func CommentHashTag(id int64) string {
  418. return fmt.Sprintf("issuecomment-%d", id)
  419. }
  420. // HashTag returns unique hash tag for comment.
  421. func (c *Comment) HashTag() string {
  422. return CommentHashTag(c.ID)
  423. }
  424. // EventTag returns unique event hash tag for comment.
  425. func (c *Comment) EventTag() string {
  426. return fmt.Sprintf("event-%d", c.ID)
  427. }
  428. // LoadLabel if comment.Type is CommentTypeLabel, then load Label
  429. func (c *Comment) LoadLabel() error {
  430. var label Label
  431. has, err := db.GetEngine(db.DefaultContext).ID(c.LabelID).Get(&label)
  432. if err != nil {
  433. return err
  434. } else if has {
  435. c.Label = &label
  436. } else {
  437. // Ignore Label is deleted, but not clear this table
  438. log.Warn("Commit %d cannot load label %d", c.ID, c.LabelID)
  439. }
  440. return nil
  441. }
  442. // LoadProject if comment.Type is CommentTypeProject, then load project.
  443. func (c *Comment) LoadProject() error {
  444. if c.OldProjectID > 0 {
  445. var oldProject project_model.Project
  446. has, err := db.GetEngine(db.DefaultContext).ID(c.OldProjectID).Get(&oldProject)
  447. if err != nil {
  448. return err
  449. } else if has {
  450. c.OldProject = &oldProject
  451. }
  452. }
  453. if c.ProjectID > 0 {
  454. var project project_model.Project
  455. has, err := db.GetEngine(db.DefaultContext).ID(c.ProjectID).Get(&project)
  456. if err != nil {
  457. return err
  458. } else if has {
  459. c.Project = &project
  460. }
  461. }
  462. return nil
  463. }
  464. // LoadMilestone if comment.Type is CommentTypeMilestone, then load milestone
  465. func (c *Comment) LoadMilestone(ctx context.Context) error {
  466. if c.OldMilestoneID > 0 {
  467. var oldMilestone Milestone
  468. has, err := db.GetEngine(ctx).ID(c.OldMilestoneID).Get(&oldMilestone)
  469. if err != nil {
  470. return err
  471. } else if has {
  472. c.OldMilestone = &oldMilestone
  473. }
  474. }
  475. if c.MilestoneID > 0 {
  476. var milestone Milestone
  477. has, err := db.GetEngine(ctx).ID(c.MilestoneID).Get(&milestone)
  478. if err != nil {
  479. return err
  480. } else if has {
  481. c.Milestone = &milestone
  482. }
  483. }
  484. return nil
  485. }
  486. // LoadAttachments loads attachments (it never returns error, the error during `GetAttachmentsByCommentIDCtx` is ignored)
  487. func (c *Comment) LoadAttachments(ctx context.Context) error {
  488. if len(c.Attachments) > 0 {
  489. return nil
  490. }
  491. var err error
  492. c.Attachments, err = repo_model.GetAttachmentsByCommentID(ctx, c.ID)
  493. if err != nil {
  494. log.Error("getAttachmentsByCommentID[%d]: %v", c.ID, err)
  495. }
  496. return nil
  497. }
  498. // UpdateAttachments update attachments by UUIDs for the comment
  499. func (c *Comment) UpdateAttachments(uuids []string) error {
  500. ctx, committer, err := db.TxContext(db.DefaultContext)
  501. if err != nil {
  502. return err
  503. }
  504. defer committer.Close()
  505. attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, uuids)
  506. if err != nil {
  507. return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", uuids, err)
  508. }
  509. for i := 0; i < len(attachments); i++ {
  510. attachments[i].IssueID = c.IssueID
  511. attachments[i].CommentID = c.ID
  512. if err := repo_model.UpdateAttachment(ctx, attachments[i]); err != nil {
  513. return fmt.Errorf("update attachment [id: %d]: %w", attachments[i].ID, err)
  514. }
  515. }
  516. return committer.Commit()
  517. }
  518. // LoadAssigneeUserAndTeam if comment.Type is CommentTypeAssignees, then load assignees
  519. func (c *Comment) LoadAssigneeUserAndTeam() error {
  520. var err error
  521. if c.AssigneeID > 0 && c.Assignee == nil {
  522. c.Assignee, err = user_model.GetUserByID(db.DefaultContext, c.AssigneeID)
  523. if err != nil {
  524. if !user_model.IsErrUserNotExist(err) {
  525. return err
  526. }
  527. c.Assignee = user_model.NewGhostUser()
  528. }
  529. } else if c.AssigneeTeamID > 0 && c.AssigneeTeam == nil {
  530. if err = c.LoadIssue(db.DefaultContext); err != nil {
  531. return err
  532. }
  533. if err = c.Issue.LoadRepo(db.DefaultContext); err != nil {
  534. return err
  535. }
  536. if err = c.Issue.Repo.GetOwner(db.DefaultContext); err != nil {
  537. return err
  538. }
  539. if c.Issue.Repo.Owner.IsOrganization() {
  540. c.AssigneeTeam, err = organization.GetTeamByID(db.DefaultContext, c.AssigneeTeamID)
  541. if err != nil && !organization.IsErrTeamNotExist(err) {
  542. return err
  543. }
  544. }
  545. }
  546. return nil
  547. }
  548. // LoadResolveDoer if comment.Type is CommentTypeCode and ResolveDoerID not zero, then load resolveDoer
  549. func (c *Comment) LoadResolveDoer() (err error) {
  550. if c.ResolveDoerID == 0 || c.Type != CommentTypeCode {
  551. return nil
  552. }
  553. c.ResolveDoer, err = user_model.GetUserByID(db.DefaultContext, c.ResolveDoerID)
  554. if err != nil {
  555. if user_model.IsErrUserNotExist(err) {
  556. c.ResolveDoer = user_model.NewGhostUser()
  557. err = nil
  558. }
  559. }
  560. return err
  561. }
  562. // IsResolved check if an code comment is resolved
  563. func (c *Comment) IsResolved() bool {
  564. return c.ResolveDoerID != 0 && c.Type == CommentTypeCode
  565. }
  566. // LoadDepIssueDetails loads Dependent Issue Details
  567. func (c *Comment) LoadDepIssueDetails() (err error) {
  568. if c.DependentIssueID <= 0 || c.DependentIssue != nil {
  569. return nil
  570. }
  571. c.DependentIssue, err = GetIssueByID(db.DefaultContext, c.DependentIssueID)
  572. return err
  573. }
  574. // LoadTime loads the associated time for a CommentTypeAddTimeManual
  575. func (c *Comment) LoadTime() error {
  576. if c.Time != nil || c.TimeID == 0 {
  577. return nil
  578. }
  579. var err error
  580. c.Time, err = GetTrackedTimeByID(c.TimeID)
  581. return err
  582. }
  583. func (c *Comment) loadReactions(ctx context.Context, repo *repo_model.Repository) (err error) {
  584. if c.Reactions != nil {
  585. return nil
  586. }
  587. c.Reactions, _, err = FindReactions(ctx, FindReactionsOptions{
  588. IssueID: c.IssueID,
  589. CommentID: c.ID,
  590. })
  591. if err != nil {
  592. return err
  593. }
  594. // Load reaction user data
  595. if _, err := c.Reactions.LoadUsers(ctx, repo); err != nil {
  596. return err
  597. }
  598. return nil
  599. }
  600. // LoadReactions loads comment reactions
  601. func (c *Comment) LoadReactions(repo *repo_model.Repository) error {
  602. return c.loadReactions(db.DefaultContext, repo)
  603. }
  604. func (c *Comment) loadReview(ctx context.Context) (err error) {
  605. if c.Review == nil {
  606. if c.Review, err = GetReviewByID(ctx, c.ReviewID); err != nil {
  607. return err
  608. }
  609. }
  610. c.Review.Issue = c.Issue
  611. return nil
  612. }
  613. // LoadReview loads the associated review
  614. func (c *Comment) LoadReview() error {
  615. return c.loadReview(db.DefaultContext)
  616. }
  617. // DiffSide returns "previous" if Comment.Line is a LOC of the previous changes and "proposed" if it is a LOC of the proposed changes.
  618. func (c *Comment) DiffSide() string {
  619. if c.Line < 0 {
  620. return "previous"
  621. }
  622. return "proposed"
  623. }
  624. // UnsignedLine returns the LOC of the code comment without + or -
  625. func (c *Comment) UnsignedLine() uint64 {
  626. if c.Line < 0 {
  627. return uint64(c.Line * -1)
  628. }
  629. return uint64(c.Line)
  630. }
  631. // CodeCommentURL returns the url to a comment in code
  632. func (c *Comment) CodeCommentURL() string {
  633. err := c.LoadIssue(db.DefaultContext)
  634. if err != nil { // Silently dropping errors :unamused:
  635. log.Error("LoadIssue(%d): %v", c.IssueID, err)
  636. return ""
  637. }
  638. err = c.Issue.LoadRepo(db.DefaultContext)
  639. if err != nil { // Silently dropping errors :unamused:
  640. log.Error("loadRepo(%d): %v", c.Issue.RepoID, err)
  641. return ""
  642. }
  643. return fmt.Sprintf("%s/files#%s", c.Issue.HTMLURL(), c.HashTag())
  644. }
  645. // LoadPushCommits Load push commits
  646. func (c *Comment) LoadPushCommits(ctx context.Context) (err error) {
  647. if c.Content == "" || c.Commits != nil || c.Type != CommentTypePullRequestPush {
  648. return nil
  649. }
  650. var data PushActionContent
  651. err = json.Unmarshal([]byte(c.Content), &data)
  652. if err != nil {
  653. return
  654. }
  655. c.IsForcePush = data.IsForcePush
  656. if c.IsForcePush {
  657. if len(data.CommitIDs) != 2 {
  658. return nil
  659. }
  660. c.OldCommit = data.CommitIDs[0]
  661. c.NewCommit = data.CommitIDs[1]
  662. } else {
  663. repoPath := c.Issue.Repo.RepoPath()
  664. gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, repoPath)
  665. if err != nil {
  666. return err
  667. }
  668. defer closer.Close()
  669. c.Commits = git_model.ConvertFromGitCommit(ctx, gitRepo.GetCommitsFromIDs(data.CommitIDs), c.Issue.Repo)
  670. c.CommitsNum = int64(len(c.Commits))
  671. }
  672. return err
  673. }
  674. // CreateComment creates comment with context
  675. func CreateComment(ctx context.Context, opts *CreateCommentOptions) (_ *Comment, err error) {
  676. e := db.GetEngine(ctx)
  677. var LabelID int64
  678. if opts.Label != nil {
  679. LabelID = opts.Label.ID
  680. }
  681. comment := &Comment{
  682. Type: opts.Type,
  683. PosterID: opts.Doer.ID,
  684. Poster: opts.Doer,
  685. IssueID: opts.Issue.ID,
  686. LabelID: LabelID,
  687. OldMilestoneID: opts.OldMilestoneID,
  688. MilestoneID: opts.MilestoneID,
  689. OldProjectID: opts.OldProjectID,
  690. ProjectID: opts.ProjectID,
  691. TimeID: opts.TimeID,
  692. RemovedAssignee: opts.RemovedAssignee,
  693. AssigneeID: opts.AssigneeID,
  694. AssigneeTeamID: opts.AssigneeTeamID,
  695. CommitID: opts.CommitID,
  696. CommitSHA: opts.CommitSHA,
  697. Line: opts.LineNum,
  698. Content: opts.Content,
  699. OldTitle: opts.OldTitle,
  700. NewTitle: opts.NewTitle,
  701. OldRef: opts.OldRef,
  702. NewRef: opts.NewRef,
  703. DependentIssueID: opts.DependentIssueID,
  704. TreePath: opts.TreePath,
  705. ReviewID: opts.ReviewID,
  706. Patch: opts.Patch,
  707. RefRepoID: opts.RefRepoID,
  708. RefIssueID: opts.RefIssueID,
  709. RefCommentID: opts.RefCommentID,
  710. RefAction: opts.RefAction,
  711. RefIsPull: opts.RefIsPull,
  712. IsForcePush: opts.IsForcePush,
  713. Invalidated: opts.Invalidated,
  714. }
  715. if _, err = e.Insert(comment); err != nil {
  716. return nil, err
  717. }
  718. if err = opts.Repo.GetOwner(ctx); err != nil {
  719. return nil, err
  720. }
  721. if err = updateCommentInfos(ctx, opts, comment); err != nil {
  722. return nil, err
  723. }
  724. if err = comment.AddCrossReferences(ctx, opts.Doer, false); err != nil {
  725. return nil, err
  726. }
  727. return comment, nil
  728. }
  729. func updateCommentInfos(ctx context.Context, opts *CreateCommentOptions, comment *Comment) (err error) {
  730. // Check comment type.
  731. switch opts.Type {
  732. case CommentTypeCode:
  733. if comment.ReviewID != 0 {
  734. if comment.Review == nil {
  735. if err := comment.loadReview(ctx); err != nil {
  736. return err
  737. }
  738. }
  739. if comment.Review.Type <= ReviewTypePending {
  740. return nil
  741. }
  742. }
  743. fallthrough
  744. case CommentTypeComment:
  745. if _, err = db.Exec(ctx, "UPDATE `issue` SET num_comments=num_comments+1 WHERE id=?", opts.Issue.ID); err != nil {
  746. return err
  747. }
  748. fallthrough
  749. case CommentTypeReview:
  750. // Check attachments
  751. attachments, err := repo_model.GetAttachmentsByUUIDs(ctx, opts.Attachments)
  752. if err != nil {
  753. return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %w", opts.Attachments, err)
  754. }
  755. for i := range attachments {
  756. attachments[i].IssueID = opts.Issue.ID
  757. attachments[i].CommentID = comment.ID
  758. // No assign value could be 0, so ignore AllCols().
  759. if _, err = db.GetEngine(ctx).ID(attachments[i].ID).Update(attachments[i]); err != nil {
  760. return fmt.Errorf("update attachment [%d]: %w", attachments[i].ID, err)
  761. }
  762. }
  763. comment.Attachments = attachments
  764. case CommentTypeReopen, CommentTypeClose:
  765. if err = repo_model.UpdateRepoIssueNumbers(ctx, opts.Issue.RepoID, opts.Issue.IsPull, true); err != nil {
  766. return err
  767. }
  768. }
  769. // update the issue's updated_unix column
  770. return UpdateIssueCols(ctx, opts.Issue, "updated_unix")
  771. }
  772. func createDeadlineComment(ctx context.Context, doer *user_model.User, issue *Issue, newDeadlineUnix timeutil.TimeStamp) (*Comment, error) {
  773. var content string
  774. var commentType CommentType
  775. // newDeadline = 0 means deleting
  776. if newDeadlineUnix == 0 {
  777. commentType = CommentTypeRemovedDeadline
  778. content = issue.DeadlineUnix.Format("2006-01-02")
  779. } else if issue.DeadlineUnix == 0 {
  780. // Check if the new date was added or modified
  781. // If the actual deadline is 0 => deadline added
  782. commentType = CommentTypeAddedDeadline
  783. content = newDeadlineUnix.Format("2006-01-02")
  784. } else { // Otherwise modified
  785. commentType = CommentTypeModifiedDeadline
  786. content = newDeadlineUnix.Format("2006-01-02") + "|" + issue.DeadlineUnix.Format("2006-01-02")
  787. }
  788. if err := issue.LoadRepo(ctx); err != nil {
  789. return nil, err
  790. }
  791. opts := &CreateCommentOptions{
  792. Type: commentType,
  793. Doer: doer,
  794. Repo: issue.Repo,
  795. Issue: issue,
  796. Content: content,
  797. }
  798. comment, err := CreateComment(ctx, opts)
  799. if err != nil {
  800. return nil, err
  801. }
  802. return comment, nil
  803. }
  804. // Creates issue dependency comment
  805. func createIssueDependencyComment(ctx context.Context, doer *user_model.User, issue, dependentIssue *Issue, add bool) (err error) {
  806. cType := CommentTypeAddDependency
  807. if !add {
  808. cType = CommentTypeRemoveDependency
  809. }
  810. if err = issue.LoadRepo(ctx); err != nil {
  811. return
  812. }
  813. // Make two comments, one in each issue
  814. opts := &CreateCommentOptions{
  815. Type: cType,
  816. Doer: doer,
  817. Repo: issue.Repo,
  818. Issue: issue,
  819. DependentIssueID: dependentIssue.ID,
  820. }
  821. if _, err = CreateComment(ctx, opts); err != nil {
  822. return
  823. }
  824. opts = &CreateCommentOptions{
  825. Type: cType,
  826. Doer: doer,
  827. Repo: issue.Repo,
  828. Issue: dependentIssue,
  829. DependentIssueID: issue.ID,
  830. }
  831. _, err = CreateComment(ctx, opts)
  832. return err
  833. }
  834. // CreateCommentOptions defines options for creating comment
  835. type CreateCommentOptions struct {
  836. Type CommentType
  837. Doer *user_model.User
  838. Repo *repo_model.Repository
  839. Issue *Issue
  840. Label *Label
  841. DependentIssueID int64
  842. OldMilestoneID int64
  843. MilestoneID int64
  844. OldProjectID int64
  845. ProjectID int64
  846. TimeID int64
  847. AssigneeID int64
  848. AssigneeTeamID int64
  849. RemovedAssignee bool
  850. OldTitle string
  851. NewTitle string
  852. OldRef string
  853. NewRef string
  854. CommitID int64
  855. CommitSHA string
  856. Patch string
  857. LineNum int64
  858. TreePath string
  859. ReviewID int64
  860. Content string
  861. Attachments []string // UUIDs of attachments
  862. RefRepoID int64
  863. RefIssueID int64
  864. RefCommentID int64
  865. RefAction references.XRefAction
  866. RefIsPull bool
  867. IsForcePush bool
  868. Invalidated bool
  869. }
  870. // GetCommentByID returns the comment by given ID.
  871. func GetCommentByID(ctx context.Context, id int64) (*Comment, error) {
  872. c := new(Comment)
  873. has, err := db.GetEngine(ctx).ID(id).Get(c)
  874. if err != nil {
  875. return nil, err
  876. } else if !has {
  877. return nil, ErrCommentNotExist{id, 0}
  878. }
  879. return c, nil
  880. }
  881. // FindCommentsOptions describes the conditions to Find comments
  882. type FindCommentsOptions struct {
  883. db.ListOptions
  884. RepoID int64
  885. IssueID int64
  886. ReviewID int64
  887. Since int64
  888. Before int64
  889. Line int64
  890. TreePath string
  891. Type CommentType
  892. IssueIDs []int64
  893. Invalidated util.OptionalBool
  894. }
  895. // ToConds implements FindOptions interface
  896. func (opts *FindCommentsOptions) ToConds() builder.Cond {
  897. cond := builder.NewCond()
  898. if opts.RepoID > 0 {
  899. cond = cond.And(builder.Eq{"issue.repo_id": opts.RepoID})
  900. }
  901. if opts.IssueID > 0 {
  902. cond = cond.And(builder.Eq{"comment.issue_id": opts.IssueID})
  903. } else if len(opts.IssueIDs) > 0 {
  904. cond = cond.And(builder.In("comment.issue_id", opts.IssueIDs))
  905. }
  906. if opts.ReviewID > 0 {
  907. cond = cond.And(builder.Eq{"comment.review_id": opts.ReviewID})
  908. }
  909. if opts.Since > 0 {
  910. cond = cond.And(builder.Gte{"comment.updated_unix": opts.Since})
  911. }
  912. if opts.Before > 0 {
  913. cond = cond.And(builder.Lte{"comment.updated_unix": opts.Before})
  914. }
  915. if opts.Type != CommentTypeUnknown {
  916. cond = cond.And(builder.Eq{"comment.type": opts.Type})
  917. }
  918. if opts.Line != 0 {
  919. cond = cond.And(builder.Eq{"comment.line": opts.Line})
  920. }
  921. if len(opts.TreePath) > 0 {
  922. cond = cond.And(builder.Eq{"comment.tree_path": opts.TreePath})
  923. }
  924. if !opts.Invalidated.IsNone() {
  925. cond = cond.And(builder.Eq{"comment.invalidated": opts.Invalidated.IsTrue()})
  926. }
  927. return cond
  928. }
  929. // FindComments returns all comments according options
  930. func FindComments(ctx context.Context, opts *FindCommentsOptions) ([]*Comment, error) {
  931. comments := make([]*Comment, 0, 10)
  932. sess := db.GetEngine(ctx).Where(opts.ToConds())
  933. if opts.RepoID > 0 {
  934. sess.Join("INNER", "issue", "issue.id = comment.issue_id")
  935. }
  936. if opts.Page != 0 {
  937. sess = db.SetSessionPagination(sess, opts)
  938. }
  939. // WARNING: If you change this order you will need to fix createCodeComment
  940. return comments, sess.
  941. Asc("comment.created_unix").
  942. Asc("comment.id").
  943. Find(&comments)
  944. }
  945. // CountComments count all comments according options by ignoring pagination
  946. func CountComments(opts *FindCommentsOptions) (int64, error) {
  947. sess := db.GetEngine(db.DefaultContext).Where(opts.ToConds())
  948. if opts.RepoID > 0 {
  949. sess.Join("INNER", "issue", "issue.id = comment.issue_id")
  950. }
  951. return sess.Count(&Comment{})
  952. }
  953. // UpdateCommentInvalidate updates comment invalidated column
  954. func UpdateCommentInvalidate(ctx context.Context, c *Comment) error {
  955. _, err := db.GetEngine(ctx).ID(c.ID).Cols("invalidated").Update(c)
  956. return err
  957. }
  958. // UpdateComment updates information of comment.
  959. func UpdateComment(c *Comment, doer *user_model.User) error {
  960. ctx, committer, err := db.TxContext(db.DefaultContext)
  961. if err != nil {
  962. return err
  963. }
  964. defer committer.Close()
  965. sess := db.GetEngine(ctx)
  966. if _, err := sess.ID(c.ID).AllCols().Update(c); err != nil {
  967. return err
  968. }
  969. if err := c.LoadIssue(ctx); err != nil {
  970. return err
  971. }
  972. if err := c.AddCrossReferences(ctx, doer, true); err != nil {
  973. return err
  974. }
  975. if err := committer.Commit(); err != nil {
  976. return fmt.Errorf("Commit: %w", err)
  977. }
  978. return nil
  979. }
  980. // DeleteComment deletes the comment
  981. func DeleteComment(ctx context.Context, comment *Comment) error {
  982. e := db.GetEngine(ctx)
  983. if _, err := e.ID(comment.ID).NoAutoCondition().Delete(comment); err != nil {
  984. return err
  985. }
  986. if _, err := db.DeleteByBean(ctx, &ContentHistory{
  987. CommentID: comment.ID,
  988. }); err != nil {
  989. return err
  990. }
  991. if comment.Type == CommentTypeComment {
  992. if _, err := e.ID(comment.IssueID).Decr("num_comments").Update(new(Issue)); err != nil {
  993. return err
  994. }
  995. }
  996. if _, err := e.Table("action").
  997. Where("comment_id = ?", comment.ID).
  998. Update(map[string]interface{}{
  999. "is_deleted": true,
  1000. }); err != nil {
  1001. return err
  1002. }
  1003. if err := comment.neuterCrossReferences(ctx); err != nil {
  1004. return err
  1005. }
  1006. return DeleteReaction(ctx, &ReactionOptions{CommentID: comment.ID})
  1007. }
  1008. // UpdateCommentsMigrationsByType updates comments' migrations information via given git service type and original id and poster id
  1009. func UpdateCommentsMigrationsByType(tp structs.GitServiceType, originalAuthorID string, posterID int64) error {
  1010. _, err := db.GetEngine(db.DefaultContext).Table("comment").
  1011. Where(builder.In("issue_id",
  1012. builder.Select("issue.id").
  1013. From("issue").
  1014. InnerJoin("repository", "issue.repo_id = repository.id").
  1015. Where(builder.Eq{
  1016. "repository.original_service_type": tp,
  1017. }),
  1018. )).
  1019. And("comment.original_author_id = ?", originalAuthorID).
  1020. Update(map[string]interface{}{
  1021. "poster_id": posterID,
  1022. "original_author": "",
  1023. "original_author_id": 0,
  1024. })
  1025. return err
  1026. }
  1027. // CreateAutoMergeComment is a internal function, only use it for CommentTypePRScheduledToAutoMerge and CommentTypePRUnScheduledToAutoMerge CommentTypes
  1028. func CreateAutoMergeComment(ctx context.Context, typ CommentType, pr *PullRequest, doer *user_model.User) (comment *Comment, err error) {
  1029. if typ != CommentTypePRScheduledToAutoMerge && typ != CommentTypePRUnScheduledToAutoMerge {
  1030. return nil, fmt.Errorf("comment type %d cannot be used to create an auto merge comment", typ)
  1031. }
  1032. if err = pr.LoadIssue(ctx); err != nil {
  1033. return
  1034. }
  1035. if err = pr.LoadBaseRepo(ctx); err != nil {
  1036. return
  1037. }
  1038. comment, err = CreateComment(ctx, &CreateCommentOptions{
  1039. Type: typ,
  1040. Doer: doer,
  1041. Repo: pr.BaseRepo,
  1042. Issue: pr.Issue,
  1043. })
  1044. return comment, err
  1045. }
  1046. // RemapExternalUser ExternalUserRemappable interface
  1047. func (c *Comment) RemapExternalUser(externalName string, externalID, userID int64) error {
  1048. c.OriginalAuthor = externalName
  1049. c.OriginalAuthorID = externalID
  1050. c.PosterID = userID
  1051. return nil
  1052. }
  1053. // GetUserID ExternalUserRemappable interface
  1054. func (c *Comment) GetUserID() int64 { return c.PosterID }
  1055. // GetExternalName ExternalUserRemappable interface
  1056. func (c *Comment) GetExternalName() string { return c.OriginalAuthor }
  1057. // GetExternalID ExternalUserRemappable interface
  1058. func (c *Comment) GetExternalID() int64 { return c.OriginalAuthorID }
  1059. // CountCommentTypeLabelWithEmptyLabel count label comments with empty label
  1060. func CountCommentTypeLabelWithEmptyLabel(ctx context.Context) (int64, error) {
  1061. return db.GetEngine(ctx).Where(builder.Eq{"type": CommentTypeLabel, "label_id": 0}).Count(new(Comment))
  1062. }
  1063. // FixCommentTypeLabelWithEmptyLabel count label comments with empty label
  1064. func FixCommentTypeLabelWithEmptyLabel(ctx context.Context) (int64, error) {
  1065. return db.GetEngine(ctx).Where(builder.Eq{"type": CommentTypeLabel, "label_id": 0}).Delete(new(Comment))
  1066. }
  1067. // CountCommentTypeLabelWithOutsideLabels count label comments with outside label
  1068. func CountCommentTypeLabelWithOutsideLabels(ctx context.Context) (int64, error) {
  1069. return db.GetEngine(ctx).Where("comment.type = ? AND ((label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id))", CommentTypeLabel).
  1070. Table("comment").
  1071. Join("inner", "label", "label.id = comment.label_id").
  1072. Join("inner", "issue", "issue.id = comment.issue_id ").
  1073. Join("inner", "repository", "issue.repo_id = repository.id").
  1074. Count()
  1075. }
  1076. // FixCommentTypeLabelWithOutsideLabels count label comments with outside label
  1077. func FixCommentTypeLabelWithOutsideLabels(ctx context.Context) (int64, error) {
  1078. res, err := db.GetEngine(ctx).Exec(`DELETE FROM comment WHERE comment.id IN (
  1079. SELECT il_too.id FROM (
  1080. SELECT com.id
  1081. FROM comment AS com
  1082. INNER JOIN label ON com.label_id = label.id
  1083. INNER JOIN issue on issue.id = com.issue_id
  1084. INNER JOIN repository ON issue.repo_id = repository.id
  1085. WHERE
  1086. com.type = ? AND ((label.org_id = 0 AND issue.repo_id != label.repo_id) OR (label.repo_id = 0 AND label.org_id != repository.owner_id))
  1087. ) AS il_too)`, CommentTypeLabel)
  1088. if err != nil {
  1089. return 0, err
  1090. }
  1091. return res.RowsAffected()
  1092. }