code.gitea.io/gitea@v1.22.3/services/issue/commit.go (about)

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