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

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