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

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