mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
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]>
222 lines
7.8 KiB
Go
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)
|
|
}
|