2
1
forked from adamm/autogits
Files
autogits/workflow-pr/pr_processor.go

451 lines
14 KiB
Go

package main
//go:generate mockgen -source=pr_processor.go -destination=mock/pr_processor.go -typed
import (
"fmt"
"path"
"runtime/debug"
"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())
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,
})
prset.AddPR(pr)
}
return nil
}
func (pr *PRProcessor) UpdatePrjGitPR(prset *common.PRSet) error {
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
remoteName, err := git.GitClone(common.DefaultGitPrj, prjGitPRbranch, PrjGit.SSHURL)
common.PanicOnError(err)
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))
// 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
if pr, err := prset.GetPrjGitPR(); err == nil {
remote, err := git.GitClone(common.DefaultGitPrj, prjGitPRbranch, pr.PR.Base.Repo.CloneURL)
common.PanicOnError(err)
git.GitExecOrPanic(common.DefaultGitPrj, "fetch", remote, pr.PR.MergeBase, pr.PR.Head.Ref)
common.LogDebug("Fetch done")
orig_subs, err := git.GitSubmoduleList(common.DefaultGitPrj, pr.PR.MergeBase)
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))
org, repo, branch := config.GetPrjGit()
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)
}