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

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