You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

issue_comment.go 23 kB

8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
Feature: Timetracking (#2211) * Added comment's hashtag to url for mail notifications. * Added explanation to return statement + documentation. * Replacing in-line link generation with HTMLURL. (+gofmt) * Replaced action-based model with nil-based model. (+gofmt) * Replaced mailIssueActionToParticipants with mailIssueCommentToParticipants. * Updating comment for mailIssueCommentToParticipants * Added link to comment in "Dashboard" * Deleting feed entry if a comment is going to be deleted * Added migration * Added improved migration to add a CommentID column to action. * Added improved links to comments in feed entries. * Fixes #1956 by filtering for deleted comments that are referenced in actions. * Introducing "IsDeleted" column to action. * Adding design draft (not functional) * Adding database models for stopwatches and trackedtimes * See go-gitea/gitea#967 * Adding design draft (not functional) * Adding translations and improving design * Implementing stopwatch (for timetracking) * Make UI functional * Add hints in timeline for time tracking events * Implementing timetracking feature * Adding "Add time manual" option * Improved stopwatch * Created report of total spent time by user * Only showing total time spent if theire is something to show. * Adding license headers. * Improved error handling for "Add Time Manual" * Adding @sapks 's changes, refactoring * Adding API for feature tracking * Adding unit test * Adding DISABLE/ENABLE option to Repository settings page * Improving translations * Applying @sapk 's changes * Removing repo_unit and using IssuesSetting for disabling/enabling timetracker * Adding DEFAULT_ENABLE_TIMETRACKER to config, installation and admin menu * Improving documentation * Fixing vendor/ folder * Changing timtracking routes by adding subgroups /times and /times/stopwatch (Proposed by @lafriks ) * Restricting write access to timetracking based on the repo settings (Proposed by @lafriks ) * Fixed minor permissions bug. * Adding CanUseTimetracker and IsTimetrackerEnabled in ctx.Repo * Allow assignees and authors to track there time too. * Fixed some build-time-errors + logical errors. * Removing unused Get...ByID functions * Moving IsTimetrackerEnabled from context.Repository to models.Repository * Adding a seperate file for issue related repo functions * Adding license headers * Fixed GetUserByParams return 404 * Moving /users/:username/times to /repos/:username/:reponame/times/:username for security reasons * Adding /repos/:username/times to get all tracked times of the repo * Updating sdk-dependency * Updating swagger.v1.json * Adding warning if user has already a running stopwatch (auto-timetracker) * Replacing GetTrackedTimesBy... with GetTrackedTimes(options FindTrackedTimesOptions) * Changing code.gitea.io/sdk back to code.gitea.io/sdk * Correcting spelling mistake * Updating vendor.json * Changing GET stopwatch/toggle to POST stopwatch/toggle * Changing GET stopwatch/cancel to POST stopwatch/cancel * Added migration for stopwatches/timetracking * Fixed some access bugs for read-only users * Added default allow only contributors to track time value to config * Fixed migration by chaging x.Iterate to x.Find * Resorted imports * Moved Add Time Manually form to repo_form.go * Removed "Seconds" field from Add Time Manually * Resorted imports * Improved permission checking * Fixed some bugs * Added integration test * gofmt * Adding integration test by @lafriks * Added created_unix to comment fixtures * Using last event instead of a fixed event * Adding another integration test by @lafriks * Fixing bug Timetracker enabled causing error 500 at sidebar.tpl * Fixed a refactoring bug that resulted in hiding "HasUserStopwatch" warning. * Returning TrackedTime instead of AddTimeOption at AddTime. * Updating SDK from go-gitea/go-sdk#69 * Resetting Go-SDK back to default repository * Fixing test-vendor by changing ini back to original repository * Adding "tags" to swagger spec * govendor sync * Removed duplicate * Formatting templates * Adding IsTimetrackingEnabled checks to API * Improving translations / english texts * Improving documentation * Updating swagger spec * Fixing integration test caused be translation-changes * Removed encoding issues in local_en-US.ini. * "Added" copyright line * Moved unit.IssuesConfig().EnableTimetracker into a != nil check * Removed some other encoding issues in local_en-US.ini * Improved javascript by checking if data-context exists * Replaced manual comment creation with CreateComment * Removed unnecessary code * Improved error checking * Small cosmetic changes * Replaced int>string>duration parsing with int>duration parsing * Fixed encoding issues * Removed unused imports Signed-off-by: Jonas Franz <info@jonasfranz.software>
7 years ago
Issue due date (#3794) * Started adding deadline to ui * Implemented basic issue due date managing * Improved UI for due date managing * Added at least write access to the repo in order to modify issue due dates * Ui improvements * Added issue comments creation when adding/modifying/removing a due date * Show due date in issue list * Added api support for issue due dates * Fixed lint suggestions * Added deadline to sdk * Updated css * Added support for adding/modifiying deadlines for pull requests via api * Fixed comments not created when updating or removing a deadline * update sdk (will do properly once go-gitea/go-sdk#103 is merged) * enhanced updateIssueDeadline * Removed unnessecary Issue.DeadlineString * UI improvements * Small improvments to comment creation + ui & validation improvements * Check if an issue is overdue is now a seperate function * Updated go-sdk with govendor as it was merged * Simplified isOverdue method * removed unessecary deadline to 0 set * Update swagger definitions * Added missing return * Added an explanary comment * Improved updateIssueDeadline method so it'll only update `deadline_unix` * Small changes and improvements * no need to explicitly load the issue when updating a deadline, just use whats already there * small optimisations * Added check if a deadline was modified before updating it * Moved comment creating logic into its own function * Code cleanup for creating deadline comment * locale improvement * When modifying a deadline, the old deadline is saved with the comment * small improvments to xorm session handling when updating an issue deadline + style nitpicks * style nitpicks * Moved checking for if the user has write acces to middleware
7 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
Issue due date (#3794) * Started adding deadline to ui * Implemented basic issue due date managing * Improved UI for due date managing * Added at least write access to the repo in order to modify issue due dates * Ui improvements * Added issue comments creation when adding/modifying/removing a due date * Show due date in issue list * Added api support for issue due dates * Fixed lint suggestions * Added deadline to sdk * Updated css * Added support for adding/modifiying deadlines for pull requests via api * Fixed comments not created when updating or removing a deadline * update sdk (will do properly once go-gitea/go-sdk#103 is merged) * enhanced updateIssueDeadline * Removed unnessecary Issue.DeadlineString * UI improvements * Small improvments to comment creation + ui & validation improvements * Check if an issue is overdue is now a seperate function * Updated go-sdk with govendor as it was merged * Simplified isOverdue method * removed unessecary deadline to 0 set * Update swagger definitions * Added missing return * Added an explanary comment * Improved updateIssueDeadline method so it'll only update `deadline_unix` * Small changes and improvements * no need to explicitly load the issue when updating a deadline, just use whats already there * small optimisations * Added check if a deadline was modified before updating it * Moved comment creating logic into its own function * Code cleanup for creating deadline comment * locale improvement * When modifying a deadline, the old deadline is saved with the comment * small improvments to xorm session handling when updating an issue deadline + style nitpicks * style nitpicks * Moved checking for if the user has write acces to middleware
7 years ago
8 years ago
8 years ago
8 years ago
8 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872
  1. // Copyright 2016 The Gogs Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package models
  5. import (
  6. "fmt"
  7. "strings"
  8. "github.com/Unknwon/com"
  9. "github.com/go-xorm/builder"
  10. "github.com/go-xorm/xorm"
  11. api "code.gitea.io/sdk/gitea"
  12. "code.gitea.io/gitea/modules/log"
  13. "code.gitea.io/gitea/modules/markup"
  14. "code.gitea.io/gitea/modules/util"
  15. )
  16. // CommentType defines whether a comment is just a simple comment, an action (like close) or a reference.
  17. type CommentType int
  18. // define unknown comment type
  19. const (
  20. CommentTypeUnknown CommentType = -1
  21. )
  22. // Enumerate all the comment types
  23. const (
  24. // Plain comment, can be associated with a commit (CommitID > 0) and a line (LineNum > 0)
  25. CommentTypeComment CommentType = iota
  26. CommentTypeReopen
  27. CommentTypeClose
  28. // References.
  29. CommentTypeIssueRef
  30. // Reference from a commit (not part of a pull request)
  31. CommentTypeCommitRef
  32. // Reference from a comment
  33. CommentTypeCommentRef
  34. // Reference from a pull request
  35. CommentTypePullRef
  36. // Labels changed
  37. CommentTypeLabel
  38. // Milestone changed
  39. CommentTypeMilestone
  40. // Assignees changed
  41. CommentTypeAssignees
  42. // Change Title
  43. CommentTypeChangeTitle
  44. // Delete Branch
  45. CommentTypeDeleteBranch
  46. // Start a stopwatch for time tracking
  47. CommentTypeStartTracking
  48. // Stop a stopwatch for time tracking
  49. CommentTypeStopTracking
  50. // Add time manual for time tracking
  51. CommentTypeAddTimeManual
  52. // Cancel a stopwatch for time tracking
  53. CommentTypeCancelTracking
  54. // Added a due date
  55. CommentTypeAddedDeadline
  56. // Modified the due date
  57. CommentTypeModifiedDeadline
  58. // Removed a due date
  59. CommentTypeRemovedDeadline
  60. // Dependency added
  61. CommentTypeAddDependency
  62. //Dependency removed
  63. CommentTypeRemoveDependency
  64. )
  65. // CommentTag defines comment tag type
  66. type CommentTag int
  67. // Enumerate all the comment tag types
  68. const (
  69. CommentTagNone CommentTag = iota
  70. CommentTagPoster
  71. CommentTagWriter
  72. CommentTagOwner
  73. )
  74. // Comment represents a comment in commit and issue page.
  75. type Comment struct {
  76. ID int64 `xorm:"pk autoincr"`
  77. Type CommentType
  78. PosterID int64 `xorm:"INDEX"`
  79. Poster *User `xorm:"-"`
  80. IssueID int64 `xorm:"INDEX"`
  81. Issue *Issue `xorm:"-"`
  82. LabelID int64
  83. Label *Label `xorm:"-"`
  84. OldMilestoneID int64
  85. MilestoneID int64
  86. OldMilestone *Milestone `xorm:"-"`
  87. Milestone *Milestone `xorm:"-"`
  88. AssigneeID int64
  89. RemovedAssignee bool
  90. Assignee *User `xorm:"-"`
  91. OldTitle string
  92. NewTitle string
  93. DependentIssueID int64
  94. DependentIssue *Issue `xorm:"-"`
  95. CommitID int64
  96. Line int64
  97. Content string `xorm:"TEXT"`
  98. RenderedContent string `xorm:"-"`
  99. CreatedUnix util.TimeStamp `xorm:"INDEX created"`
  100. UpdatedUnix util.TimeStamp `xorm:"INDEX updated"`
  101. // Reference issue in commit message
  102. CommitSHA string `xorm:"VARCHAR(40)"`
  103. Attachments []*Attachment `xorm:"-"`
  104. Reactions ReactionList `xorm:"-"`
  105. // For view issue page.
  106. ShowTag CommentTag `xorm:"-"`
  107. }
  108. // LoadIssue loads issue from database
  109. func (c *Comment) LoadIssue() (err error) {
  110. if c.Issue != nil {
  111. return nil
  112. }
  113. c.Issue, err = GetIssueByID(c.IssueID)
  114. return
  115. }
  116. // AfterLoad is invoked from XORM after setting the values of all fields of this object.
  117. func (c *Comment) AfterLoad(session *xorm.Session) {
  118. var err error
  119. c.Attachments, err = getAttachmentsByCommentID(session, c.ID)
  120. if err != nil {
  121. log.Error(3, "getAttachmentsByCommentID[%d]: %v", c.ID, err)
  122. }
  123. c.Poster, err = getUserByID(session, c.PosterID)
  124. if err != nil {
  125. if IsErrUserNotExist(err) {
  126. c.PosterID = -1
  127. c.Poster = NewGhostUser()
  128. } else {
  129. log.Error(3, "getUserByID[%d]: %v", c.ID, err)
  130. }
  131. }
  132. }
  133. // AfterDelete is invoked from XORM after the object is deleted.
  134. func (c *Comment) AfterDelete() {
  135. if c.ID <= 0 {
  136. return
  137. }
  138. _, err := DeleteAttachmentsByComment(c.ID, true)
  139. if err != nil {
  140. log.Info("Could not delete files for comment %d on issue #%d: %s", c.ID, c.IssueID, err)
  141. }
  142. }
  143. // HTMLURL formats a URL-string to the issue-comment
  144. func (c *Comment) HTMLURL() string {
  145. err := c.LoadIssue()
  146. if err != nil { // Silently dropping errors :unamused:
  147. log.Error(4, "LoadIssue(%d): %v", c.IssueID, err)
  148. return ""
  149. }
  150. return fmt.Sprintf("%s#%s", c.Issue.HTMLURL(), c.HashTag())
  151. }
  152. // IssueURL formats a URL-string to the issue
  153. func (c *Comment) IssueURL() string {
  154. err := c.LoadIssue()
  155. if err != nil { // Silently dropping errors :unamused:
  156. log.Error(4, "LoadIssue(%d): %v", c.IssueID, err)
  157. return ""
  158. }
  159. if c.Issue.IsPull {
  160. return ""
  161. }
  162. return c.Issue.HTMLURL()
  163. }
  164. // PRURL formats a URL-string to the pull-request
  165. func (c *Comment) PRURL() string {
  166. err := c.LoadIssue()
  167. if err != nil { // Silently dropping errors :unamused:
  168. log.Error(4, "LoadIssue(%d): %v", c.IssueID, err)
  169. return ""
  170. }
  171. if !c.Issue.IsPull {
  172. return ""
  173. }
  174. return c.Issue.HTMLURL()
  175. }
  176. // APIFormat converts a Comment to the api.Comment format
  177. func (c *Comment) APIFormat() *api.Comment {
  178. return &api.Comment{
  179. ID: c.ID,
  180. Poster: c.Poster.APIFormat(),
  181. HTMLURL: c.HTMLURL(),
  182. IssueURL: c.IssueURL(),
  183. PRURL: c.PRURL(),
  184. Body: c.Content,
  185. Created: c.CreatedUnix.AsTime(),
  186. Updated: c.UpdatedUnix.AsTime(),
  187. }
  188. }
  189. // CommentHashTag returns unique hash tag for comment id.
  190. func CommentHashTag(id int64) string {
  191. return fmt.Sprintf("issuecomment-%d", id)
  192. }
  193. // HashTag returns unique hash tag for comment.
  194. func (c *Comment) HashTag() string {
  195. return CommentHashTag(c.ID)
  196. }
  197. // EventTag returns unique event hash tag for comment.
  198. func (c *Comment) EventTag() string {
  199. return "event-" + com.ToStr(c.ID)
  200. }
  201. // LoadLabel if comment.Type is CommentTypeLabel, then load Label
  202. func (c *Comment) LoadLabel() error {
  203. var label Label
  204. has, err := x.ID(c.LabelID).Get(&label)
  205. if err != nil {
  206. return err
  207. } else if has {
  208. c.Label = &label
  209. } else {
  210. // Ignore Label is deleted, but not clear this table
  211. log.Warn("Commit %d cannot load label %d", c.ID, c.LabelID)
  212. }
  213. return nil
  214. }
  215. // LoadMilestone if comment.Type is CommentTypeMilestone, then load milestone
  216. func (c *Comment) LoadMilestone() error {
  217. if c.OldMilestoneID > 0 {
  218. var oldMilestone Milestone
  219. has, err := x.ID(c.OldMilestoneID).Get(&oldMilestone)
  220. if err != nil {
  221. return err
  222. } else if has {
  223. c.OldMilestone = &oldMilestone
  224. }
  225. }
  226. if c.MilestoneID > 0 {
  227. var milestone Milestone
  228. has, err := x.ID(c.MilestoneID).Get(&milestone)
  229. if err != nil {
  230. return err
  231. } else if has {
  232. c.Milestone = &milestone
  233. }
  234. }
  235. return nil
  236. }
  237. // LoadAssigneeUser if comment.Type is CommentTypeAssignees, then load assignees
  238. func (c *Comment) LoadAssigneeUser() error {
  239. var err error
  240. if c.AssigneeID > 0 {
  241. c.Assignee, err = getUserByID(x, c.AssigneeID)
  242. if err != nil {
  243. if !IsErrUserNotExist(err) {
  244. return err
  245. }
  246. c.Assignee = NewGhostUser()
  247. }
  248. }
  249. return nil
  250. }
  251. // LoadDepIssueDetails loads Dependent Issue Details
  252. func (c *Comment) LoadDepIssueDetails() (err error) {
  253. if c.DependentIssueID <= 0 || c.DependentIssue != nil {
  254. return nil
  255. }
  256. c.DependentIssue, err = getIssueByID(x, c.DependentIssueID)
  257. return err
  258. }
  259. // MailParticipants sends new comment emails to repository watchers
  260. // and mentioned people.
  261. func (c *Comment) MailParticipants(e Engine, opType ActionType, issue *Issue) (err error) {
  262. mentions := markup.FindAllMentions(c.Content)
  263. if err = UpdateIssueMentions(e, c.IssueID, mentions); err != nil {
  264. return fmt.Errorf("UpdateIssueMentions [%d]: %v", c.IssueID, err)
  265. }
  266. content := c.Content
  267. switch opType {
  268. case ActionCloseIssue:
  269. content = fmt.Sprintf("Closed #%d", issue.Index)
  270. case ActionReopenIssue:
  271. content = fmt.Sprintf("Reopened #%d", issue.Index)
  272. }
  273. if err = mailIssueCommentToParticipants(e, issue, c.Poster, content, c, mentions); err != nil {
  274. log.Error(4, "mailIssueCommentToParticipants: %v", err)
  275. }
  276. return nil
  277. }
  278. func (c *Comment) loadReactions(e Engine) (err error) {
  279. if c.Reactions != nil {
  280. return nil
  281. }
  282. c.Reactions, err = findReactions(e, FindReactionsOptions{
  283. IssueID: c.IssueID,
  284. CommentID: c.ID,
  285. })
  286. if err != nil {
  287. return err
  288. }
  289. // Load reaction user data
  290. if _, err := c.Reactions.LoadUsers(); err != nil {
  291. return err
  292. }
  293. return nil
  294. }
  295. // LoadReactions loads comment reactions
  296. func (c *Comment) LoadReactions() error {
  297. return c.loadReactions(x)
  298. }
  299. func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err error) {
  300. var LabelID int64
  301. if opts.Label != nil {
  302. LabelID = opts.Label.ID
  303. }
  304. comment := &Comment{
  305. Type: opts.Type,
  306. PosterID: opts.Doer.ID,
  307. Poster: opts.Doer,
  308. IssueID: opts.Issue.ID,
  309. LabelID: LabelID,
  310. OldMilestoneID: opts.OldMilestoneID,
  311. MilestoneID: opts.MilestoneID,
  312. RemovedAssignee: opts.RemovedAssignee,
  313. AssigneeID: opts.AssigneeID,
  314. CommitID: opts.CommitID,
  315. CommitSHA: opts.CommitSHA,
  316. Line: opts.LineNum,
  317. Content: opts.Content,
  318. OldTitle: opts.OldTitle,
  319. NewTitle: opts.NewTitle,
  320. DependentIssueID: opts.DependentIssueID,
  321. }
  322. if _, err = e.Insert(comment); err != nil {
  323. return nil, err
  324. }
  325. if err = opts.Repo.getOwner(e); err != nil {
  326. return nil, err
  327. }
  328. // Compose comment action, could be plain comment, close or reopen issue/pull request.
  329. // This object will be used to notify watchers in the end of function.
  330. act := &Action{
  331. ActUserID: opts.Doer.ID,
  332. ActUser: opts.Doer,
  333. Content: fmt.Sprintf("%d|%s", opts.Issue.Index, strings.Split(opts.Content, "\n")[0]),
  334. RepoID: opts.Repo.ID,
  335. Repo: opts.Repo,
  336. Comment: comment,
  337. CommentID: comment.ID,
  338. IsPrivate: opts.Repo.IsPrivate,
  339. }
  340. // Check comment type.
  341. switch opts.Type {
  342. case CommentTypeComment:
  343. act.OpType = ActionCommentIssue
  344. if _, err = e.Exec("UPDATE `issue` SET num_comments=num_comments+1 WHERE id=?", opts.Issue.ID); err != nil {
  345. return nil, err
  346. }
  347. // Check attachments
  348. attachments := make([]*Attachment, 0, len(opts.Attachments))
  349. for _, uuid := range opts.Attachments {
  350. attach, err := getAttachmentByUUID(e, uuid)
  351. if err != nil {
  352. if IsErrAttachmentNotExist(err) {
  353. continue
  354. }
  355. return nil, fmt.Errorf("getAttachmentByUUID [%s]: %v", uuid, err)
  356. }
  357. attachments = append(attachments, attach)
  358. }
  359. for i := range attachments {
  360. attachments[i].IssueID = opts.Issue.ID
  361. attachments[i].CommentID = comment.ID
  362. // No assign value could be 0, so ignore AllCols().
  363. if _, err = e.ID(attachments[i].ID).Update(attachments[i]); err != nil {
  364. return nil, fmt.Errorf("update attachment [%d]: %v", attachments[i].ID, err)
  365. }
  366. }
  367. case CommentTypeReopen:
  368. act.OpType = ActionReopenIssue
  369. if opts.Issue.IsPull {
  370. act.OpType = ActionReopenPullRequest
  371. }
  372. if opts.Issue.IsPull {
  373. _, err = e.Exec("UPDATE `repository` SET num_closed_pulls=num_closed_pulls-1 WHERE id=?", opts.Repo.ID)
  374. } else {
  375. _, err = e.Exec("UPDATE `repository` SET num_closed_issues=num_closed_issues-1 WHERE id=?", opts.Repo.ID)
  376. }
  377. if err != nil {
  378. return nil, err
  379. }
  380. case CommentTypeClose:
  381. act.OpType = ActionCloseIssue
  382. if opts.Issue.IsPull {
  383. act.OpType = ActionClosePullRequest
  384. }
  385. if opts.Issue.IsPull {
  386. _, err = e.Exec("UPDATE `repository` SET num_closed_pulls=num_closed_pulls+1 WHERE id=?", opts.Repo.ID)
  387. } else {
  388. _, err = e.Exec("UPDATE `repository` SET num_closed_issues=num_closed_issues+1 WHERE id=?", opts.Repo.ID)
  389. }
  390. if err != nil {
  391. return nil, err
  392. }
  393. }
  394. // update the issue's updated_unix column
  395. if err = updateIssueCols(e, opts.Issue, "updated_unix"); err != nil {
  396. return nil, err
  397. }
  398. // Notify watchers for whatever action comes in, ignore if no action type.
  399. if act.OpType > 0 {
  400. if err = notifyWatchers(e, act); err != nil {
  401. log.Error(4, "notifyWatchers: %v", err)
  402. }
  403. if err = comment.MailParticipants(e, act.OpType, opts.Issue); err != nil {
  404. log.Error(4, "MailParticipants: %v", err)
  405. }
  406. }
  407. return comment, nil
  408. }
  409. func createStatusComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue) (*Comment, error) {
  410. cmtType := CommentTypeClose
  411. if !issue.IsClosed {
  412. cmtType = CommentTypeReopen
  413. }
  414. return createComment(e, &CreateCommentOptions{
  415. Type: cmtType,
  416. Doer: doer,
  417. Repo: repo,
  418. Issue: issue,
  419. })
  420. }
  421. func createLabelComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue, label *Label, add bool) (*Comment, error) {
  422. var content string
  423. if add {
  424. content = "1"
  425. }
  426. return createComment(e, &CreateCommentOptions{
  427. Type: CommentTypeLabel,
  428. Doer: doer,
  429. Repo: repo,
  430. Issue: issue,
  431. Label: label,
  432. Content: content,
  433. })
  434. }
  435. func createMilestoneComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue, oldMilestoneID, milestoneID int64) (*Comment, error) {
  436. return createComment(e, &CreateCommentOptions{
  437. Type: CommentTypeMilestone,
  438. Doer: doer,
  439. Repo: repo,
  440. Issue: issue,
  441. OldMilestoneID: oldMilestoneID,
  442. MilestoneID: milestoneID,
  443. })
  444. }
  445. func createAssigneeComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue, assigneeID int64, removedAssignee bool) (*Comment, error) {
  446. return createComment(e, &CreateCommentOptions{
  447. Type: CommentTypeAssignees,
  448. Doer: doer,
  449. Repo: repo,
  450. Issue: issue,
  451. RemovedAssignee: removedAssignee,
  452. AssigneeID: assigneeID,
  453. })
  454. }
  455. func createDeadlineComment(e *xorm.Session, doer *User, issue *Issue, newDeadlineUnix util.TimeStamp) (*Comment, error) {
  456. var content string
  457. var commentType CommentType
  458. // newDeadline = 0 means deleting
  459. if newDeadlineUnix == 0 {
  460. commentType = CommentTypeRemovedDeadline
  461. content = issue.DeadlineUnix.Format("2006-01-02")
  462. } else if issue.DeadlineUnix == 0 {
  463. // Check if the new date was added or modified
  464. // If the actual deadline is 0 => deadline added
  465. commentType = CommentTypeAddedDeadline
  466. content = newDeadlineUnix.Format("2006-01-02")
  467. } else { // Otherwise modified
  468. commentType = CommentTypeModifiedDeadline
  469. content = newDeadlineUnix.Format("2006-01-02") + "|" + issue.DeadlineUnix.Format("2006-01-02")
  470. }
  471. return createComment(e, &CreateCommentOptions{
  472. Type: commentType,
  473. Doer: doer,
  474. Repo: issue.Repo,
  475. Issue: issue,
  476. Content: content,
  477. })
  478. }
  479. func createChangeTitleComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue, oldTitle, newTitle string) (*Comment, error) {
  480. return createComment(e, &CreateCommentOptions{
  481. Type: CommentTypeChangeTitle,
  482. Doer: doer,
  483. Repo: repo,
  484. Issue: issue,
  485. OldTitle: oldTitle,
  486. NewTitle: newTitle,
  487. })
  488. }
  489. func createDeleteBranchComment(e *xorm.Session, doer *User, repo *Repository, issue *Issue, branchName string) (*Comment, error) {
  490. return createComment(e, &CreateCommentOptions{
  491. Type: CommentTypeDeleteBranch,
  492. Doer: doer,
  493. Repo: repo,
  494. Issue: issue,
  495. CommitSHA: branchName,
  496. })
  497. }
  498. // Creates issue dependency comment
  499. func createIssueDependencyComment(e *xorm.Session, doer *User, issue *Issue, dependentIssue *Issue, add bool) (err error) {
  500. cType := CommentTypeAddDependency
  501. if !add {
  502. cType = CommentTypeRemoveDependency
  503. }
  504. // Make two comments, one in each issue
  505. _, err = createComment(e, &CreateCommentOptions{
  506. Type: cType,
  507. Doer: doer,
  508. Repo: issue.Repo,
  509. Issue: issue,
  510. DependentIssueID: dependentIssue.ID,
  511. })
  512. if err != nil {
  513. return
  514. }
  515. _, err = createComment(e, &CreateCommentOptions{
  516. Type: cType,
  517. Doer: doer,
  518. Repo: issue.Repo,
  519. Issue: dependentIssue,
  520. DependentIssueID: issue.ID,
  521. })
  522. if err != nil {
  523. return
  524. }
  525. return
  526. }
  527. // CreateCommentOptions defines options for creating comment
  528. type CreateCommentOptions struct {
  529. Type CommentType
  530. Doer *User
  531. Repo *Repository
  532. Issue *Issue
  533. Label *Label
  534. DependentIssueID int64
  535. OldMilestoneID int64
  536. MilestoneID int64
  537. AssigneeID int64
  538. RemovedAssignee bool
  539. OldTitle string
  540. NewTitle string
  541. CommitID int64
  542. CommitSHA string
  543. LineNum int64
  544. Content string
  545. Attachments []string // UUIDs of attachments
  546. }
  547. // CreateComment creates comment of issue or commit.
  548. func CreateComment(opts *CreateCommentOptions) (comment *Comment, err error) {
  549. sess := x.NewSession()
  550. defer sess.Close()
  551. if err = sess.Begin(); err != nil {
  552. return nil, err
  553. }
  554. comment, err = createComment(sess, opts)
  555. if err != nil {
  556. return nil, err
  557. }
  558. if err = sess.Commit(); err != nil {
  559. return nil, err
  560. }
  561. if opts.Type == CommentTypeComment {
  562. UpdateIssueIndexer(opts.Issue.ID)
  563. }
  564. return comment, nil
  565. }
  566. // CreateIssueComment creates a plain issue comment.
  567. func CreateIssueComment(doer *User, repo *Repository, issue *Issue, content string, attachments []string) (*Comment, error) {
  568. comment, err := CreateComment(&CreateCommentOptions{
  569. Type: CommentTypeComment,
  570. Doer: doer,
  571. Repo: repo,
  572. Issue: issue,
  573. Content: content,
  574. Attachments: attachments,
  575. })
  576. if err != nil {
  577. return nil, fmt.Errorf("CreateComment: %v", err)
  578. }
  579. mode, _ := AccessLevel(doer.ID, repo)
  580. if err = PrepareWebhooks(repo, HookEventIssueComment, &api.IssueCommentPayload{
  581. Action: api.HookIssueCommentCreated,
  582. Issue: issue.APIFormat(),
  583. Comment: comment.APIFormat(),
  584. Repository: repo.APIFormat(mode),
  585. Sender: doer.APIFormat(),
  586. }); err != nil {
  587. log.Error(2, "PrepareWebhooks [comment_id: %d]: %v", comment.ID, err)
  588. } else {
  589. go HookQueue.Add(repo.ID)
  590. }
  591. return comment, nil
  592. }
  593. // CreateRefComment creates a commit reference comment to issue.
  594. func CreateRefComment(doer *User, repo *Repository, issue *Issue, content, commitSHA string) error {
  595. if len(commitSHA) == 0 {
  596. return fmt.Errorf("cannot create reference with empty commit SHA")
  597. }
  598. // Check if same reference from same commit has already existed.
  599. has, err := x.Get(&Comment{
  600. Type: CommentTypeCommitRef,
  601. IssueID: issue.ID,
  602. CommitSHA: commitSHA,
  603. })
  604. if err != nil {
  605. return fmt.Errorf("check reference comment: %v", err)
  606. } else if has {
  607. return nil
  608. }
  609. _, err = CreateComment(&CreateCommentOptions{
  610. Type: CommentTypeCommitRef,
  611. Doer: doer,
  612. Repo: repo,
  613. Issue: issue,
  614. CommitSHA: commitSHA,
  615. Content: content,
  616. })
  617. return err
  618. }
  619. // GetCommentByID returns the comment by given ID.
  620. func GetCommentByID(id int64) (*Comment, error) {
  621. c := new(Comment)
  622. has, err := x.ID(id).Get(c)
  623. if err != nil {
  624. return nil, err
  625. } else if !has {
  626. return nil, ErrCommentNotExist{id, 0}
  627. }
  628. return c, nil
  629. }
  630. // FindCommentsOptions describes the conditions to Find comments
  631. type FindCommentsOptions struct {
  632. RepoID int64
  633. IssueID int64
  634. Since int64
  635. Type CommentType
  636. }
  637. func (opts *FindCommentsOptions) toConds() builder.Cond {
  638. var cond = builder.NewCond()
  639. if opts.RepoID > 0 {
  640. cond = cond.And(builder.Eq{"issue.repo_id": opts.RepoID})
  641. }
  642. if opts.IssueID > 0 {
  643. cond = cond.And(builder.Eq{"comment.issue_id": opts.IssueID})
  644. }
  645. if opts.Since > 0 {
  646. cond = cond.And(builder.Gte{"comment.updated_unix": opts.Since})
  647. }
  648. if opts.Type != CommentTypeUnknown {
  649. cond = cond.And(builder.Eq{"comment.type": opts.Type})
  650. }
  651. return cond
  652. }
  653. func findComments(e Engine, opts FindCommentsOptions) ([]*Comment, error) {
  654. comments := make([]*Comment, 0, 10)
  655. sess := e.Where(opts.toConds())
  656. if opts.RepoID > 0 {
  657. sess.Join("INNER", "issue", "issue.id = comment.issue_id")
  658. }
  659. return comments, sess.
  660. Asc("comment.created_unix").
  661. Asc("comment.id").
  662. Find(&comments)
  663. }
  664. // FindComments returns all comments according options
  665. func FindComments(opts FindCommentsOptions) ([]*Comment, error) {
  666. return findComments(x, opts)
  667. }
  668. // GetCommentsByIssueID returns all comments of an issue.
  669. func GetCommentsByIssueID(issueID int64) ([]*Comment, error) {
  670. return findComments(x, FindCommentsOptions{
  671. IssueID: issueID,
  672. Type: CommentTypeUnknown,
  673. })
  674. }
  675. // GetCommentsByIssueIDSince returns a list of comments of an issue since a given time point.
  676. func GetCommentsByIssueIDSince(issueID, since int64) ([]*Comment, error) {
  677. return findComments(x, FindCommentsOptions{
  678. IssueID: issueID,
  679. Type: CommentTypeUnknown,
  680. Since: since,
  681. })
  682. }
  683. // GetCommentsByRepoIDSince returns a list of comments for all issues in a repo since a given time point.
  684. func GetCommentsByRepoIDSince(repoID, since int64) ([]*Comment, error) {
  685. return findComments(x, FindCommentsOptions{
  686. RepoID: repoID,
  687. Type: CommentTypeUnknown,
  688. Since: since,
  689. })
  690. }
  691. // UpdateComment updates information of comment.
  692. func UpdateComment(doer *User, c *Comment, oldContent string) error {
  693. if _, err := x.ID(c.ID).AllCols().Update(c); err != nil {
  694. return err
  695. } else if c.Type == CommentTypeComment {
  696. UpdateIssueIndexer(c.IssueID)
  697. }
  698. if err := c.LoadIssue(); err != nil {
  699. return err
  700. }
  701. if err := c.Issue.LoadAttributes(); err != nil {
  702. return err
  703. }
  704. mode, _ := AccessLevel(doer.ID, c.Issue.Repo)
  705. if err := PrepareWebhooks(c.Issue.Repo, HookEventIssueComment, &api.IssueCommentPayload{
  706. Action: api.HookIssueCommentEdited,
  707. Issue: c.Issue.APIFormat(),
  708. Comment: c.APIFormat(),
  709. Changes: &api.ChangesPayload{
  710. Body: &api.ChangesFromPayload{
  711. From: oldContent,
  712. },
  713. },
  714. Repository: c.Issue.Repo.APIFormat(mode),
  715. Sender: doer.APIFormat(),
  716. }); err != nil {
  717. log.Error(2, "PrepareWebhooks [comment_id: %d]: %v", c.ID, err)
  718. } else {
  719. go HookQueue.Add(c.Issue.Repo.ID)
  720. }
  721. return nil
  722. }
  723. // DeleteComment deletes the comment
  724. func DeleteComment(doer *User, comment *Comment) error {
  725. sess := x.NewSession()
  726. defer sess.Close()
  727. if err := sess.Begin(); err != nil {
  728. return err
  729. }
  730. if _, err := sess.Delete(&Comment{
  731. ID: comment.ID,
  732. }); err != nil {
  733. return err
  734. }
  735. if comment.Type == CommentTypeComment {
  736. if _, err := sess.Exec("UPDATE `issue` SET num_comments = num_comments - 1 WHERE id = ?", comment.IssueID); err != nil {
  737. return err
  738. }
  739. }
  740. if _, err := sess.Where("comment_id = ?", comment.ID).Cols("is_deleted").Update(&Action{IsDeleted: true}); err != nil {
  741. return err
  742. }
  743. if err := sess.Commit(); err != nil {
  744. return err
  745. } else if comment.Type == CommentTypeComment {
  746. UpdateIssueIndexer(comment.IssueID)
  747. }
  748. if err := comment.LoadIssue(); err != nil {
  749. return err
  750. }
  751. if err := comment.Issue.LoadAttributes(); err != nil {
  752. return err
  753. }
  754. mode, _ := AccessLevel(doer.ID, comment.Issue.Repo)
  755. if err := PrepareWebhooks(comment.Issue.Repo, HookEventIssueComment, &api.IssueCommentPayload{
  756. Action: api.HookIssueCommentDeleted,
  757. Issue: comment.Issue.APIFormat(),
  758. Comment: comment.APIFormat(),
  759. Repository: comment.Issue.Repo.APIFormat(mode),
  760. Sender: doer.APIFormat(),
  761. }); err != nil {
  762. log.Error(2, "PrepareWebhooks [comment_id: %d]: %v", comment.ID, err)
  763. } else {
  764. go HookQueue.Add(comment.Issue.Repo.ID)
  765. }
  766. return nil
  767. }