2021-11-21 12:11:48 +03:00
// Copyright 2021 The Gitea Authors. All rights reserved.
2022-11-27 21:20:29 +03:00
// SPDX-License-Identifier: MIT
2019-07-30 04:59:10 +03:00
2021-11-21 12:11:48 +03:00
package issue
2019-07-30 04:59:10 +03:00
import (
"fmt"
2019-12-07 18:52:36 +03:00
"html"
2021-11-16 21:18:25 +03:00
"net/url"
2020-09-04 18:37:37 +03:00
"regexp"
"strconv"
2020-06-19 10:19:56 +03:00
"strings"
2020-09-04 18:37:37 +03:00
"time"
2019-07-30 04:59:10 +03:00
2022-04-28 14:48:48 +03:00
"code.gitea.io/gitea/models/db"
2022-06-13 12:37:59 +03:00
issues_model "code.gitea.io/gitea/models/issues"
2022-05-11 13:09:36 +03:00
access_model "code.gitea.io/gitea/models/perm/access"
2021-12-10 04:27:50 +03:00
repo_model "code.gitea.io/gitea/models/repo"
2021-11-24 12:49:20 +03:00
user_model "code.gitea.io/gitea/models/user"
2022-10-12 08:18:26 +03:00
"code.gitea.io/gitea/modules/container"
2023-01-19 03:24:38 +03:00
"code.gitea.io/gitea/modules/git"
2022-08-25 05:31:57 +03:00
"code.gitea.io/gitea/modules/log"
2019-12-07 18:52:36 +03:00
"code.gitea.io/gitea/modules/references"
2020-01-10 12:34:21 +03:00
"code.gitea.io/gitea/modules/repository"
2019-07-30 04:59:10 +03:00
)
2020-09-04 18:37:37 +03:00
const (
secondsByMinute = float64 ( time . Minute / time . Second ) // seconds in a minute
secondsByHour = 60 * secondsByMinute // seconds in an hour
secondsByDay = 8 * secondsByHour // seconds in a day
secondsByWeek = 5 * secondsByDay // seconds in a week
secondsByMonth = 4 * secondsByWeek // seconds in a month
)
var reDuration = regexp . MustCompile ( ` (?i)^(?:(\d+([\.,]\d+)?)(?:mo))?(?:(\d+([\.,]\d+)?)(?:w))?(?:(\d+([\.,]\d+)?)(?:d))?(?:(\d+([\.,]\d+)?)(?:h))?(?:(\d+([\.,]\d+)?)(?:m))?$ ` )
// timeLogToAmount parses time log string and returns amount in seconds
func timeLogToAmount ( str string ) int64 {
matches := reDuration . FindAllStringSubmatch ( str , - 1 )
if len ( matches ) == 0 {
return 0
}
match := matches [ 0 ]
var a int64
// months
if len ( match [ 1 ] ) > 0 {
mo , _ := strconv . ParseFloat ( strings . Replace ( match [ 1 ] , "," , "." , 1 ) , 64 )
a += int64 ( mo * secondsByMonth )
}
// weeks
if len ( match [ 3 ] ) > 0 {
w , _ := strconv . ParseFloat ( strings . Replace ( match [ 3 ] , "," , "." , 1 ) , 64 )
a += int64 ( w * secondsByWeek )
}
// days
if len ( match [ 5 ] ) > 0 {
d , _ := strconv . ParseFloat ( strings . Replace ( match [ 5 ] , "," , "." , 1 ) , 64 )
a += int64 ( d * secondsByDay )
}
// hours
if len ( match [ 7 ] ) > 0 {
h , _ := strconv . ParseFloat ( strings . Replace ( match [ 7 ] , "," , "." , 1 ) , 64 )
a += int64 ( h * secondsByHour )
}
// minutes
if len ( match [ 9 ] ) > 0 {
d , _ := strconv . ParseFloat ( strings . Replace ( match [ 9 ] , "," , "." , 1 ) , 64 )
a += int64 ( d * secondsByMinute )
}
return a
}
2022-06-13 12:37:59 +03:00
func issueAddTime ( issue * issues_model . Issue , doer * user_model . User , time time . Time , timeLog string ) error {
2020-09-04 18:37:37 +03:00
amount := timeLogToAmount ( timeLog )
if amount == 0 {
return nil
}
2022-06-13 12:37:59 +03:00
_ , err := issues_model . AddTime ( doer , issue , amount , time )
2020-09-04 18:37:37 +03:00
return err
}
2021-11-21 12:11:48 +03:00
// getIssueFromRef returns the issue referenced by a ref. Returns a nil *Issue
// if the provided ref references a non-existent issue.
2022-06-13 12:37:59 +03:00
func getIssueFromRef ( repo * repo_model . Repository , index int64 ) ( * issues_model . Issue , error ) {
issue , err := issues_model . GetIssueByIndex ( repo . ID , index )
2019-12-16 00:57:34 +03:00
if err != nil {
2022-06-13 12:37:59 +03:00
if issues_model . IsErrIssueNotExist ( err ) {
2021-11-21 12:11:48 +03:00
return nil , nil
2019-12-07 18:52:36 +03:00
}
2021-11-21 12:11:48 +03:00
return nil , err
2019-12-07 18:52:36 +03:00
}
2021-11-21 12:11:48 +03:00
return issue , nil
2019-12-07 18:52:36 +03:00
}
// UpdateIssuesCommit checks if issues are manipulated by commit message.
2021-12-10 04:27:50 +03:00
func UpdateIssuesCommit ( doer * user_model . User , repo * repo_model . Repository , commits [ ] * repository . PushCommit , branchName string ) error {
2019-12-07 18:52:36 +03:00
// Commits are appended in the reverse order.
for i := len ( commits ) - 1 ; i >= 0 ; i -- {
c := commits [ i ]
type markKey struct {
ID int64
Action references . XRefAction
}
2022-10-12 08:18:26 +03:00
refMarked := make ( container . Set [ markKey ] )
2021-12-10 04:27:50 +03:00
var refRepo * repo_model . Repository
2022-06-13 12:37:59 +03:00
var refIssue * issues_model . Issue
2019-12-07 18:52:36 +03:00
var err error
for _ , ref := range references . FindAllIssueReferences ( c . Message ) {
// issue is from another repo
if len ( ref . Owner ) > 0 && len ( ref . Name ) > 0 {
2022-12-03 05:48:26 +03:00
refRepo , err = repo_model . GetRepositoryByOwnerAndName ( db . DefaultContext , ref . Owner , ref . Name )
2019-12-07 18:52:36 +03:00
if err != nil {
2022-08-25 05:31:57 +03:00
if repo_model . IsErrRepoNotExist ( err ) {
log . Warn ( "Repository referenced in commit but does not exist: %v" , err )
} else {
log . Error ( "repo_model.GetRepositoryByOwnerAndName: %v" , err )
}
2019-12-07 18:52:36 +03:00
continue
}
} else {
refRepo = repo
}
if refIssue , err = getIssueFromRef ( refRepo , ref . Index ) ; err != nil {
return err
}
if refIssue == nil {
continue
}
2022-05-11 13:09:36 +03:00
perm , err := access_model . GetUserRepoPermission ( db . DefaultContext , refRepo , doer )
2019-12-07 18:52:36 +03:00
if err != nil {
return err
}
key := markKey { ID : refIssue . ID , Action : ref . Action }
2022-10-12 08:18:26 +03:00
if ! refMarked . Add ( key ) {
2019-12-07 18:52:36 +03:00
continue
}
// FIXME: this kind of condition is all over the code, it should be consolidated in a single place
2020-01-20 15:00:32 +03:00
canclose := perm . IsAdmin ( ) || perm . IsOwner ( ) || perm . CanWriteIssuesOrPulls ( refIssue . IsPull ) || refIssue . PosterID == doer . ID
cancomment := canclose || perm . CanReadIssuesOrPulls ( refIssue . IsPull )
2019-12-07 18:52:36 +03:00
// Don't proceed if the user can't comment
if ! cancomment {
continue
}
2021-11-16 21:18:25 +03:00
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 ] ) )
2022-12-10 05:46:31 +03:00
if err = CreateRefComment ( doer , refRepo , refIssue , message , c . Sha1 ) ; err != nil {
2019-12-07 18:52:36 +03:00
return err
}
// Only issues can be closed/reopened this way, and user needs the correct permissions
if refIssue . IsPull || ! canclose {
continue
}
// Only process closing/reopening keywords
if ref . Action != references . XRefActionCloses && ref . Action != references . XRefActionReopens {
continue
}
if ! repo . CloseIssuesViaCommitInAnyBranch {
// If the issue was specified to be in a particular branch, don't allow commits in other branches to close it
if refIssue . Ref != "" {
2023-01-19 03:24:38 +03:00
issueBranchName := strings . TrimPrefix ( refIssue . Ref , git . BranchPrefix )
if branchName != issueBranchName {
2019-12-07 18:52:36 +03:00
continue
}
// Otherwise, only process commits to the default branch
} else if branchName != repo . DefaultBranch {
continue
}
}
2021-04-09 10:40:34 +03:00
close := ref . Action == references . XRefActionCloses
2020-09-04 18:37:37 +03:00
if close && len ( ref . TimeLog ) > 0 {
if err := issueAddTime ( refIssue , doer , c . Timestamp , ref . TimeLog ) ; err != nil {
return err
}
}
2020-01-11 04:20:11 +03:00
if close != refIssue . IsClosed {
2021-11-21 12:11:48 +03:00
refIssue . Repo = refRepo
if err := ChangeStatus ( refIssue , doer , close ) ; err != nil {
2020-01-11 04:20:11 +03:00
return err
}
2019-12-07 18:52:36 +03:00
}
}
}
return nil
}