package main //go:generate mockgen -source=pr_processor.go -destination=mock/pr_processor.go -typed import ( "fmt" "path" "github.com/opentracing/opentracing-go/log" "src.opensuse.org/autogits/common" "src.opensuse.org/autogits/common/gitea-generated/models" ) func prGitBranchNameForPR(req *common.PullRequestWebhookEvent) string { return fmt.Sprintf("PR_%s#%d", req.Pull_Request.Base.Repo.Name, req.Pull_Request.Number) } 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(req *common.PullRequestWebhookEvent, git common.Git) { common.LogDebug(req, git) submoduleName := req.Pull_Request.Base.Repo.Name commitID := req.Pull_Request.Head.Sha common.PanicOnError(git.GitExec(common.DefaultGitPrj, "submodule", "update", "--init", "--checkout", "--depth", "1", submoduleName)) common.PanicOnError(git.GitExec(path.Join(common.DefaultGitPrj, submoduleName), "fetch", "--depth", "1", "origin", commitID)) common.PanicOnError(git.GitExec(path.Join(common.DefaultGitPrj, submoduleName), "checkout", commitID)) } 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 assumed_git_project_name := org + "/" + repo + "#" + branch PRstr := fmt.Sprintf("%s/%s#%d", org, repo, id) common.LogInfo("*** Starting processing PR:", PRstr) c := configs.GetPrjGitConfig(org, repo, branch) if c == nil { if req.Pull_Request.Base.Repo.Default_Branch == branch { c = configs.GetPrjGitConfig(org, repo, "") } } if c == nil { common.LogError("Cannot find config for PR.") return nil, fmt.Errorf("Cannot find config for PR") } var config *common.AutogitConfig for _, c := range configs { if c.GitProjectName == assumed_git_project_name { config = c break } if c.Organization == org { // default branch *or* match branch if (c.Branch == "" && branch == req.Pull_Request.Base.Repo.Default_Branch) || (c.Branch != "" && c.Branch == branch) { config = c break } } } 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(branch) 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) CreateOrUpdatePrjGitPR(req *common.PullRequestWebhookEvent) error { config := pr.config git := pr.git branchName := prGitBranchNameForPR(req) org, prj, _ := config.GetPrjGit() prOrg := req.Pull_Request.Base.Repo.Owner.Username prRepo := req.Pull_Request.Base.Repo.Name if org == prOrg && prj == prRepo { common.LogDebug("PrjGit PR. No need to update it...") return nil } prjGit, err := Gitea.CreateRepositoryIfNotExist(git, org, prj) common.PanicOnErrorWithMsg(err, "Error creating a prjgitrepo:", err) common.PanicOnError(verifyRepositoryConfiguration(prjGit)) remoteName, err := git.GitClone(common.DefaultGitPrj, config.Branch, prjGit.SSHURL) common.PanicOnError(err) // check if branch already there, and check that out if available if err := git.GitExec(common.DefaultGitPrj, "fetch", remoteName, branchName); err == nil { git.GitExecOrPanic(common.DefaultGitPrj, "checkout", "-B", branchName, remoteName+"/"+branchName) } commitMsg := fmt.Sprintf(`auto-created for %s This commit was autocreated by %s referencing `+common.PrPattern, prRepo, GitAuthor, prOrg, prRepo, req.Pull_Request.Number, ) subList, err := git.GitSubmoduleList(common.DefaultGitPrj, "HEAD") common.PanicOnError(err) if id := subList[prRepo]; id != req.Pull_Request.Head.Sha { updateSubmoduleInPR(req, git) status, err := git.GitStatus(common.DefaultGitPrj) common.LogDebug("status:", status) common.LogDebug("submodule", prRepo, " hash:", id, " -> ", req.Pull_Request.Head.Sha) common.PanicOnError(err) common.PanicOnError(git.GitExec(common.DefaultGitPrj, "commit", "-a", "-m", commitMsg)) common.PanicOnError(git.GitExec(common.DefaultGitPrj, "push", remoteName, "+HEAD:"+branchName)) } _, err = Gitea.CreatePullRequestIfNotExist(prjGit, branchName, prjGit.DefaultBranch, fmt.Sprintf("Forwarded PR: %s", prRepo), fmt.Sprintf(`This is a forwarded pull request by %s referencing the following pull request: `+common.PrPattern, GitAuthor, prOrg, prRepo, req.Pull_Request.Number), ) return err } 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 common.LogError(req) prjGitOrg, prjGitRepo, prjGitBranch := config.GetPrjGit() if prOrg != prjGitOrg || prRepo != prjGitRepo { if err := pr.CreateOrUpdatePrjGitPR(req); err != nil { return err } } 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)) // 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, prjGitBranch, pr.Base.Repo.CloneURL) common.PanicOnError(err) git.GitExecOrPanic(common.DefaultGitPrj, "fetch", remote, pr.Base.Ref, pr.Head.Ref) common.LogDebug("Fetch done") orig_subs, err := git.GitSubmoduleList(common.DefaultGitPrj, pr.Base.Sha) common.PanicOnError(err) new_subs, err := git.GitSubmoduleList(common.DefaultGitPrj, pr.Head.Sha) common.PanicOnError(err) common.LogDebug("Submodule parse done") reset_submodule := func(submodule, sha string) { spath := path.Join(common.DefaultGitPrj, submodule) git.GitExecOrPanic(common.DefaultGitPrj, "submodule", "update", "--init", "--depth", "1", submodule) git.GitExecOrPanic(spath, "fetch", "origin", sha) git.GitExecOrPanic(spath, "checkout", sha) } 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") git.GitExecOrPanic(common.DefaultGitPrj, "push") } } // request build review PR, err := prset.GetPrjGitPR() if err != nil { common.LogError("Error fetching PrjGitPR:", err) return nil } common.LogDebug(" num of reviewers:", len(PR.RequestedReviewers)) org, repo, branch := config.GetPrjGit() maintainers, err := common.FetchProjectMaintainershipData(Gitea, org, repo, branch) if err != nil { return err } err = 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 PullRequestProcessor interface { Process(req *common.PullRequestWebhookEvent, git common.Git, config *common.AutogitConfig) error } type RequestProcessor struct { configuredRepos map[string][]*common.AutogitConfig } func ProcesPullRequest(req *common.PullRequestWebhookEvent, configs []*common.AutogitConfig) error { if len(configs) < 1 { // ignoring pull request against unconfigured project (could be just regular sources?) return nil } if req.Pull_Request.State != "open" { common.LogError("Can only deal with open PRs. This one is", req.Pull_Request.State) 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) }