Files
zeekayandHanzo Dev 27cde39e36 Name every remaining identifier, path and default for this product
Storage buckets, indexer names, the bleve analyzer, the lock prefix, SSH host
keys, the markdown front-matter key, the container data paths, the RPM repo id
and the API security scheme all read as this product now, and app.example.ini
matches the code again. The upstream maintainer list and the changelog
generator config went with the release history they belonged to, and the
citation comments state the fact rather than link the issue.

Three inert rules turn out to have been inert because they named a module path
this tree does not use: gci grouped no first-party imports under prefix(), and
depguard let models/migrations import models and modules/structs. They now name
github.com/hanzoai/git. The config example also documented .gitea/workflows
while the code has read .hanzo/workflows for some time.

The authorized_keys marker follows the delegate-hook precedent: the new name is
written and the old one is still recognised, because a marker we do not know is
copied through as a hand-added key and leaves a revoked key authorized. The
stored user_setting key keeps its value; only a migration may change that.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 19:14:35 -07:00

222 lines
7.8 KiB
Go

// Copyright 2026 Hanzo AI, Inc. All rights reserved.
// SPDX-License-Identifier: MIT
package pull
import (
"context"
"errors"
"fmt"
git_model "github.com/hanzoai/git/models/git"
issues_model "github.com/hanzoai/git/models/issues"
access_model "github.com/hanzoai/git/models/perm/access"
repo_model "github.com/hanzoai/git/models/repo"
"github.com/hanzoai/git/models/unit"
user_model "github.com/hanzoai/git/models/user"
"github.com/hanzoai/git/modules/gitrepo"
"github.com/hanzoai/git/modules/globallock"
"github.com/hanzoai/git/modules/log"
"github.com/hanzoai/git/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 := gitrepo.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() {
// Inherited from an earlier change and kept only because nothing has
// shown it to be wrong.
// 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 := gitrepo.GetDivergingCommits(ctx, pr.BaseRepo, pr.BaseBranch, pr.GetGitHeadRefName())
if err != nil {
return err
}
return pr.UpdateCommitDivergence(ctx, divergence.Ahead, divergence.Behind)
}