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

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