Files
gitea/services/issue/pull.go
wxiaoguang 9dc04289aa refactor: make git package handle all git operations (#38543)
before: gitrepo vs git packages
after: git package fully handle all git operations

by the way, use `WithRepo(repo)` instead of `WithDir(repo.Path)` to hide
path details.

benefits:

1. remove all unnecessary wrappers, developers no need to struggle with
"which package should be used"
2. simplify code, RepositoryFacade can (will) be used everywhere, all
"path" details are (will be) hidden
2026-07-20 16:07:38 +00:00

181 lines
4.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package issue
import (
"context"
"fmt"
"slices"
"time"
issues_model "gitea.dev/models/issues"
org_model "gitea.dev/models/organization"
user_model "gitea.dev/models/user"
"gitea.dev/modules/git"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
)
type ReviewRequestNotifier struct {
Comment *issues_model.Comment
IsAdd bool
Reviewer *user_model.User
ReviewTeam *org_model.Team
}
var codeOwnerFiles = []string{"CODEOWNERS", "docs/CODEOWNERS", ".gitea/CODEOWNERS"}
// codeOwnerMatchBudget caps the total wall-clock time spent evaluating all
// CODEOWNERS rules against all changed files for a single PR.
const codeOwnerMatchBudget = 2 * time.Second
func IsCodeOwnerFile(f string) bool {
return slices.Contains(codeOwnerFiles, f)
}
func PullRequestCodeOwnersReview(ctx context.Context, pr *issues_model.PullRequest) ([]*ReviewRequestNotifier, error) {
if err := pr.LoadIssue(ctx); err != nil {
return nil, err
}
issue := pr.Issue
if pr.IsWorkInProgress(ctx) {
return nil, nil
}
if err := pr.LoadHeadRepo(ctx); err != nil {
return nil, err
}
if err := pr.LoadBaseRepo(ctx); err != nil {
return nil, err
}
pr.Issue.Repo = pr.BaseRepo
if pr.BaseRepo.IsFork {
return nil, nil
}
repo, err := git.OpenRepository(pr.BaseRepo)
if err != nil {
return nil, err
}
defer repo.Close()
commit, err := repo.GetBranchCommit(ctx, pr.BaseRepo.DefaultBranch)
if err != nil {
return nil, err
}
var data string
for _, file := range codeOwnerFiles {
if blob, err := commit.GetBlobByPath(ctx, repo, file); err == nil {
data, err = blob.GetBlobContent(ctx, setting.UI.MaxDisplayFileSize)
if err == nil {
break
}
}
}
if data == "" {
return nil, nil
}
rules, _ := issues_model.GetCodeOwnersFromContent(ctx, data)
if len(rules) == 0 {
return nil, nil
}
// get the mergebase
mergeBase, err := git.MergeBase(ctx, pr.BaseRepo, git.BranchPrefix+pr.BaseBranch, pr.GetGitHeadRefName())
if err != nil {
return nil, err
}
// https://github.com/go-gitea/gitea/issues/29763, we need to get the files changed
// between the merge base and the head commit but not the base branch and the head commit
changedFiles, err := repo.GetFilesChangedBetween(ctx, mergeBase, pr.GetGitHeadRefName())
if err != nil {
return nil, err
}
uniqUsers := make(map[int64]*user_model.User)
uniqTeams := make(map[string]*org_model.Team)
// Bound the total time spent matching rules×files. The per-rule MatchTimeout
// only caps a single match; without an aggregate budget a crafted CODEOWNERS
// plus a PR touching many files could still exhaust CPU inside this loop.
matchDeadline := time.Now().Add(codeOwnerMatchBudget)
ruleLoop:
for _, rule := range rules {
for _, f := range changedFiles {
if time.Now().After(matchDeadline) {
log.Warn("CODEOWNERS matching for PR %s#%d exceeded its time budget; some rules were not evaluated", pr.BaseRepo.FullName(), pr.ID)
break ruleLoop
}
shouldMatch := !rule.Negative
matched, _ := rule.Rule.MatchString(f) // err only happens when timeouts, any error can be considered as not matched
if matched == shouldMatch {
for _, u := range rule.Users {
uniqUsers[u.ID] = u
}
for _, t := range rule.Teams {
uniqTeams[fmt.Sprintf("%d/%d", t.OrgID, t.ID)] = t
}
}
}
}
notifiers := make([]*ReviewRequestNotifier, 0, len(uniqUsers)+len(uniqTeams))
if err := issue.LoadPoster(ctx); err != nil {
return nil, err
}
// load all reviews from database
latestReviews, _, err := issues_model.GetReviewsByIssueID(ctx, pr.IssueID)
if err != nil {
return nil, err
}
contain := func(list issues_model.ReviewList, u *user_model.User) bool {
for _, review := range list {
if review.ReviewerTeamID == 0 && review.ReviewerID == u.ID {
return true
}
}
return false
}
for _, u := range uniqUsers {
if u.ID != issue.Poster.ID && !contain(latestReviews, u) {
comment, err := issues_model.AddReviewRequest(ctx, issue, u, issue.Poster, true)
if err != nil {
log.Warn("Failed add review user: %s to PR review: %s#%d, error: %s", u.Name, pr.BaseRepo.Name, pr.ID, err)
return nil, err
}
if comment == nil { // comment maybe nil if review type is ReviewTypeRequest
continue
}
notifiers = append(notifiers, &ReviewRequestNotifier{
Comment: comment,
IsAdd: true,
Reviewer: u,
})
}
}
for _, t := range uniqTeams {
comment, err := issues_model.AddTeamReviewRequest(ctx, issue, t, issue.Poster, true)
if err != nil {
log.Warn("Failed add reviewer team: %s to PR review: %s#%d, error: %s", t.Name, pr.BaseRepo.Name, pr.ID, err)
return nil, err
}
if comment == nil { // comment maybe nil if review type is ReviewTypeRequest
continue
}
notifiers = append(notifiers, &ReviewRequestNotifier{
Comment: comment,
IsAdd: true,
ReviewTeam: t,
})
}
return notifiers, nil
}