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

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