forgejo/services/agit/agit.go
Lunny Xiao 6905540088
Use the database object format name but not read from git repoisitory everytime and fix possible migration wrong objectformat when migrating a sha256 repository (#29294)
Now we can get object format name from git command line or from the
database repository table. Assume the column is right, we don't need to
read from git command line every time.

This also fixed a possible bug that the object format is wrong when
migrating a sha256 repository from external.

<img width="658" alt="image"
src="https://github.com/go-gitea/gitea/assets/81045/6e9a9dcf-13bf-4267-928b-6bf2c2560423">

(cherry picked from commit b79c30435f439af8243ee281310258cdf141e27b)

Conflicts:
	routers/web/repo/blame.go
	services/agit/agit.go
	context
2024-02-26 22:30:26 +01:00

259 lines
9 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/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.GitPushOptions["topic"]
_, forcePush := opts.GitPushOptions["force-push"]
title, hasTitle := opts.GitPushOptions["title"]
description, hasDesc := opts.GitPushOptions["description"]
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
}