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

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