Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

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