mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-11-01 13:01:15 +00:00
5561e80b04
If a repository has git config --add push.pushOption submit=".sourcehut/*.yml" it failed when pushed because of the unknown submit push option. It will be ignored instead. Filtering out the push options is done in an earlier stage, when the hook command runs, before it submits the options map to the private endpoint. * move all the push options logic to modules/git/pushoptions * add 100% test coverage for modules/git/pushoptions Test coverage for the code paths from which code was moved to the modules/git/pushoptions package: * cmd/hook.go:runHookPreReceive * routers/private/hook_pre_receive.go:validatePushOptions tests/integration/git_push_test.go:TestOptionsGitPush runs through both. The test verifying the option is rejected was removed and, if added again, will fail because the option is now ignored instead of being rejected. * cmd/hook.go:runHookProcReceive * services/agit/agit.go:ProcReceive tests/integration/git_test.go: doCreateAgitFlowPull runs through both. It uses variations of AGit related push options. * cmd/hook.go:runHookPostReceive * routers/private/hook_post_receive.go:HookPostReceive tests/integration/git_test.go:doPushCreate called by TestGit/HTTP/sha1/PushCreate runs through both. Note that although it provides coverage for this code path it does not use push options. Fixes: https://codeberg.org/forgejo/forgejo/issues/3651
260 lines
9.2 KiB
Go
260 lines
9.2 KiB
Go
// Copyright 2021 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package agit
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
issues_model "code.gitea.io/gitea/models/issues"
|
|
repo_model "code.gitea.io/gitea/models/repo"
|
|
user_model "code.gitea.io/gitea/models/user"
|
|
"code.gitea.io/gitea/modules/git"
|
|
"code.gitea.io/gitea/modules/git/pushoptions"
|
|
"code.gitea.io/gitea/modules/log"
|
|
"code.gitea.io/gitea/modules/private"
|
|
notify_service "code.gitea.io/gitea/services/notify"
|
|
pull_service "code.gitea.io/gitea/services/pull"
|
|
)
|
|
|
|
// 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))
|
|
|
|
topicBranch, _ := opts.GetGitPushOptions().GetString(pushoptions.AgitTopic)
|
|
_, forcePush := opts.GetGitPushOptions().GetString(pushoptions.AgitForcePush)
|
|
title, hasTitle := opts.GetGitPushOptions().GetString(pushoptions.AgitTitle)
|
|
description, hasDesc := opts.GetGitPushOptions().GetString(pushoptions.AgitDescription)
|
|
|
|
objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
|
|
|
|
pusher, err := user_model.GetUserByID(ctx, opts.UserID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get user[%d]: %w", opts.UserID, err)
|
|
}
|
|
|
|
for i := range opts.OldCommitIDs {
|
|
// Avoid processing this change if the new commit is empty.
|
|
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: "Cannot delete a non-existent branch.",
|
|
})
|
|
continue
|
|
}
|
|
|
|
// Only process references that are in the form of refs/for/
|
|
if !opts.RefFullNames[i].IsFor() {
|
|
results = append(results, private.HookProcReceiveRefResult{
|
|
IsNotMatched: true,
|
|
OriginalRef: opts.RefFullNames[i],
|
|
})
|
|
continue
|
|
}
|
|
|
|
// Get the anything after the refs/for/ prefix.
|
|
baseBranchName := opts.RefFullNames[i].ForBranchName()
|
|
curentTopicBranch := topicBranch
|
|
|
|
// If the reference was given in the format of refs/for/<target-branch>/<topic-branch>,
|
|
// where <target-branch> and <topic-branch> can contain slashes, we need to iteratively
|
|
// search for what the target and topic branch is.
|
|
if !gitRepo.IsBranchExist(baseBranchName) {
|
|
for p, v := range baseBranchName {
|
|
if v == '/' && gitRepo.IsBranchExist(baseBranchName[:p]) && p != len(baseBranchName)-1 {
|
|
curentTopicBranch = baseBranchName[p+1:]
|
|
baseBranchName = baseBranchName[:p]
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(curentTopicBranch) == 0 {
|
|
results = append(results, private.HookProcReceiveRefResult{
|
|
OriginalRef: opts.RefFullNames[i],
|
|
OldOID: opts.OldCommitIDs[i],
|
|
NewOID: opts.NewCommitIDs[i],
|
|
Err: "The topic-branch option is not set",
|
|
})
|
|
continue
|
|
}
|
|
|
|
// Include the user's name in the head branch, to avoid conflicts
|
|
// with other users.
|
|
headBranch := curentTopicBranch
|
|
userName := strings.ToLower(opts.UserName)
|
|
if !strings.HasPrefix(curentTopicBranch, userName+"/") {
|
|
headBranch = userName + "/" + curentTopicBranch
|
|
}
|
|
|
|
// Check if a AGit pull request already exist for this branch.
|
|
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 %q: %w", repo.FullName(), err)
|
|
}
|
|
|
|
// Check if the changes are already in the target branch.
|
|
stdout, _, gitErr := git.NewCommand(ctx, "branch", "--contains").AddDynamicArguments(opts.NewCommitIDs[i], baseBranchName).RunStdString(&git.RunOpts{Dir: repo.RepoPath()})
|
|
if gitErr != nil {
|
|
return nil, fmt.Errorf("failed to check if the target branch already contains the new commit in repository %q: %w", repo.FullName(), err)
|
|
}
|
|
if len(stdout) > 0 {
|
|
results = append(results, private.HookProcReceiveRefResult{
|
|
OriginalRef: opts.RefFullNames[i],
|
|
OldOID: opts.OldCommitIDs[i],
|
|
NewOID: opts.NewCommitIDs[i],
|
|
Err: "The target branch already contains this commit",
|
|
})
|
|
continue
|
|
}
|
|
|
|
// Automatically fill out the title and the description from the first commit.
|
|
shouldGetCommit := len(title) == 0 || len(description) == 0
|
|
|
|
var commit *git.Commit
|
|
if shouldGetCommit {
|
|
commit, err = gitRepo.GetCommit(opts.NewCommitIDs[i])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get commit %s in repository %q: %w", opts.NewCommitIDs[i], repo.FullName(), err)
|
|
}
|
|
}
|
|
if !hasTitle || len(title) == 0 {
|
|
title = strings.Split(commit.CommitMessage, "\n")[0]
|
|
}
|
|
if !hasDesc || len(description) == 0 {
|
|
_, description, _ = strings.Cut(commit.CommitMessage, "\n\n")
|
|
}
|
|
|
|
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,
|
|
}
|
|
|
|
if err := pull_service.NewPullRequest(ctx, repo, prIssue, []int64{}, []string{}, pr, []int64{}); err != nil {
|
|
return nil, fmt.Errorf("unable to create new pull request: %w", err)
|
|
}
|
|
|
|
log.Trace("Pull request created: %d/%d", repo.ID, prIssue.ID)
|
|
|
|
results = append(results, private.HookProcReceiveRefResult{
|
|
Ref: pr.GetGitRefName(),
|
|
OriginalRef: opts.RefFullNames[i],
|
|
OldOID: objectFormat.EmptyObjectID().String(),
|
|
NewOID: opts.NewCommitIDs[i],
|
|
})
|
|
continue
|
|
}
|
|
|
|
// Update an existing pull request.
|
|
if err := pr.LoadBaseRepo(ctx); err != nil {
|
|
return nil, fmt.Errorf("unable to load base repository for PR[%d]: %w", pr.ID, err)
|
|
}
|
|
|
|
oldCommitID, err := gitRepo.GetRefCommitID(pr.GetGitRefName())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to get commit id of reference[%s] in base repository for PR[%d]: %w", pr.GetGitRefName(), pr.ID, err)
|
|
}
|
|
|
|
// Do not process this change if nothing was changed.
|
|
if oldCommitID == opts.NewCommitIDs[i] {
|
|
results = append(results, private.HookProcReceiveRefResult{
|
|
OriginalRef: opts.RefFullNames[i],
|
|
OldOID: opts.OldCommitIDs[i],
|
|
NewOID: opts.NewCommitIDs[i],
|
|
Err: "The new commit is the same as the old commit",
|
|
})
|
|
continue
|
|
}
|
|
|
|
// If the force push option was not set, ensure that this change isn't a force push.
|
|
if !forcePush {
|
|
output, _, err := git.NewCommand(ctx, "rev-list", "--max-count=1").AddDynamicArguments(oldCommitID, "^"+opts.NewCommitIDs[i]).RunStdString(&git.RunOpts{Dir: repo.RepoPath(), Env: os.Environ()})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to detect a 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: "Updates were rejected because the tip of your current branch is behind its remote counterpart. If this is intentional, set the `force-push` option by adding `-o force-push=true` to your `git push` command.",
|
|
})
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Set the new commit as reference of the pull request.
|
|
pr.HeadCommitID = opts.NewCommitIDs[i]
|
|
if err = pull_service.UpdateRef(ctx, pr); err != nil {
|
|
return nil, fmt.Errorf("failed to update the reference of the pull request: %w", err)
|
|
}
|
|
|
|
// Add the pull request to the merge conflicting checker queue.
|
|
pull_service.AddToTaskQueue(ctx, pr)
|
|
|
|
if err := pr.LoadIssue(ctx); err != nil {
|
|
return nil, fmt.Errorf("failed to load the issue of the pull request: %w", err)
|
|
}
|
|
|
|
// Create and notify about the new commits.
|
|
comment, err := pull_service.CreatePushPullComment(ctx, pusher, pr, oldCommitID, opts.NewCommitIDs[i])
|
|
if err == nil && comment != nil {
|
|
notify_service.PullRequestPushCommits(ctx, pusher, pr, comment)
|
|
}
|
|
notify_service.PullRequestSynchronized(ctx, pusher, pr)
|
|
isForcePush := comment != nil && comment.IsForcePush
|
|
|
|
results = append(results, private.HookProcReceiveRefResult{
|
|
OldOID: oldCommitID,
|
|
NewOID: opts.NewCommitIDs[i],
|
|
Ref: pr.GetGitRefName(),
|
|
OriginalRef: opts.RefFullNames[i],
|
|
IsForcePush: isForcePush,
|
|
})
|
|
}
|
|
|
|
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
|
|
}
|