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