When project is advanced, and we have other package changes to same project, the project git changes need to be rebased. The simplest way of doing this is to skip all the submodule conflicts and re-create them. This allows the submodules changes to be mergeable again.
491 lines
16 KiB
Go
491 lines
16 KiB
Go
package main
|
|
|
|
//go:generate mockgen -source=pr_processor.go -destination=mock/pr_processor.go -typed
|
|
|
|
import (
|
|
"fmt"
|
|
"path"
|
|
"runtime/debug"
|
|
"slices"
|
|
"strings"
|
|
|
|
"github.com/opentracing/opentracing-go/log"
|
|
"src.opensuse.org/autogits/common"
|
|
"src.opensuse.org/autogits/common/gitea-generated/models"
|
|
)
|
|
|
|
func prGitBranchNameForPR(repo string, prNo int) string {
|
|
return fmt.Sprintf("PR_%s#%d", repo, prNo)
|
|
}
|
|
|
|
func verifyRepositoryConfiguration(repo *models.Repository) error {
|
|
if repo.AutodetectManualMerge && repo.AllowManualMerge {
|
|
return nil
|
|
}
|
|
|
|
// modify repo to allow above
|
|
common.LogDebug("Adjusting repo to accept manual merges:", repo.Owner.UserName+"/"+repo.Name)
|
|
_, err := Gitea.SetRepoOptions(repo.Owner.UserName, repo.Name, true)
|
|
if err != nil {
|
|
common.LogError("Failed to set repo to manual merges:", err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func updateSubmoduleInPR(submodule, headSha string, git common.Git) {
|
|
common.LogDebug("updating submodule", submodule, "to HEAD", headSha)
|
|
// NOTE: this can fail if current PrjGit is pointing to outdated, GC'ed commit
|
|
// as long as we can update to newer one later, we are still OK
|
|
git.GitExec(common.DefaultGitPrj, "submodule", "update", "--init", "--checkout", "--depth", "1", submodule)
|
|
common.PanicOnError(git.GitExec(path.Join(common.DefaultGitPrj, submodule), "fetch", "--depth", "1", "origin", headSha))
|
|
common.PanicOnError(git.GitExec(path.Join(common.DefaultGitPrj, submodule), "checkout", headSha))
|
|
}
|
|
|
|
type PRProcessor struct {
|
|
config *common.AutogitConfig
|
|
git common.Git
|
|
}
|
|
|
|
func AllocatePRProcessor(req *common.PullRequestWebhookEvent, configs common.AutogitConfigs) (*PRProcessor, error) {
|
|
org := req.Pull_Request.Base.Repo.Owner.Username
|
|
repo := req.Pull_Request.Base.Repo.Name
|
|
id := req.Pull_Request.Number
|
|
|
|
branch := req.Pull_Request.Base.Ref
|
|
|
|
PRstr := fmt.Sprintf("%s/%s#%d", org, repo, id)
|
|
common.LogInfo("*** Starting processing PR:", PRstr, "branch:", branch)
|
|
|
|
config := configs.GetPrjGitConfig(org, repo, branch)
|
|
if config == nil {
|
|
if req.Pull_Request.Base.Repo.Default_Branch == branch {
|
|
common.LogDebug("Default branch submission...", org, repo)
|
|
config = configs.GetPrjGitConfig(org, repo, "")
|
|
}
|
|
}
|
|
if config == nil {
|
|
common.LogError("Cannot find config for PR.")
|
|
return nil, fmt.Errorf("Cannot find config for PR")
|
|
}
|
|
|
|
common.LogDebug("found config", config)
|
|
if config == nil {
|
|
common.LogError("Cannot find config for branch '%s'", req.Pull_Request.Base.Ref)
|
|
return nil, fmt.Errorf("Cannot find config for branch '%s'", req.Pull_Request.Base.Ref)
|
|
}
|
|
|
|
git, err := GitHandler.CreateGitHandler(config.Organization)
|
|
if err != nil {
|
|
common.LogError("Cannot allocate GitHandler:", err)
|
|
return nil, fmt.Errorf("Error allocating GitHandler. Err: %w", err)
|
|
}
|
|
common.LogDebug("git path:", git.GetPath())
|
|
|
|
// git.GitExecOrPanic("", "config", "set", "--global", "advice.submoduleMergeConflict", "false")
|
|
// git.GitExecOrPanic("", "config", "set", "--global", "advice.mergeConflict", "false")
|
|
|
|
return &PRProcessor{
|
|
config: config,
|
|
git: git,
|
|
}, nil
|
|
}
|
|
|
|
func (pr *PRProcessor) SetSubmodulesToMatchPRSet(prset *common.PRSet) ([]string, []string, error) {
|
|
git := pr.git
|
|
subList, err := git.GitSubmoduleList(common.DefaultGitPrj, "HEAD")
|
|
if err != nil {
|
|
common.LogError("Error fetching submodule list for PrjGit", err)
|
|
return nil, nil, err
|
|
}
|
|
|
|
refs := make([]string, 0, len(prset.PRs))
|
|
title_refs := make([]string, 0, len(prset.PRs))
|
|
for _, pr := range prset.PRs {
|
|
if prset.IsPrjGitPR(pr.PR) {
|
|
continue
|
|
}
|
|
|
|
org := pr.PR.Base.Repo.Owner.UserName
|
|
repo := pr.PR.Base.Repo.Name
|
|
idx := pr.PR.Index
|
|
prHead := pr.PR.Head.Sha
|
|
revert := false
|
|
|
|
if pr.PR.State != "open" {
|
|
prjGitPR, err := prset.GetPrjGitPR()
|
|
if prjGitPR != nil {
|
|
// remove PR from PrjGit
|
|
var valid bool
|
|
if prHead, valid = git.GitSubmoduleCommitId(common.DefaultGitPrj, repo, prjGitPR.PR.MergeBase); !valid {
|
|
common.LogError("Failed fetching original submodule commit id for repo")
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
revert = true
|
|
}
|
|
|
|
// find 'repo' in the submodule list
|
|
submodule_found := false
|
|
for submodulePath, id := range subList {
|
|
if path.Base(submodulePath) == repo {
|
|
submodule_found = true
|
|
if id != prHead {
|
|
ref := fmt.Sprintf(common.PrPattern, org, repo, idx)
|
|
commitMsg := fmt.Sprintln("auto-created for", repo, "\n\nThis commit was autocreated by", GitAuthor, "referencing\n", ref)
|
|
|
|
if revert {
|
|
commitMsg = fmt.Sprintln("auto-created for", repo, "\n\nThis commit was autocreated by", GitAuthor, "removing\n", ref)
|
|
} else {
|
|
refs = append(refs, ref)
|
|
title_refs = append(title_refs, repo)
|
|
}
|
|
|
|
updateSubmoduleInPR(submodulePath, prHead, git)
|
|
status, err := git.GitStatus(common.DefaultGitPrj)
|
|
common.LogDebug("status:", status)
|
|
common.LogDebug("submodule", repo, " hash:", id, " -> ", prHead)
|
|
common.PanicOnError(err)
|
|
common.PanicOnError(git.GitExec(common.DefaultGitPrj, "commit", "-a", "-m", commitMsg))
|
|
}
|
|
submodule_found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !submodule_found {
|
|
common.LogError("Failed to find expected repo:", repo)
|
|
}
|
|
}
|
|
return title_refs, refs, nil
|
|
}
|
|
|
|
func (pr *PRProcessor) CreatePRjGitPR(prjGitPRbranch string, prset *common.PRSet) error {
|
|
git := pr.git
|
|
PrjGitOrg, PrjGitRepo, PrjGitBranch := prset.Config.GetPrjGit()
|
|
PrjGit, err := Gitea.GetRepository(PrjGitOrg, PrjGitRepo)
|
|
if err != nil {
|
|
common.LogError("Failed to fetch PrjGit repository data.", PrjGitOrg, PrjGitRepo, err)
|
|
return err
|
|
}
|
|
RemoteName, err := git.GitClone(common.DefaultGitPrj, PrjGitBranch, PrjGit.SSHURL)
|
|
common.PanicOnError(err)
|
|
git.GitExecOrPanic(common.DefaultGitPrj, "checkout", "-B", prjGitPRbranch, RemoteName+"/"+PrjGitBranch)
|
|
|
|
headCommit, err := git.GitBranchHead(common.DefaultGitPrj, prjGitPRbranch)
|
|
if err != nil {
|
|
common.LogError("Failed to fetch PrjGit branch", prjGitPRbranch, err)
|
|
return err
|
|
}
|
|
title_refs, refs, err := pr.SetSubmodulesToMatchPRSet(prset)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
newHeadCommit, err := git.GitBranchHead(common.DefaultGitPrj, prjGitPRbranch)
|
|
if err != nil {
|
|
common.LogError("Failed to fetch updated PrjGit branch", prjGitPRbranch, err)
|
|
return err
|
|
}
|
|
|
|
if !common.IsDryRun && headCommit != newHeadCommit {
|
|
common.PanicOnError(git.GitExec(common.DefaultGitPrj, "push", RemoteName, "+HEAD:"+prjGitPRbranch))
|
|
pr, err := Gitea.CreatePullRequestIfNotExist(PrjGit, prjGitPRbranch, PrjGitBranch,
|
|
"Forwarded PRs: "+strings.Join(title_refs, ", "),
|
|
fmt.Sprintf("This is a forwarded pull request by %s\nreferencing the following pull request(s):\n\n", GitAuthor)+strings.Join(refs, ", "),
|
|
)
|
|
if err != nil {
|
|
common.LogError("Error creating PrjGit PR:", err)
|
|
return err
|
|
}
|
|
Gitea.UpdatePullRequest(PrjGit.Owner.UserName, PrjGit.Name, pr.Index, &models.EditPullRequestOption{
|
|
RemoveDeadline: true,
|
|
})
|
|
|
|
prinfo := prset.AddPR(pr)
|
|
prinfo.RemoteName = RemoteName
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (pr *PRProcessor) RebaseAndSkipSubmoduleCommits(prset *common.PRSet, branch string) error {
|
|
git := pr.git
|
|
PrjGitPR, err := prset.GetPrjGitPR()
|
|
common.PanicOnError(err)
|
|
|
|
remoteBranch := PrjGitPR.RemoteName + "/" + branch
|
|
|
|
common.LogDebug("Rebasing on top of", remoteBranch)
|
|
for conflict := git.GitExec(common.DefaultGitPrj, "rebase", remoteBranch); conflict != nil; {
|
|
statuses, err := git.GitStatus(common.DefaultGitPrj)
|
|
if err != nil {
|
|
git.GitExecOrPanic(common.DefaultGitPrj, "rebase", "--abort")
|
|
common.PanicOnError(err)
|
|
}
|
|
for _, s := range statuses {
|
|
if s.SubmoduleChanges != "S..." {
|
|
git.GitExecOrPanic(common.DefaultGitPrj, "rebase", "--abort")
|
|
return fmt.Errorf("Unexpected conflict in rebase. %s", s)
|
|
}
|
|
}
|
|
conflict = git.GitExec(common.DefaultGitPrj, "rebase", "--skip")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (pr *PRProcessor) UpdatePrjGitPR(prset *common.PRSet) error {
|
|
_, _, PrjGitBranch := prset.Config.GetPrjGit()
|
|
PrjGitPR, err := prset.GetPrjGitPR()
|
|
if err != nil {
|
|
common.LogError("Updating PrjGitPR but not found?", err)
|
|
return err
|
|
}
|
|
|
|
git := pr.git
|
|
PrjGit := PrjGitPR.PR.Base.Repo
|
|
prjGitPRbranch := PrjGitPR.PR.Head.Name
|
|
|
|
PrjGitPR.RemoteName, err = git.GitClone(common.DefaultGitPrj, prjGitPRbranch, PrjGit.SSHURL)
|
|
common.PanicOnError(err)
|
|
git.GitExecOrPanic(common.DefaultGitPrj, "fetch", PrjGitPR.RemoteName, PrjGitBranch)
|
|
ExpectedMergeCommit, err := git.GitRemoteHead(common.DefaultGitPrj, PrjGitPR.RemoteName, PrjGitBranch)
|
|
|
|
forcePush := false
|
|
if ExpectedMergeCommit != PrjGitPR.PR.MergeBase {
|
|
common.PanicOnError(pr.RebaseAndSkipSubmoduleCommits(prset, PrjGitBranch))
|
|
forcePush = true
|
|
}
|
|
|
|
headCommit, err := git.GitBranchHead(common.DefaultGitPrj, prjGitPRbranch)
|
|
if err != nil {
|
|
common.LogError("Failed to fetch PrjGit branch", prjGitPRbranch, err)
|
|
return err
|
|
}
|
|
title_refs, refs, err := pr.SetSubmodulesToMatchPRSet(prset)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
newHeadCommit, err := git.GitBranchHead(common.DefaultGitPrj, prjGitPRbranch)
|
|
if err != nil {
|
|
common.LogError("Failed to fetch updated PrjGit branch", prjGitPRbranch, err)
|
|
return err
|
|
}
|
|
|
|
if !common.IsDryRun && headCommit != newHeadCommit {
|
|
params := []string{"push", PrjGitPR.RemoteName, "+HEAD:" + prjGitPRbranch}
|
|
if forcePush {
|
|
params = slices.Insert(params, 1, "-f")
|
|
}
|
|
common.PanicOnError(git.GitExec(common.DefaultGitPrj, params...))
|
|
|
|
// update PR
|
|
PrjGitTitle := "Forwarded PRs: " + strings.Join(title_refs, ", ")
|
|
PrjGitBody := fmt.Sprintf("This is a forwarded pull request by %s\nreferencing the following pull request(s):\n\n", GitAuthor) + strings.Join(refs, ", ")
|
|
|
|
Gitea.UpdatePullRequest(PrjGit.Owner.UserName, PrjGit.Name, PrjGitPR.PR.Index, &models.EditPullRequestOption{
|
|
RemoveDeadline: true,
|
|
Title: PrjGitTitle,
|
|
Body: PrjGitBody,
|
|
})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (pr *PRProcessor) Process(req *common.PullRequestWebhookEvent) error {
|
|
config := pr.config
|
|
git := pr.git
|
|
|
|
// requests against project are not handled here
|
|
common.LogInfo("processing opened PR:", req.Pull_Request.Url)
|
|
prOrg := req.Pull_Request.Base.Repo.Owner.Username
|
|
prRepo := req.Pull_Request.Base.Repo.Name
|
|
prNo := int(req.Pull_Request.Number)
|
|
|
|
common.LogError(req)
|
|
|
|
prset, err := common.FetchPRSet(CurrentUser.UserName, Gitea, prOrg, prRepo, req.Number, config)
|
|
if err != nil {
|
|
common.LogError("Cannot fetch PRSet:", err)
|
|
return err
|
|
}
|
|
common.LogInfo("fetched PRSet of size:", len(prset.PRs))
|
|
|
|
prjGitPRbranch := prGitBranchNameForPR(prRepo, prNo)
|
|
prjGitPR, err := prset.GetPrjGitPR()
|
|
if err == common.PRSet_PrjGitMissing {
|
|
common.LogDebug("Missing PrjGit. Need to create one...")
|
|
|
|
if err = pr.CreatePRjGitPR(prjGitPRbranch, prset); err != nil {
|
|
return err
|
|
}
|
|
} else if err == nil {
|
|
common.LogDebug("Found PrjGit PR:", common.PRtoString(prjGitPR.PR))
|
|
prjGitPRbranch = prjGitPR.PR.Head.Name
|
|
|
|
if prjGitPR.PR.State != "open" {
|
|
// close entire prset
|
|
common.LogInfo("PR State is closed:", prjGitPR.PR.State)
|
|
for _, pr := range prset.PRs {
|
|
if pr.PR.State == "open" {
|
|
org := pr.PR.Base.Repo.Owner.UserName
|
|
repo := pr.PR.Base.Repo.Name
|
|
idx := pr.PR.Index
|
|
Gitea.UpdatePullRequest(org, repo, idx, &models.EditPullRequestOption{
|
|
State: "closed",
|
|
})
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if err = pr.UpdatePrjGitPR(prset); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if prjGitPR == nil {
|
|
prjGitPR, err = prset.GetPrjGitPR()
|
|
if err != nil {
|
|
common.LogError("Error fetching PrjGitPR:", err)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
common.LogDebug("Updated PR")
|
|
|
|
// make sure that prjgit is consistent and only submodules that are to be *updated*
|
|
// reset anything that changed that is not part of the prset
|
|
// package removals/additions are *not* counted here
|
|
org, repo, branch := config.GetPrjGit()
|
|
if pr, err := prset.GetPrjGitPR(); err == nil {
|
|
common.LogDebug("Submodule parse begin")
|
|
orig_subs, err := git.GitSubmoduleList(common.DefaultGitPrj, pr.RemoteName+"/"+branch) // merge base must remote branch, checked in prjgit udate
|
|
common.PanicOnError(err)
|
|
new_subs, err := git.GitSubmoduleList(common.DefaultGitPrj, pr.PR.Head.Sha)
|
|
common.PanicOnError(err)
|
|
common.LogDebug("Submodule parse done")
|
|
|
|
reset_submodule := func(submodule, sha string) {
|
|
updateSubmoduleInPR(submodule, sha, git)
|
|
}
|
|
|
|
for path, commit := range new_subs {
|
|
if old, ok := orig_subs[path]; ok && old != commit {
|
|
found := false
|
|
for _, pr := range prset.PRs {
|
|
if pr.PR.Base.Repo.Name == path && commit == pr.PR.Head.Sha {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
reset_submodule(path, old)
|
|
}
|
|
}
|
|
}
|
|
|
|
stats, err := git.GitStatus(common.DefaultGitPrj)
|
|
common.PanicOnError(err)
|
|
if len(stats) > 0 {
|
|
git.GitExecOrPanic(common.DefaultGitPrj, "commit", "-a", "-m", "Sync submodule updates with PR-set")
|
|
git.GitExecOrPanic(common.DefaultGitPrj, "submodule", "deinit", "--all", "--force")
|
|
if !common.IsDryRun {
|
|
git.GitExecOrPanic(common.DefaultGitPrj, "push")
|
|
}
|
|
}
|
|
}
|
|
|
|
common.LogDebug(" num of reviewers:", len(prjGitPR.PR.RequestedReviewers))
|
|
maintainers, err := common.FetchProjectMaintainershipData(Gitea, org, repo, branch)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// handle case where PrjGit PR is only one left and there are no changes, then we can just close the PR
|
|
if len(prset.PRs) == 1 && prset.PRs[0] == prjGitPR && prjGitPR.PR.User.UserName == prset.BotUser {
|
|
common.LogDebug(" --> checking if superflous PR")
|
|
diff, err := git.GitDiff(common.DefaultGitPrj, prjGitPR.PR.MergeBase, prjGitPR.PR.Head.Sha)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(diff) == 0 {
|
|
common.LogInfo("PR is no-op and can be closed. Closing.")
|
|
if !common.IsDryRun {
|
|
Gitea.AddComment(prjGitPR.PR, "Pull request no longer contains any changes. Closing.")
|
|
_, err = Gitea.UpdatePullRequest(prjGitPR.PR.Base.Repo.Owner.UserName, prjGitPR.PR.Base.Repo.Name, prjGitPR.PR.Index, &models.EditPullRequestOption{
|
|
State: "closed",
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
common.LogDebug(" --> NOT superflous PR")
|
|
}
|
|
|
|
prset.AssignReviewers(Gitea, maintainers)
|
|
for _, pr := range prset.PRs {
|
|
if err := verifyRepositoryConfiguration(pr.PR.Base.Repo); err != nil {
|
|
common.LogError("Cannot set manual merge... aborting processing")
|
|
return err
|
|
}
|
|
}
|
|
|
|
common.LogInfo("Consistent PRSet:", prset.IsConsistent())
|
|
common.LogInfo("Reviewed?", prset.IsApproved(Gitea, maintainers))
|
|
if prset.IsConsistent() && prset.IsApproved(Gitea, maintainers) {
|
|
common.LogInfo("Merging...")
|
|
if err = prset.Merge(Gitea, git); err != nil {
|
|
common.LogError("merge error:", err)
|
|
}
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
type RequestProcessor struct {
|
|
configuredRepos map[string][]*common.AutogitConfig
|
|
}
|
|
|
|
func ProcesPullRequest(req *common.PullRequestWebhookEvent, configs []*common.AutogitConfig) error {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
common.LogInfo("panic cought --- recovered")
|
|
common.LogError(string(debug.Stack()))
|
|
}
|
|
}()
|
|
|
|
if len(configs) < 1 {
|
|
// ignoring pull request against unconfigured project (could be just regular sources?)
|
|
return nil
|
|
}
|
|
|
|
pr, err := AllocatePRProcessor(req, configs)
|
|
if err != nil {
|
|
log.Error(err)
|
|
return err
|
|
}
|
|
defer pr.git.Close()
|
|
|
|
switch req.Action {
|
|
case "opened", "reopened", "synchronized", "edited", "closed", "reviewed":
|
|
return pr.Process(req)
|
|
}
|
|
|
|
common.LogError("Unhandled pull request action:", req.Action)
|
|
return nil
|
|
}
|
|
|
|
func (w *RequestProcessor) ProcessFunc(request *common.Request) error {
|
|
req, ok := request.Data.(*common.PullRequestWebhookEvent)
|
|
if !ok {
|
|
common.LogError("*** Invalid data format for PR processing.")
|
|
return fmt.Errorf("*** Invalid data format for PR processing.")
|
|
}
|
|
|
|
org := req.Repository.Owner.Username
|
|
configs := w.configuredRepos[org]
|
|
return ProcesPullRequest(req, configs)
|
|
|
|
}
|