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

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