code.gitea.io/gitea@v1.21.7/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  	"fmt"
     9  	"html"
    10  	"net/url"
    11  	"regexp"
    12  	"strconv"
    13  	"strings"
    14  	"time"
    15  
    16  	issues_model "code.gitea.io/gitea/models/issues"
    17  	access_model "code.gitea.io/gitea/models/perm/access"
    18  	repo_model "code.gitea.io/gitea/models/repo"
    19  	user_model "code.gitea.io/gitea/models/user"
    20  	"code.gitea.io/gitea/modules/container"
    21  	"code.gitea.io/gitea/modules/git"
    22  	"code.gitea.io/gitea/modules/log"
    23  	"code.gitea.io/gitea/modules/references"
    24  	"code.gitea.io/gitea/modules/repository"
    25  )
    26  
    27  const (
    28  	secondsByMinute = float64(time.Minute / time.Second) // seconds in a minute
    29  	secondsByHour   = 60 * secondsByMinute               // seconds in an hour
    30  	secondsByDay    = 8 * secondsByHour                  // seconds in a day
    31  	secondsByWeek   = 5 * secondsByDay                   // seconds in a week
    32  	secondsByMonth  = 4 * secondsByWeek                  // seconds in a month
    33  )
    34  
    35  var reDuration = regexp.MustCompile(`(?i)^(?:(\d+([\.,]\d+)?)(?:mo))?(?:(\d+([\.,]\d+)?)(?:w))?(?:(\d+([\.,]\d+)?)(?:d))?(?:(\d+([\.,]\d+)?)(?:h))?(?:(\d+([\.,]\d+)?)(?:m))?$`)
    36  
    37  // timeLogToAmount parses time log string and returns amount in seconds
    38  func timeLogToAmount(str string) int64 {
    39  	matches := reDuration.FindAllStringSubmatch(str, -1)
    40  	if len(matches) == 0 {
    41  		return 0
    42  	}
    43  
    44  	match := matches[0]
    45  
    46  	var a int64
    47  
    48  	// months
    49  	if len(match[1]) > 0 {
    50  		mo, _ := strconv.ParseFloat(strings.Replace(match[1], ",", ".", 1), 64)
    51  		a += int64(mo * secondsByMonth)
    52  	}
    53  
    54  	// weeks
    55  	if len(match[3]) > 0 {
    56  		w, _ := strconv.ParseFloat(strings.Replace(match[3], ",", ".", 1), 64)
    57  		a += int64(w * secondsByWeek)
    58  	}
    59  
    60  	// days
    61  	if len(match[5]) > 0 {
    62  		d, _ := strconv.ParseFloat(strings.Replace(match[5], ",", ".", 1), 64)
    63  		a += int64(d * secondsByDay)
    64  	}
    65  
    66  	// hours
    67  	if len(match[7]) > 0 {
    68  		h, _ := strconv.ParseFloat(strings.Replace(match[7], ",", ".", 1), 64)
    69  		a += int64(h * secondsByHour)
    70  	}
    71  
    72  	// minutes
    73  	if len(match[9]) > 0 {
    74  		d, _ := strconv.ParseFloat(strings.Replace(match[9], ",", ".", 1), 64)
    75  		a += int64(d * secondsByMinute)
    76  	}
    77  
    78  	return a
    79  }
    80  
    81  func issueAddTime(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, time time.Time, timeLog string) error {
    82  	amount := timeLogToAmount(timeLog)
    83  	if amount == 0 {
    84  		return nil
    85  	}
    86  
    87  	_, err := issues_model.AddTime(ctx, doer, issue, amount, time)
    88  	return err
    89  }
    90  
    91  // getIssueFromRef returns the issue referenced by a ref. Returns a nil *Issue
    92  // if the provided ref references a non-existent issue.
    93  func getIssueFromRef(ctx context.Context, repo *repo_model.Repository, index int64) (*issues_model.Issue, error) {
    94  	issue, err := issues_model.GetIssueByIndex(ctx, repo.ID, index)
    95  	if err != nil {
    96  		if issues_model.IsErrIssueNotExist(err) {
    97  			return nil, nil
    98  		}
    99  		return nil, err
   100  	}
   101  	return issue, nil
   102  }
   103  
   104  // UpdateIssuesCommit checks if issues are manipulated by commit message.
   105  func UpdateIssuesCommit(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, commits []*repository.PushCommit, branchName string) error {
   106  	// Commits are appended in the reverse order.
   107  	for i := len(commits) - 1; i >= 0; i-- {
   108  		c := commits[i]
   109  
   110  		type markKey struct {
   111  			ID     int64
   112  			Action references.XRefAction
   113  		}
   114  
   115  		refMarked := make(container.Set[markKey])
   116  		var refRepo *repo_model.Repository
   117  		var refIssue *issues_model.Issue
   118  		var err error
   119  		for _, ref := range references.FindAllIssueReferences(c.Message) {
   120  
   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  				return err
   164  			}
   165  
   166  			// Only issues can be closed/reopened this way, and user needs the correct permissions
   167  			if refIssue.IsPull || !canclose {
   168  				continue
   169  			}
   170  
   171  			// Only process closing/reopening keywords
   172  			if ref.Action != references.XRefActionCloses && ref.Action != references.XRefActionReopens {
   173  				continue
   174  			}
   175  
   176  			if !repo.CloseIssuesViaCommitInAnyBranch {
   177  				// If the issue was specified to be in a particular branch, don't allow commits in other branches to close it
   178  				if refIssue.Ref != "" {
   179  					issueBranchName := strings.TrimPrefix(refIssue.Ref, git.BranchPrefix)
   180  					if branchName != issueBranchName {
   181  						continue
   182  					}
   183  					// Otherwise, only process commits to the default branch
   184  				} else if branchName != repo.DefaultBranch {
   185  					continue
   186  				}
   187  			}
   188  			close := ref.Action == references.XRefActionCloses
   189  			if close && len(ref.TimeLog) > 0 {
   190  				if err := issueAddTime(ctx, refIssue, doer, c.Timestamp, ref.TimeLog); err != nil {
   191  					return err
   192  				}
   193  			}
   194  			if close != refIssue.IsClosed {
   195  				refIssue.Repo = refRepo
   196  				if err := ChangeStatus(ctx, refIssue, doer, c.Sha1, close); err != nil {
   197  					return err
   198  				}
   199  			}
   200  		}
   201  	}
   202  	return nil
   203  }