You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

issue_comment.go 40KB

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