初始提交: Gitea 项目代码
This commit is contained in:
@@ -0,0 +1,324 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package agit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
git_model "gitea.dev/models/git"
|
||||
issues_model "gitea.dev/models/issues"
|
||||
repo_model "gitea.dev/models/repo"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/git"
|
||||
"gitea.dev/modules/git/gitcmd"
|
||||
"gitea.dev/modules/gitrepo"
|
||||
"gitea.dev/modules/log"
|
||||
"gitea.dev/modules/private"
|
||||
"gitea.dev/modules/setting"
|
||||
"gitea.dev/modules/util"
|
||||
notify_service "gitea.dev/services/notify"
|
||||
pull_service "gitea.dev/services/pull"
|
||||
)
|
||||
|
||||
func parseAgitPushOptionValue(s string) string {
|
||||
if base64Value, ok := strings.CutPrefix(s, "{base64}"); ok {
|
||||
decoded, err := base64.StdEncoding.DecodeString(base64Value)
|
||||
return util.Iif(err == nil, string(decoded), s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func GetAgitBranchInfo(ctx context.Context, repoID int64, baseBranchName string) (string, string, error) {
|
||||
baseBranchExist, err := git_model.IsBranchExist(ctx, repoID, baseBranchName)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if baseBranchExist {
|
||||
return baseBranchName, "", nil
|
||||
}
|
||||
|
||||
// try match <target-branch>/<topic-branch>
|
||||
// refs/for have been trimmed to get baseBranchName
|
||||
for p, v := range baseBranchName {
|
||||
if v != '/' {
|
||||
continue
|
||||
}
|
||||
|
||||
baseBranchExist, err := git_model.IsBranchExist(ctx, repoID, baseBranchName[:p])
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if baseBranchExist {
|
||||
return baseBranchName[:p], baseBranchName[p+1:], nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", "", util.NewNotExistErrorf("base branch does not exist")
|
||||
}
|
||||
|
||||
// ProcReceive handle proc receive work
|
||||
func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, opts *private.HookOptions) ([]private.HookProcReceiveRefResult, error) {
|
||||
results := make([]private.HookProcReceiveRefResult, 0, len(opts.OldCommitIDs))
|
||||
forcePush := opts.GitPushOptions.Bool(private.GitPushOptionForcePush)
|
||||
topicBranch := opts.GitPushOptions["topic"]
|
||||
|
||||
// some options are base64-encoded with "{base64}" prefix if they contain new lines
|
||||
// other agit push options like "issue", "reviewer" and "cc" are not supported
|
||||
title := parseAgitPushOptionValue(opts.GitPushOptions["title"])
|
||||
description := parseAgitPushOptionValue(opts.GitPushOptions["description"])
|
||||
|
||||
objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
|
||||
userName := strings.ToLower(opts.UserName)
|
||||
|
||||
pusher, err := user_model.GetUserByID(ctx, opts.UserID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user. Error: %w", err)
|
||||
}
|
||||
|
||||
for i := range opts.OldCommitIDs {
|
||||
if opts.NewCommitIDs[i] == objectFormat.EmptyObjectID().String() {
|
||||
results = append(results, private.HookProcReceiveRefResult{
|
||||
OriginalRef: opts.RefFullNames[i],
|
||||
OldOID: opts.OldCommitIDs[i],
|
||||
NewOID: opts.NewCommitIDs[i],
|
||||
Err: "Can't delete not exist branch",
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if !opts.RefFullNames[i].IsFor() {
|
||||
results = append(results, private.HookProcReceiveRefResult{
|
||||
IsNotMatched: true,
|
||||
OriginalRef: opts.RefFullNames[i],
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
baseBranchName, currentTopicBranch, err := GetAgitBranchInfo(ctx, repo.ID, opts.RefFullNames[i].ForBranchName())
|
||||
if err != nil {
|
||||
if !errors.Is(err, util.ErrNotExist) {
|
||||
return nil, fmt.Errorf("failed to get branch information. Error: %w", err)
|
||||
}
|
||||
// If branch does not exist, we can continue
|
||||
results = append(results, private.HookProcReceiveRefResult{
|
||||
OriginalRef: opts.RefFullNames[i],
|
||||
OldOID: opts.OldCommitIDs[i],
|
||||
NewOID: opts.NewCommitIDs[i],
|
||||
Err: "base-branch does not exist",
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if len(topicBranch) == 0 && len(currentTopicBranch) == 0 {
|
||||
results = append(results, private.HookProcReceiveRefResult{
|
||||
OriginalRef: opts.RefFullNames[i],
|
||||
OldOID: opts.OldCommitIDs[i],
|
||||
NewOID: opts.NewCommitIDs[i],
|
||||
Err: "topic-branch is not set",
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if len(currentTopicBranch) == 0 {
|
||||
currentTopicBranch = topicBranch
|
||||
}
|
||||
|
||||
// because different user maybe want to use same topic,
|
||||
// So it's better to make sure the topic branch name
|
||||
// has username prefix
|
||||
var headBranch string
|
||||
if !strings.HasPrefix(currentTopicBranch, userName+"/") {
|
||||
headBranch = userName + "/" + currentTopicBranch
|
||||
} else {
|
||||
headBranch = currentTopicBranch
|
||||
}
|
||||
|
||||
pr, err := issues_model.GetUnmergedPullRequest(ctx, repo.ID, repo.ID, headBranch, baseBranchName, issues_model.PullRequestFlowAGit)
|
||||
if err != nil {
|
||||
if !issues_model.IsErrPullRequestNotExist(err) {
|
||||
return nil, fmt.Errorf("failed to get unmerged agit flow pull request in repository: %s Error: %w", repo.FullName(), err)
|
||||
}
|
||||
|
||||
var commit *git.Commit
|
||||
if title == "" || description == "" {
|
||||
commit, err = gitRepo.GetCommit(opts.NewCommitIDs[i])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get commit %s in repository: %s Error: %w", opts.NewCommitIDs[i], repo.FullName(), err)
|
||||
}
|
||||
// create a new pull request
|
||||
if title == "" {
|
||||
title = commit.MessageTitle()
|
||||
}
|
||||
if description == "" {
|
||||
description = commit.MessageBody()
|
||||
}
|
||||
}
|
||||
|
||||
prIssue := &issues_model.Issue{
|
||||
RepoID: repo.ID,
|
||||
Title: title,
|
||||
PosterID: pusher.ID,
|
||||
Poster: pusher,
|
||||
IsPull: true,
|
||||
Content: description,
|
||||
}
|
||||
|
||||
pr := &issues_model.PullRequest{
|
||||
HeadRepoID: repo.ID,
|
||||
BaseRepoID: repo.ID,
|
||||
HeadBranch: headBranch,
|
||||
HeadCommitID: opts.NewCommitIDs[i],
|
||||
BaseBranch: baseBranchName,
|
||||
HeadRepo: repo,
|
||||
BaseRepo: repo,
|
||||
MergeBase: "",
|
||||
Type: issues_model.PullRequestGitea,
|
||||
Flow: issues_model.PullRequestFlowAGit,
|
||||
}
|
||||
prOpts := &pull_service.NewPullRequestOptions{
|
||||
Repo: repo,
|
||||
Issue: prIssue,
|
||||
PullRequest: pr,
|
||||
}
|
||||
if err := pull_service.NewPullRequest(ctx, prOpts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Trace("Pull request created: %d/%d", repo.ID, prIssue.ID)
|
||||
|
||||
results = append(results, private.HookProcReceiveRefResult{
|
||||
Ref: pr.GetGitHeadRefName(),
|
||||
OriginalRef: opts.RefFullNames[i],
|
||||
OldOID: objectFormat.EmptyObjectID().String(),
|
||||
NewOID: opts.NewCommitIDs[i],
|
||||
IsCreatePR: false, // AGit always creates a pull request so there is no point in prompting user to create one
|
||||
URL: fmt.Sprintf("%s/pulls/%d", repo.HTMLURL(), pr.Index),
|
||||
ShouldShowMessage: setting.Git.PullRequestPushMessage && repo.AllowsPulls(ctx),
|
||||
HeadBranch: headBranch,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// update exist pull request
|
||||
if err := pr.LoadBaseRepo(ctx); err != nil {
|
||||
return nil, fmt.Errorf("unable to load base repository for PR[%d] Error: %w", pr.ID, err)
|
||||
}
|
||||
|
||||
oldCommitID, err := gitRepo.GetRefCommitID(pr.GetGitHeadRefName())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get ref commit id in base repository for PR[%d] Error: %w", pr.ID, err)
|
||||
}
|
||||
|
||||
if oldCommitID == opts.NewCommitIDs[i] {
|
||||
results = append(results, private.HookProcReceiveRefResult{
|
||||
OriginalRef: opts.RefFullNames[i],
|
||||
OldOID: opts.OldCommitIDs[i],
|
||||
NewOID: opts.NewCommitIDs[i],
|
||||
Err: "new commit is same with old commit",
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if !forcePush.Value() {
|
||||
output, _, err := gitrepo.RunCmdString(ctx, repo,
|
||||
gitcmd.NewCommand("rev-list", "--max-count=1").
|
||||
AddDynamicArguments(oldCommitID, "^"+opts.NewCommitIDs[i]),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to detect force push: %w", err)
|
||||
} else if len(output) > 0 {
|
||||
results = append(results, private.HookProcReceiveRefResult{
|
||||
OriginalRef: opts.RefFullNames[i],
|
||||
OldOID: opts.OldCommitIDs[i],
|
||||
NewOID: opts.NewCommitIDs[i],
|
||||
Err: "request `force-push` push option",
|
||||
})
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Store old commit ID for review staleness checking
|
||||
oldHeadCommitID := pr.HeadCommitID
|
||||
|
||||
pr.HeadCommitID = opts.NewCommitIDs[i]
|
||||
if err = pull_service.UpdateRef(ctx, pr); err != nil {
|
||||
return nil, fmt.Errorf("failed to update pull ref. Error: %w", err)
|
||||
}
|
||||
|
||||
// Mark existing reviews as stale when PR content changes (same as regular GitHub flow)
|
||||
if oldHeadCommitID != opts.NewCommitIDs[i] {
|
||||
if err := issues_model.MarkReviewsAsStale(ctx, pr.IssueID); err != nil {
|
||||
log.Error("MarkReviewsAsStale: %v", err)
|
||||
}
|
||||
|
||||
// Dismiss all approval reviews if protected branch rule item enabled
|
||||
pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pr.BaseRepoID, pr.BaseBranch)
|
||||
if err != nil {
|
||||
log.Error("GetFirstMatchProtectedBranchRule: %v", err)
|
||||
}
|
||||
if pb != nil && pb.DismissStaleApprovals {
|
||||
if err := pull_service.DismissApprovalReviews(ctx, pusher, pr); err != nil {
|
||||
log.Error("DismissApprovalReviews: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Mark reviews for the new commit as not stale
|
||||
if err := issues_model.MarkReviewsAsNotStale(ctx, pr.IssueID, opts.NewCommitIDs[i]); err != nil {
|
||||
log.Error("MarkReviewsAsNotStale: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
pull_service.StartPullRequestCheckImmediately(ctx, pr)
|
||||
err = pr.LoadIssue(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load pull issue. Error: %w", err)
|
||||
}
|
||||
|
||||
isForcePush := forcePush.Value()
|
||||
comment, commentCreated, err := pull_service.CreatePushPullComment(ctx, pusher, pr, oldCommitID, opts.NewCommitIDs[i], isForcePush)
|
||||
if err != nil {
|
||||
log.Error("CreatePushPullComment: %v", err)
|
||||
} else if commentCreated {
|
||||
notify_service.PullRequestPushCommits(ctx, pusher, pr, comment)
|
||||
}
|
||||
notify_service.PullRequestSynchronized(ctx, pusher, pr, oldCommitID, opts.NewCommitIDs[i])
|
||||
|
||||
results = append(results, private.HookProcReceiveRefResult{
|
||||
OldOID: oldCommitID,
|
||||
NewOID: opts.NewCommitIDs[i],
|
||||
Ref: pr.GetGitHeadRefName(),
|
||||
OriginalRef: opts.RefFullNames[i],
|
||||
IsForcePush: isForcePush,
|
||||
IsCreatePR: false,
|
||||
URL: fmt.Sprintf("%s/pulls/%d", repo.HTMLURL(), pr.Index),
|
||||
ShouldShowMessage: setting.Git.PullRequestPushMessage && repo.AllowsPulls(ctx),
|
||||
})
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// UserNameChanged handle user name change for agit flow pull
|
||||
func UserNameChanged(ctx context.Context, user *user_model.User, newName string) error {
|
||||
pulls, err := issues_model.GetAllUnmergedAgitPullRequestByPoster(ctx, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newName = strings.ToLower(newName)
|
||||
|
||||
for _, pull := range pulls {
|
||||
pull.HeadBranch = strings.TrimPrefix(pull.HeadBranch, user.LowerName+"/")
|
||||
pull.HeadBranch = newName + "/" + pull.HeadBranch
|
||||
if err = pull.UpdateCols(ctx, "head_branch"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user