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

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378
  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. "fmt"
  9. "regexp"
  10. "strconv"
  11. "strings"
  12. "unicode/utf8"
  13. "code.gitea.io/gitea/modules/git"
  14. "code.gitea.io/gitea/modules/json"
  15. "code.gitea.io/gitea/modules/log"
  16. "code.gitea.io/gitea/modules/markup"
  17. "code.gitea.io/gitea/modules/markup/markdown"
  18. "code.gitea.io/gitea/modules/references"
  19. "code.gitea.io/gitea/modules/structs"
  20. "code.gitea.io/gitea/modules/timeutil"
  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 happens if RefIssueID resolves
  168. RefIsPull bool
  169. RefRepo *Repository `xorm:"-"`
  170. RefIssue *Issue `xorm:"-"`
  171. RefComment *Comment `xorm:"-"`
  172. Commits []*SignCommitWithStatuses `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. err = json.Unmarshal([]byte(c.Content), &data)
  581. if err != nil {
  582. return
  583. }
  584. c.IsForcePush = data.IsForcePush
  585. if c.IsForcePush {
  586. if len(data.CommitIDs) != 2 {
  587. return nil
  588. }
  589. c.OldCommit = data.CommitIDs[0]
  590. c.NewCommit = data.CommitIDs[1]
  591. } else {
  592. repoPath := c.Issue.Repo.RepoPath()
  593. gitRepo, err := git.OpenRepository(repoPath)
  594. if err != nil {
  595. return err
  596. }
  597. defer gitRepo.Close()
  598. c.Commits = ConvertFromGitCommit(gitRepo.GetCommitsFromIDs(data.CommitIDs), c.Issue.Repo)
  599. c.CommitsNum = int64(len(c.Commits))
  600. }
  601. return err
  602. }
  603. func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err error) {
  604. var LabelID int64
  605. if opts.Label != nil {
  606. LabelID = opts.Label.ID
  607. }
  608. comment := &Comment{
  609. Type: opts.Type,
  610. PosterID: opts.Doer.ID,
  611. Poster: opts.Doer,
  612. IssueID: opts.Issue.ID,
  613. LabelID: LabelID,
  614. OldMilestoneID: opts.OldMilestoneID,
  615. MilestoneID: opts.MilestoneID,
  616. OldProjectID: opts.OldProjectID,
  617. ProjectID: opts.ProjectID,
  618. TimeID: opts.TimeID,
  619. RemovedAssignee: opts.RemovedAssignee,
  620. AssigneeID: opts.AssigneeID,
  621. AssigneeTeamID: opts.AssigneeTeamID,
  622. CommitID: opts.CommitID,
  623. CommitSHA: opts.CommitSHA,
  624. Line: opts.LineNum,
  625. Content: opts.Content,
  626. OldTitle: opts.OldTitle,
  627. NewTitle: opts.NewTitle,
  628. OldRef: opts.OldRef,
  629. NewRef: opts.NewRef,
  630. DependentIssueID: opts.DependentIssueID,
  631. TreePath: opts.TreePath,
  632. ReviewID: opts.ReviewID,
  633. Patch: opts.Patch,
  634. RefRepoID: opts.RefRepoID,
  635. RefIssueID: opts.RefIssueID,
  636. RefCommentID: opts.RefCommentID,
  637. RefAction: opts.RefAction,
  638. RefIsPull: opts.RefIsPull,
  639. IsForcePush: opts.IsForcePush,
  640. Invalidated: opts.Invalidated,
  641. }
  642. if _, err = e.Insert(comment); err != nil {
  643. return nil, err
  644. }
  645. if err = opts.Repo.getOwner(e); err != nil {
  646. return nil, err
  647. }
  648. if err = updateCommentInfos(e, opts, comment); err != nil {
  649. return nil, err
  650. }
  651. if err = comment.addCrossReferences(e, opts.Doer, false); err != nil {
  652. return nil, err
  653. }
  654. return comment, nil
  655. }
  656. func updateCommentInfos(e *xorm.Session, opts *CreateCommentOptions, comment *Comment) (err error) {
  657. // Check comment type.
  658. switch opts.Type {
  659. case CommentTypeCode:
  660. if comment.ReviewID != 0 {
  661. if comment.Review == nil {
  662. if err := comment.loadReview(e); err != nil {
  663. return err
  664. }
  665. }
  666. if comment.Review.Type <= ReviewTypePending {
  667. return nil
  668. }
  669. }
  670. fallthrough
  671. case CommentTypeReview:
  672. fallthrough
  673. case CommentTypeComment:
  674. if _, err = e.Exec("UPDATE `issue` SET num_comments=num_comments+1 WHERE id=?", opts.Issue.ID); err != nil {
  675. return err
  676. }
  677. // Check attachments
  678. attachments, err := getAttachmentsByUUIDs(e, opts.Attachments)
  679. if err != nil {
  680. return fmt.Errorf("getAttachmentsByUUIDs [uuids: %v]: %v", opts.Attachments, err)
  681. }
  682. for i := range attachments {
  683. attachments[i].IssueID = opts.Issue.ID
  684. attachments[i].CommentID = comment.ID
  685. // No assign value could be 0, so ignore AllCols().
  686. if _, err = e.ID(attachments[i].ID).Update(attachments[i]); err != nil {
  687. return fmt.Errorf("update attachment [%d]: %v", attachments[i].ID, err)
  688. }
  689. }
  690. case CommentTypeReopen, CommentTypeClose:
  691. if err = opts.Issue.updateClosedNum(e); err != nil {
  692. return err
  693. }
  694. }
  695. // update the issue's updated_unix column
  696. return updateIssueCols(e, opts.Issue, "updated_unix")
  697. }
  698. func createDeadlineComment(e *xorm.Session, doer *User, issue *Issue, newDeadlineUnix timeutil.TimeStamp) (*Comment, error) {
  699. var content string
  700. var commentType CommentType
  701. // newDeadline = 0 means deleting
  702. if newDeadlineUnix == 0 {
  703. commentType = CommentTypeRemovedDeadline
  704. content = issue.DeadlineUnix.Format("2006-01-02")
  705. } else if issue.DeadlineUnix == 0 {
  706. // Check if the new date was added or modified
  707. // If the actual deadline is 0 => deadline added
  708. commentType = CommentTypeAddedDeadline
  709. content = newDeadlineUnix.Format("2006-01-02")
  710. } else { // Otherwise modified
  711. commentType = CommentTypeModifiedDeadline
  712. content = newDeadlineUnix.Format("2006-01-02") + "|" + issue.DeadlineUnix.Format("2006-01-02")
  713. }
  714. if err := issue.loadRepo(e); err != nil {
  715. return nil, err
  716. }
  717. opts := &CreateCommentOptions{
  718. Type: commentType,
  719. Doer: doer,
  720. Repo: issue.Repo,
  721. Issue: issue,
  722. Content: content,
  723. }
  724. comment, err := createComment(e, opts)
  725. if err != nil {
  726. return nil, err
  727. }
  728. return comment, nil
  729. }
  730. // Creates issue dependency comment
  731. func createIssueDependencyComment(e *xorm.Session, doer *User, issue, dependentIssue *Issue, add bool) (err error) {
  732. cType := CommentTypeAddDependency
  733. if !add {
  734. cType = CommentTypeRemoveDependency
  735. }
  736. if err = issue.loadRepo(e); err != nil {
  737. return
  738. }
  739. // Make two comments, one in each issue
  740. opts := &CreateCommentOptions{
  741. Type: cType,
  742. Doer: doer,
  743. Repo: issue.Repo,
  744. Issue: issue,
  745. DependentIssueID: dependentIssue.ID,
  746. }
  747. if _, err = createComment(e, opts); err != nil {
  748. return
  749. }
  750. opts = &CreateCommentOptions{
  751. Type: cType,
  752. Doer: doer,
  753. Repo: issue.Repo,
  754. Issue: dependentIssue,
  755. DependentIssueID: issue.ID,
  756. }
  757. _, err = createComment(e, opts)
  758. return
  759. }
  760. // CreateCommentOptions defines options for creating comment
  761. type CreateCommentOptions struct {
  762. Type CommentType
  763. Doer *User
  764. Repo *Repository
  765. Issue *Issue
  766. Label *Label
  767. DependentIssueID int64
  768. OldMilestoneID int64
  769. MilestoneID int64
  770. OldProjectID int64
  771. ProjectID int64
  772. TimeID int64
  773. AssigneeID int64
  774. AssigneeTeamID int64
  775. RemovedAssignee bool
  776. OldTitle string
  777. NewTitle string
  778. OldRef string
  779. NewRef string
  780. CommitID int64
  781. CommitSHA string
  782. Patch string
  783. LineNum int64
  784. TreePath string
  785. ReviewID int64
  786. Content string
  787. Attachments []string // UUIDs of attachments
  788. RefRepoID int64
  789. RefIssueID int64
  790. RefCommentID int64
  791. RefAction references.XRefAction
  792. RefIsPull bool
  793. IsForcePush bool
  794. Invalidated bool
  795. }
  796. // CreateComment creates comment of issue or commit.
  797. func CreateComment(opts *CreateCommentOptions) (comment *Comment, err error) {
  798. sess := x.NewSession()
  799. defer sess.Close()
  800. if err = sess.Begin(); err != nil {
  801. return nil, err
  802. }
  803. comment, err = createComment(sess, opts)
  804. if err != nil {
  805. return nil, err
  806. }
  807. if err = sess.Commit(); err != nil {
  808. return nil, err
  809. }
  810. return comment, nil
  811. }
  812. // CreateRefComment creates a commit reference comment to issue.
  813. func CreateRefComment(doer *User, repo *Repository, issue *Issue, content, commitSHA string) error {
  814. if len(commitSHA) == 0 {
  815. return fmt.Errorf("cannot create reference with empty commit SHA")
  816. }
  817. // Check if same reference from same commit has already existed.
  818. has, err := x.Get(&Comment{
  819. Type: CommentTypeCommitRef,
  820. IssueID: issue.ID,
  821. CommitSHA: commitSHA,
  822. })
  823. if err != nil {
  824. return fmt.Errorf("check reference comment: %v", err)
  825. } else if has {
  826. return nil
  827. }
  828. _, err = CreateComment(&CreateCommentOptions{
  829. Type: CommentTypeCommitRef,
  830. Doer: doer,
  831. Repo: repo,
  832. Issue: issue,
  833. CommitSHA: commitSHA,
  834. Content: content,
  835. })
  836. return err
  837. }
  838. // GetCommentByID returns the comment by given ID.
  839. func GetCommentByID(id int64) (*Comment, error) {
  840. return getCommentByID(x, id)
  841. }
  842. func getCommentByID(e Engine, id int64) (*Comment, error) {
  843. c := new(Comment)
  844. has, err := e.ID(id).Get(c)
  845. if err != nil {
  846. return nil, err
  847. } else if !has {
  848. return nil, ErrCommentNotExist{id, 0}
  849. }
  850. return c, nil
  851. }
  852. // FindCommentsOptions describes the conditions to Find comments
  853. type FindCommentsOptions struct {
  854. ListOptions
  855. RepoID int64
  856. IssueID int64
  857. ReviewID int64
  858. Since int64
  859. Before int64
  860. Line int64
  861. TreePath string
  862. Type CommentType
  863. }
  864. func (opts *FindCommentsOptions) toConds() builder.Cond {
  865. cond := builder.NewCond()
  866. if opts.RepoID > 0 {
  867. cond = cond.And(builder.Eq{"issue.repo_id": opts.RepoID})
  868. }
  869. if opts.IssueID > 0 {
  870. cond = cond.And(builder.Eq{"comment.issue_id": opts.IssueID})
  871. }
  872. if opts.ReviewID > 0 {
  873. cond = cond.And(builder.Eq{"comment.review_id": opts.ReviewID})
  874. }
  875. if opts.Since > 0 {
  876. cond = cond.And(builder.Gte{"comment.updated_unix": opts.Since})
  877. }
  878. if opts.Before > 0 {
  879. cond = cond.And(builder.Lte{"comment.updated_unix": opts.Before})
  880. }
  881. if opts.Type != CommentTypeUnknown {
  882. cond = cond.And(builder.Eq{"comment.type": opts.Type})
  883. }
  884. if opts.Line != 0 {
  885. cond = cond.And(builder.Eq{"comment.line": opts.Line})
  886. }
  887. if len(opts.TreePath) > 0 {
  888. cond = cond.And(builder.Eq{"comment.tree_path": opts.TreePath})
  889. }
  890. return cond
  891. }
  892. func findComments(e Engine, opts *FindCommentsOptions) ([]*Comment, error) {
  893. comments := make([]*Comment, 0, 10)
  894. sess := e.Where(opts.toConds())
  895. if opts.RepoID > 0 {
  896. sess.Join("INNER", "issue", "issue.id = comment.issue_id")
  897. }
  898. if opts.Page != 0 {
  899. sess = opts.setSessionPagination(sess)
  900. }
  901. // WARNING: If you change this order you will need to fix createCodeComment
  902. return comments, sess.
  903. Asc("comment.created_unix").
  904. Asc("comment.id").
  905. Find(&comments)
  906. }
  907. // FindComments returns all comments according options
  908. func FindComments(opts *FindCommentsOptions) ([]*Comment, error) {
  909. return findComments(x, opts)
  910. }
  911. // CountComments count all comments according options by ignoring pagination
  912. func CountComments(opts *FindCommentsOptions) (int64, error) {
  913. sess := x.Where(opts.toConds())
  914. if opts.RepoID > 0 {
  915. sess.Join("INNER", "issue", "issue.id = comment.issue_id")
  916. }
  917. return sess.Count(&Comment{})
  918. }
  919. // UpdateComment updates information of comment.
  920. func UpdateComment(c *Comment, doer *User) error {
  921. sess := x.NewSession()
  922. defer sess.Close()
  923. if err := sess.Begin(); err != nil {
  924. return err
  925. }
  926. if _, err := sess.ID(c.ID).AllCols().Update(c); err != nil {
  927. return err
  928. }
  929. if err := c.loadIssue(sess); err != nil {
  930. return err
  931. }
  932. if err := c.addCrossReferences(sess, doer, true); err != nil {
  933. return err
  934. }
  935. if err := sess.Commit(); err != nil {
  936. return fmt.Errorf("Commit: %v", err)
  937. }
  938. return nil
  939. }
  940. // DeleteComment deletes the comment
  941. func DeleteComment(comment *Comment) error {
  942. sess := x.NewSession()
  943. defer sess.Close()
  944. if err := sess.Begin(); err != nil {
  945. return err
  946. }
  947. if err := deleteComment(sess, comment); err != nil {
  948. return err
  949. }
  950. return sess.Commit()
  951. }
  952. func deleteComment(e Engine, comment *Comment) error {
  953. if _, err := e.Delete(&Comment{
  954. ID: comment.ID,
  955. }); err != nil {
  956. return err
  957. }
  958. if comment.Type == CommentTypeComment {
  959. if _, err := e.Exec("UPDATE `issue` SET num_comments = num_comments - 1 WHERE id = ?", comment.IssueID); err != nil {
  960. return err
  961. }
  962. }
  963. if _, err := e.Where("comment_id = ?", comment.ID).Cols("is_deleted").Update(&Action{IsDeleted: true}); err != nil {
  964. return err
  965. }
  966. if err := comment.neuterCrossReferences(e); err != nil {
  967. return err
  968. }
  969. return deleteReaction(e, &ReactionOptions{Comment: comment})
  970. }
  971. // CodeComments represents comments on code by using this structure: FILENAME -> LINE (+ == proposed; - == previous) -> COMMENTS
  972. type CodeComments map[string]map[int64][]*Comment
  973. func fetchCodeComments(e Engine, issue *Issue, currentUser *User) (CodeComments, error) {
  974. return fetchCodeCommentsByReview(e, issue, currentUser, nil)
  975. }
  976. func fetchCodeCommentsByReview(e Engine, issue *Issue, currentUser *User, review *Review) (CodeComments, error) {
  977. pathToLineToComment := make(CodeComments)
  978. if review == nil {
  979. review = &Review{ID: 0}
  980. }
  981. opts := FindCommentsOptions{
  982. Type: CommentTypeCode,
  983. IssueID: issue.ID,
  984. ReviewID: review.ID,
  985. }
  986. comments, err := findCodeComments(e, opts, issue, currentUser, review)
  987. if err != nil {
  988. return nil, err
  989. }
  990. for _, comment := range comments {
  991. if pathToLineToComment[comment.TreePath] == nil {
  992. pathToLineToComment[comment.TreePath] = make(map[int64][]*Comment)
  993. }
  994. pathToLineToComment[comment.TreePath][comment.Line] = append(pathToLineToComment[comment.TreePath][comment.Line], comment)
  995. }
  996. return pathToLineToComment, nil
  997. }
  998. func findCodeComments(e Engine, opts FindCommentsOptions, issue *Issue, currentUser *User, review *Review) ([]*Comment, error) {
  999. var comments []*Comment
  1000. if review == nil {
  1001. review = &Review{ID: 0}
  1002. }
  1003. conds := opts.toConds()
  1004. if review.ID == 0 {
  1005. conds = conds.And(builder.Eq{"invalidated": false})
  1006. }
  1007. if err := e.Where(conds).
  1008. Asc("comment.created_unix").
  1009. Asc("comment.id").
  1010. Find(&comments); err != nil {
  1011. return nil, err
  1012. }
  1013. if err := issue.loadRepo(e); err != nil {
  1014. return nil, err
  1015. }
  1016. if err := CommentList(comments).loadPosters(e); err != nil {
  1017. return nil, err
  1018. }
  1019. // Find all reviews by ReviewID
  1020. reviews := make(map[int64]*Review)
  1021. ids := make([]int64, 0, len(comments))
  1022. for _, comment := range comments {
  1023. if comment.ReviewID != 0 {
  1024. ids = append(ids, comment.ReviewID)
  1025. }
  1026. }
  1027. if err := e.In("id", ids).Find(&reviews); err != nil {
  1028. return nil, err
  1029. }
  1030. n := 0
  1031. for _, comment := range comments {
  1032. if re, ok := reviews[comment.ReviewID]; ok && re != nil {
  1033. // If the review is pending only the author can see the comments (except if the review is set)
  1034. if review.ID == 0 && re.Type == ReviewTypePending &&
  1035. (currentUser == nil || currentUser.ID != re.ReviewerID) {
  1036. continue
  1037. }
  1038. comment.Review = re
  1039. }
  1040. comments[n] = comment
  1041. n++
  1042. if err := comment.LoadResolveDoer(); err != nil {
  1043. return nil, err
  1044. }
  1045. if err := comment.LoadReactions(issue.Repo); err != nil {
  1046. return nil, err
  1047. }
  1048. var err error
  1049. if comment.RenderedContent, err = markdown.RenderString(&markup.RenderContext{
  1050. URLPrefix: issue.Repo.Link(),
  1051. Metas: issue.Repo.ComposeMetas(),
  1052. }, comment.Content); err != nil {
  1053. return nil, err
  1054. }
  1055. }
  1056. return comments[:n], nil
  1057. }
  1058. // FetchCodeCommentsByLine fetches the code comments for a given treePath and line number
  1059. func FetchCodeCommentsByLine(issue *Issue, currentUser *User, treePath string, line int64) ([]*Comment, error) {
  1060. opts := FindCommentsOptions{
  1061. Type: CommentTypeCode,
  1062. IssueID: issue.ID,
  1063. TreePath: treePath,
  1064. Line: line,
  1065. }
  1066. return findCodeComments(x, opts, issue, currentUser, nil)
  1067. }
  1068. // FetchCodeComments will return a 2d-map: ["Path"]["Line"] = Comments at line
  1069. func FetchCodeComments(issue *Issue, currentUser *User) (CodeComments, error) {
  1070. return fetchCodeComments(x, issue, currentUser)
  1071. }
  1072. // UpdateCommentsMigrationsByType updates comments' migrations information via given git service type and original id and poster id
  1073. func UpdateCommentsMigrationsByType(tp structs.GitServiceType, originalAuthorID string, posterID int64) error {
  1074. _, err := x.Table("comment").
  1075. Where(builder.In("issue_id",
  1076. builder.Select("issue.id").
  1077. From("issue").
  1078. InnerJoin("repository", "issue.repo_id = repository.id").
  1079. Where(builder.Eq{
  1080. "repository.original_service_type": tp,
  1081. }),
  1082. )).
  1083. And("comment.original_author_id = ?", originalAuthorID).
  1084. Update(map[string]interface{}{
  1085. "poster_id": posterID,
  1086. "original_author": "",
  1087. "original_author_id": 0,
  1088. })
  1089. return err
  1090. }
  1091. // CreatePushPullComment create push code to pull base comment
  1092. func CreatePushPullComment(pusher *User, pr *PullRequest, oldCommitID, newCommitID string) (comment *Comment, err error) {
  1093. if pr.HasMerged || oldCommitID == "" || newCommitID == "" {
  1094. return nil, nil
  1095. }
  1096. ops := &CreateCommentOptions{
  1097. Type: CommentTypePullPush,
  1098. Doer: pusher,
  1099. Repo: pr.BaseRepo,
  1100. }
  1101. var data PushActionContent
  1102. data.CommitIDs, data.IsForcePush, err = getCommitIDsFromRepo(pr.BaseRepo, oldCommitID, newCommitID, pr.BaseBranch)
  1103. if err != nil {
  1104. return nil, err
  1105. }
  1106. ops.Issue = pr.Issue
  1107. dataJSON, err := json.Marshal(data)
  1108. if err != nil {
  1109. return nil, err
  1110. }
  1111. ops.Content = string(dataJSON)
  1112. comment, err = CreateComment(ops)
  1113. return
  1114. }
  1115. // getCommitsFromRepo get commit IDs from repo in between oldCommitID and newCommitID
  1116. // isForcePush will be true if oldCommit isn't on the branch
  1117. // Commit on baseBranch will skip
  1118. func getCommitIDsFromRepo(repo *Repository, oldCommitID, newCommitID, baseBranch string) (commitIDs []string, isForcePush bool, err error) {
  1119. repoPath := repo.RepoPath()
  1120. gitRepo, err := git.OpenRepository(repoPath)
  1121. if err != nil {
  1122. return nil, false, err
  1123. }
  1124. defer gitRepo.Close()
  1125. oldCommit, err := gitRepo.GetCommit(oldCommitID)
  1126. if err != nil {
  1127. return nil, false, err
  1128. }
  1129. if err = oldCommit.LoadBranchName(); err != nil {
  1130. return nil, false, err
  1131. }
  1132. if len(oldCommit.Branch) == 0 {
  1133. commitIDs = make([]string, 2)
  1134. commitIDs[0] = oldCommitID
  1135. commitIDs[1] = newCommitID
  1136. return commitIDs, true, err
  1137. }
  1138. newCommit, err := gitRepo.GetCommit(newCommitID)
  1139. if err != nil {
  1140. return nil, false, err
  1141. }
  1142. commits, err := newCommit.CommitsBeforeUntil(oldCommitID)
  1143. if err != nil {
  1144. return nil, false, err
  1145. }
  1146. commitIDs = make([]string, 0, len(commits))
  1147. commitChecks := make(map[string]*commitBranchCheckItem)
  1148. for _, commit := range commits {
  1149. commitChecks[commit.ID.String()] = &commitBranchCheckItem{
  1150. Commit: commit,
  1151. Checked: false,
  1152. }
  1153. }
  1154. if err = commitBranchCheck(gitRepo, newCommit, oldCommitID, baseBranch, commitChecks); err != nil {
  1155. return
  1156. }
  1157. for i := len(commits) - 1; i >= 0; i-- {
  1158. commitID := commits[i].ID.String()
  1159. if item, ok := commitChecks[commitID]; ok && item.Checked {
  1160. commitIDs = append(commitIDs, commitID)
  1161. }
  1162. }
  1163. return
  1164. }
  1165. type commitBranchCheckItem struct {
  1166. Commit *git.Commit
  1167. Checked bool
  1168. }
  1169. func commitBranchCheck(gitRepo *git.Repository, startCommit *git.Commit, endCommitID, baseBranch string, commitList map[string]*commitBranchCheckItem) error {
  1170. if startCommit.ID.String() == endCommitID {
  1171. return nil
  1172. }
  1173. checkStack := make([]string, 0, 10)
  1174. checkStack = append(checkStack, startCommit.ID.String())
  1175. for len(checkStack) > 0 {
  1176. commitID := checkStack[0]
  1177. checkStack = checkStack[1:]
  1178. item, ok := commitList[commitID]
  1179. if !ok {
  1180. continue
  1181. }
  1182. if item.Commit.ID.String() == endCommitID {
  1183. continue
  1184. }
  1185. if err := item.Commit.LoadBranchName(); err != nil {
  1186. return err
  1187. }
  1188. if item.Commit.Branch == baseBranch {
  1189. continue
  1190. }
  1191. if item.Checked {
  1192. continue
  1193. }
  1194. item.Checked = true
  1195. parentNum := item.Commit.ParentCount()
  1196. for i := 0; i < parentNum; i++ {
  1197. parentCommit, err := item.Commit.Parent(i)
  1198. if err != nil {
  1199. return err
  1200. }
  1201. checkStack = append(checkStack, parentCommit.ID.String())
  1202. }
  1203. }
  1204. return nil
  1205. }