mirror of
https://github.com/go-gitea/gitea.git
synced 2026-07-22 17:02:41 +00:00
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
222 lines
7.7 KiB
Go
222 lines
7.7 KiB
Go
// Copyright 2020 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package pull
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
git_model "gitea.dev/models/git"
|
|
issues_model "gitea.dev/models/issues"
|
|
access_model "gitea.dev/models/perm/access"
|
|
repo_model "gitea.dev/models/repo"
|
|
"gitea.dev/models/unit"
|
|
user_model "gitea.dev/models/user"
|
|
"gitea.dev/modules/git"
|
|
"gitea.dev/modules/globallock"
|
|
"gitea.dev/modules/log"
|
|
"gitea.dev/modules/repository"
|
|
)
|
|
|
|
// Update updates pull request with base branch.
|
|
func Update(ctx context.Context, pr *issues_model.PullRequest, doer *user_model.User, message string, rebase bool) error {
|
|
if pr.Flow == issues_model.PullRequestFlowAGit {
|
|
// TODO: update of agit flow pull request's head branch is unsupported
|
|
return errors.New("update of agit flow pull request's head branch is unsupported")
|
|
}
|
|
|
|
releaser, err := globallock.Lock(ctx, getPullWorkingLockKey(pr.ID))
|
|
if err != nil {
|
|
log.Error("lock.Lock(): %v", err)
|
|
return fmt.Errorf("lock.Lock: %w", err)
|
|
}
|
|
defer releaser()
|
|
|
|
if err := pr.LoadBaseRepo(ctx); err != nil {
|
|
log.Error("unable to load BaseRepo for %-v during update-by-merge: %v", pr, err)
|
|
return fmt.Errorf("unable to load BaseRepo for PR[%d] during update-by-merge: %w", pr.ID, err)
|
|
}
|
|
|
|
// TODO: FakePR: if the PR is a fake PR (for example: from Merge Upstream), then no need to check diverging
|
|
if pr.ID > 0 {
|
|
diffCount, err := git.GetDivergingCommits(ctx, pr.BaseRepo, pr.BaseBranch, pr.GetGitHeadRefName())
|
|
if err != nil {
|
|
return err
|
|
} else if diffCount.Behind == 0 {
|
|
return fmt.Errorf("HeadBranch of PR %d is up to date", pr.Index)
|
|
}
|
|
}
|
|
|
|
if err := pr.LoadHeadRepo(ctx); err != nil {
|
|
log.Error("unable to load HeadRepo for PR %-v during update-by-merge: %v", pr, err)
|
|
return fmt.Errorf("unable to load HeadRepo for PR[%d] during update-by-merge: %w", pr.ID, err)
|
|
}
|
|
if pr.HeadRepo == nil {
|
|
// LoadHeadRepo will swallow ErrRepoNotExist so if pr.HeadRepo is still nil recreate the error
|
|
err := repo_model.ErrRepoNotExist{
|
|
ID: pr.HeadRepoID,
|
|
}
|
|
log.Error("unable to load HeadRepo for PR %-v during update-by-merge: %v", pr, err)
|
|
return fmt.Errorf("unable to load HeadRepo for PR[%d] during update-by-merge: %w", pr.ID, err)
|
|
}
|
|
|
|
defer func() {
|
|
// The code is from https://github.com/go-gitea/gitea/pull/9784,
|
|
// it seems a simple copy-paste from https://github.com/go-gitea/gitea/pull/7082 without a real reason.
|
|
// TODO: DUPLICATE-PR-TASK: search and see another TODO comment for more details
|
|
go AddTestPullRequestTask(TestPullRequestOptions{
|
|
RepoID: pr.BaseRepo.ID,
|
|
Doer: doer,
|
|
Branch: pr.BaseBranch,
|
|
IsSync: false,
|
|
IsForcePush: false,
|
|
OldCommitID: "",
|
|
NewCommitID: "",
|
|
})
|
|
}()
|
|
|
|
if rebase {
|
|
return updateHeadByRebaseOnToBase(ctx, pr, doer)
|
|
}
|
|
|
|
// TODO: FakePR: it is somewhat hacky, but it is the only way to "merge" at the moment
|
|
// ideally in the future the "merge" functions should be refactored to decouple from the PullRequest
|
|
// now use a fake reverse PR to switch head&base repos/branches
|
|
reversePR := &issues_model.PullRequest{
|
|
ID: pr.ID,
|
|
|
|
HeadRepoID: pr.BaseRepoID,
|
|
HeadRepo: pr.BaseRepo,
|
|
HeadBranch: pr.BaseBranch,
|
|
|
|
BaseRepoID: pr.HeadRepoID,
|
|
BaseRepo: pr.HeadRepo,
|
|
BaseBranch: pr.HeadBranch,
|
|
}
|
|
|
|
_, err = doMergeAndPush(ctx, reversePR, doer, repo_model.MergeStyleMerge, "", message, repository.PushTriggerPRUpdateWithBase)
|
|
return err
|
|
}
|
|
|
|
// isUserAllowedToPushOrForcePushInRepoBranch checks whether user is allowed to push or force push in the given repo and branch
|
|
// it will check both user permission and branch protection rules
|
|
func isUserAllowedToPushOrForcePushInRepoBranch(ctx context.Context, user *user_model.User, repo *repo_model.Repository, branch string) (pushAllowed, forcePushAllowed bool, err error) {
|
|
if user == nil {
|
|
return false, false, nil
|
|
}
|
|
|
|
// 1. check user push permission on the given repository
|
|
repoPerm, err := access_model.GetDoerRepoPermission(ctx, repo, user)
|
|
if err != nil {
|
|
if repo_model.IsErrUnitTypeNotExist(err) {
|
|
return false, false, nil
|
|
}
|
|
return false, false, err
|
|
}
|
|
pushAllowed = repoPerm.CanWrite(unit.TypeCode)
|
|
forcePushAllowed = pushAllowed
|
|
|
|
// 2. check branch protection whether user can push or force push
|
|
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, branch)
|
|
if err != nil {
|
|
return false, false, err
|
|
}
|
|
if pb != nil { // override previous results if there is a branch protection rule
|
|
pb.Repo = repo
|
|
pushAllowed = pb.CanUserPush(ctx, user)
|
|
forcePushAllowed = pb.CanUserForcePush(ctx, user)
|
|
}
|
|
return pushAllowed, forcePushAllowed, nil
|
|
}
|
|
|
|
// CheckUserAllowedToUpdate check if user is allowed to update PR with given permissions and branch protections
|
|
// update PR means send new commits to PR head branch from base branch
|
|
func CheckUserAllowedToUpdate(ctx context.Context, pull *issues_model.PullRequest, user *user_model.User) (ret struct {
|
|
MergeAllowed, RebaseAllowed bool
|
|
DefaultUpdateStyle repo_model.UpdateStyle
|
|
}, err error,
|
|
) {
|
|
if user == nil {
|
|
return ret, nil
|
|
}
|
|
if err := pull.LoadBaseRepo(ctx); err != nil {
|
|
return ret, err
|
|
}
|
|
if err := pull.LoadHeadRepo(ctx); err != nil {
|
|
return ret, err
|
|
}
|
|
|
|
// 1. check whether pull request enabled.
|
|
prBaseUnit, err := pull.BaseRepo.GetUnit(ctx, unit.TypePullRequests)
|
|
if repo_model.IsErrUnitTypeNotExist(err) {
|
|
return ret, nil // the PR unit is disabled in base repo means no update allowed
|
|
} else if err != nil {
|
|
return ret, fmt.Errorf("get base repo unit: %v", err)
|
|
}
|
|
|
|
// 2. only support Github style pull request
|
|
if pull.Flow == issues_model.PullRequestFlowAGit {
|
|
return ret, nil
|
|
}
|
|
|
|
// 3. check user push permission on head repository
|
|
ret.MergeAllowed, ret.RebaseAllowed, err = isUserAllowedToPushOrForcePushInRepoBranch(ctx, user, pull.HeadRepo, pull.HeadBranch)
|
|
if err != nil {
|
|
return ret, err
|
|
}
|
|
|
|
// 4. if the pull creator allows maintainer to edit, we need to check whether
|
|
// user is a maintainer (has permission to merge into base branch) and inherit pull request poster's permission
|
|
if pull.AllowMaintainerEdit && (!ret.MergeAllowed || !ret.RebaseAllowed) {
|
|
baseRepoPerm, err := access_model.GetDoerRepoPermission(ctx, pull.BaseRepo, user)
|
|
if err != nil {
|
|
return ret, err
|
|
}
|
|
userAllowedToMergePR, err := isUserAllowedToMergeInRepoBranch(ctx, pull.BaseRepoID, pull.BaseBranch, baseRepoPerm, user)
|
|
if err != nil {
|
|
return ret, err
|
|
}
|
|
if userAllowedToMergePR {
|
|
// the user is maintainer (can merge PR), and this PR is allowed to be edited by maintainers,
|
|
// then the user should inherit the PR poster's push/rebase permission for the head branch
|
|
if err := pull.LoadIssue(ctx); err != nil {
|
|
return ret, err
|
|
}
|
|
if err := pull.Issue.LoadPoster(ctx); err != nil {
|
|
return ret, err
|
|
}
|
|
posterPushAllowed, posterRebaseAllowed, err := isUserAllowedToPushOrForcePushInRepoBranch(ctx, pull.Issue.Poster, pull.HeadRepo, pull.HeadBranch)
|
|
if err != nil {
|
|
return ret, err
|
|
}
|
|
if !ret.MergeAllowed {
|
|
ret.MergeAllowed = posterPushAllowed
|
|
}
|
|
if !ret.RebaseAllowed {
|
|
ret.RebaseAllowed = posterRebaseAllowed
|
|
}
|
|
}
|
|
}
|
|
|
|
// 5. apply base repository's update configuration; it is a config on the base repo,
|
|
// but it controls the head (fork) repo's "Update" behavior.
|
|
prConfig := prBaseUnit.PullRequestsConfig()
|
|
ret.MergeAllowed = ret.MergeAllowed && prConfig.AllowMergeUpdate
|
|
ret.RebaseAllowed = ret.RebaseAllowed && prConfig.AllowRebaseUpdate
|
|
ret.DefaultUpdateStyle = prConfig.DefaultUpdateStyle
|
|
return ret, nil
|
|
}
|
|
|
|
func syncCommitDivergence(ctx context.Context, pr *issues_model.PullRequest) error {
|
|
if err := pr.LoadBaseRepo(ctx); err != nil {
|
|
return err
|
|
}
|
|
divergence, err := git.GetDivergingCommits(ctx, pr.BaseRepo, pr.BaseBranch, pr.GetGitHeadRefName())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return pr.UpdateCommitDivergence(ctx, divergence.Ahead, divergence.Behind)
|
|
}
|