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

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