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.

commit.go 5.3KB

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