gitea源码

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