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.

218 lines
6.0 KiB

  1. // Copyright 2019 The Gitea 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 repofiles
  5. import (
  6. "fmt"
  7. "html"
  8. "regexp"
  9. "strconv"
  10. "strings"
  11. "time"
  12. "code.gitea.io/gitea/models"
  13. "code.gitea.io/gitea/modules/notification"
  14. "code.gitea.io/gitea/modules/references"
  15. "code.gitea.io/gitea/modules/repository"
  16. )
  17. const (
  18. secondsByMinute = float64(time.Minute / time.Second) // seconds in a minute
  19. secondsByHour = 60 * secondsByMinute // seconds in an hour
  20. secondsByDay = 8 * secondsByHour // seconds in a day
  21. secondsByWeek = 5 * secondsByDay // seconds in a week
  22. secondsByMonth = 4 * secondsByWeek // seconds in a month
  23. )
  24. var reDuration = regexp.MustCompile(`(?i)^(?:(\d+([\.,]\d+)?)(?:mo))?(?:(\d+([\.,]\d+)?)(?:w))?(?:(\d+([\.,]\d+)?)(?:d))?(?:(\d+([\.,]\d+)?)(?:h))?(?:(\d+([\.,]\d+)?)(?:m))?$`)
  25. // getIssueFromRef returns the issue referenced by a ref. Returns a nil *Issue
  26. // if the provided ref references a non-existent issue.
  27. func getIssueFromRef(repo *models.Repository, index int64) (*models.Issue, error) {
  28. issue, err := models.GetIssueByIndex(repo.ID, index)
  29. if err != nil {
  30. if models.IsErrIssueNotExist(err) {
  31. return nil, nil
  32. }
  33. return nil, err
  34. }
  35. return issue, nil
  36. }
  37. // timeLogToAmount parses time log string and returns amount in seconds
  38. func timeLogToAmount(str string) int64 {
  39. matches := reDuration.FindAllStringSubmatch(str, -1)
  40. if len(matches) == 0 {
  41. return 0
  42. }
  43. match := matches[0]
  44. var a int64
  45. // months
  46. if len(match[1]) > 0 {
  47. mo, _ := strconv.ParseFloat(strings.Replace(match[1], ",", ".", 1), 64)
  48. a += int64(mo * secondsByMonth)
  49. }
  50. // weeks
  51. if len(match[3]) > 0 {
  52. w, _ := strconv.ParseFloat(strings.Replace(match[3], ",", ".", 1), 64)
  53. a += int64(w * secondsByWeek)
  54. }
  55. // days
  56. if len(match[5]) > 0 {
  57. d, _ := strconv.ParseFloat(strings.Replace(match[5], ",", ".", 1), 64)
  58. a += int64(d * secondsByDay)
  59. }
  60. // hours
  61. if len(match[7]) > 0 {
  62. h, _ := strconv.ParseFloat(strings.Replace(match[7], ",", ".", 1), 64)
  63. a += int64(h * secondsByHour)
  64. }
  65. // minutes
  66. if len(match[9]) > 0 {
  67. d, _ := strconv.ParseFloat(strings.Replace(match[9], ",", ".", 1), 64)
  68. a += int64(d * secondsByMinute)
  69. }
  70. return a
  71. }
  72. func issueAddTime(issue *models.Issue, doer *models.User, time time.Time, timeLog string) error {
  73. amount := timeLogToAmount(timeLog)
  74. if amount == 0 {
  75. return nil
  76. }
  77. _, err := models.AddTime(doer, issue, amount, time)
  78. return err
  79. }
  80. func changeIssueStatus(repo *models.Repository, issue *models.Issue, doer *models.User, closed bool) error {
  81. stopTimerIfAvailable := func(doer *models.User, issue *models.Issue) error {
  82. if models.StopwatchExists(doer.ID, issue.ID) {
  83. if err := models.CreateOrStopIssueStopwatch(doer, issue); err != nil {
  84. return err
  85. }
  86. }
  87. return nil
  88. }
  89. issue.Repo = repo
  90. comment, err := issue.ChangeStatus(doer, closed)
  91. if err != nil {
  92. // Don't return an error when dependencies are open as this would let the push fail
  93. if models.IsErrDependenciesLeft(err) {
  94. return stopTimerIfAvailable(doer, issue)
  95. }
  96. return err
  97. }
  98. notification.NotifyIssueChangeStatus(doer, issue, comment, closed)
  99. return stopTimerIfAvailable(doer, issue)
  100. }
  101. // UpdateIssuesCommit checks if issues are manipulated by commit message.
  102. func UpdateIssuesCommit(doer *models.User, repo *models.Repository, commits []*repository.PushCommit, branchName string) error {
  103. // Commits are appended in the reverse order.
  104. for i := len(commits) - 1; i >= 0; i-- {
  105. c := commits[i]
  106. type markKey struct {
  107. ID int64
  108. Action references.XRefAction
  109. }
  110. refMarked := make(map[markKey]bool)
  111. var refRepo *models.Repository
  112. var refIssue *models.Issue
  113. var err error
  114. for _, ref := range references.FindAllIssueReferences(c.Message) {
  115. // issue is from another repo
  116. if len(ref.Owner) > 0 && len(ref.Name) > 0 {
  117. refRepo, err = models.GetRepositoryFromMatch(ref.Owner, ref.Name)
  118. if err != nil {
  119. continue
  120. }
  121. } else {
  122. refRepo = repo
  123. }
  124. if refIssue, err = getIssueFromRef(refRepo, ref.Index); err != nil {
  125. return err
  126. }
  127. if refIssue == nil {
  128. continue
  129. }
  130. perm, err := models.GetUserRepoPermission(refRepo, doer)
  131. if err != nil {
  132. return err
  133. }
  134. key := markKey{ID: refIssue.ID, Action: ref.Action}
  135. if refMarked[key] {
  136. continue
  137. }
  138. refMarked[key] = true
  139. // FIXME: this kind of condition is all over the code, it should be consolidated in a single place
  140. canclose := perm.IsAdmin() || perm.IsOwner() || perm.CanWriteIssuesOrPulls(refIssue.IsPull) || refIssue.PosterID == doer.ID
  141. cancomment := canclose || perm.CanReadIssuesOrPulls(refIssue.IsPull)
  142. // Don't proceed if the user can't comment
  143. if !cancomment {
  144. continue
  145. }
  146. message := fmt.Sprintf(`<a href="%s/commit/%s">%s</a>`, repo.Link(), c.Sha1, html.EscapeString(strings.SplitN(c.Message, "\n", 2)[0]))
  147. if err = models.CreateRefComment(doer, refRepo, refIssue, message, c.Sha1); err != nil {
  148. return err
  149. }
  150. // Only issues can be closed/reopened this way, and user needs the correct permissions
  151. if refIssue.IsPull || !canclose {
  152. continue
  153. }
  154. // Only process closing/reopening keywords
  155. if ref.Action != references.XRefActionCloses && ref.Action != references.XRefActionReopens {
  156. continue
  157. }
  158. if !repo.CloseIssuesViaCommitInAnyBranch {
  159. // If the issue was specified to be in a particular branch, don't allow commits in other branches to close it
  160. if refIssue.Ref != "" {
  161. if branchName != refIssue.Ref {
  162. continue
  163. }
  164. // Otherwise, only process commits to the default branch
  165. } else if branchName != repo.DefaultBranch {
  166. continue
  167. }
  168. }
  169. close := (ref.Action == references.XRefActionCloses)
  170. if close && len(ref.TimeLog) > 0 {
  171. if err := issueAddTime(refIssue, doer, c.Timestamp, ref.TimeLog); err != nil {
  172. return err
  173. }
  174. }
  175. if close != refIssue.IsClosed {
  176. if err := changeIssueStatus(refRepo, refIssue, doer, close); err != nil {
  177. return err
  178. }
  179. }
  180. }
  181. }
  182. return nil
  183. }