to avoid merging pull requests where lfs objects are not correctly registered. Can happen when user has not installed lfs for example.
737 lines
24 KiB
Go
737 lines
24 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"path"
|
|
"runtime/debug"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/opentracing/opentracing-go/log"
|
|
"src.opensuse.org/autogits/common"
|
|
"src.opensuse.org/autogits/common/gitea-generated/client/repository"
|
|
"src.opensuse.org/autogits/common/gitea-generated/models"
|
|
)
|
|
|
|
func prGitBranchNameForPR(repo string, prNo int64) string {
|
|
return fmt.Sprintf("PR_%s#%d", repo, prNo)
|
|
}
|
|
|
|
func PrjGitDescription(prset *common.PRSet) (title string, desc string) {
|
|
title_refs := make([]string, 0, len(prset.PRs)-1)
|
|
refs := make([]string, 0, len(prset.PRs)-1)
|
|
|
|
prefix := ""
|
|
for _, pr := range prset.PRs {
|
|
if prset.IsPrjGitPR(pr.PR) {
|
|
continue
|
|
}
|
|
if pr.PR.State != "open" {
|
|
// remove PRs that are not open from description
|
|
continue
|
|
}
|
|
if strings.HasPrefix(pr.PR.Title, "WIP:") {
|
|
prefix = "WIP: "
|
|
}
|
|
org, repo, idx := pr.PRComponents()
|
|
|
|
title_refs = append(title_refs, repo)
|
|
ref := fmt.Sprintf(common.PrPattern, org, repo, idx)
|
|
refs = append(refs, ref)
|
|
}
|
|
|
|
slices.Sort(title_refs)
|
|
slices.Sort(refs)
|
|
|
|
title = prefix + "Forwarded PRs: " + strings.Join(title_refs, ", ")
|
|
desc = fmt.Sprintf("This is a forwarded pull request by %s\nreferencing the following pull request(s):\n\n", GitAuthor) + strings.Join(refs, "\n") + "\n"
|
|
|
|
if prset.Config.ManualMergeOnly {
|
|
desc = desc + "\n### ManualMergeOnly enabled. To merge, 'merge ok' is required in either the project PR or every package PR."
|
|
}
|
|
if prset.Config.ManualMergeProject {
|
|
desc = desc + "\n### ManualMergeProject enabled. To merge, 'merge ok' is required by project maintainer in the project PR."
|
|
}
|
|
if !prset.Config.ManualMergeOnly && !prset.Config.ManualMergeProject {
|
|
desc = desc + "\n### Automatic merge enabled. This will merge when all review requirements are satisfied."
|
|
}
|
|
return
|
|
}
|
|
|
|
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", "-f", headSha))
|
|
}
|
|
|
|
type PRProcessor struct {
|
|
config *common.AutogitConfig
|
|
git common.Git
|
|
}
|
|
|
|
func AllocatePRProcessor(req *models.PullRequest, configs common.AutogitConfigs) (*PRProcessor, error) {
|
|
org := req.Base.Repo.Owner.UserName
|
|
repo := req.Base.Repo.Name
|
|
id := req.Index
|
|
|
|
branch := req.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.Base.Repo.DefaultBranch == 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")
|
|
}
|
|
|
|
if common.GetLoggingLevel() >= common.LogLevelDebug {
|
|
cjson, _ := json.Marshal(config)
|
|
common.LogDebug("found config:", string(cjson))
|
|
}
|
|
if config == nil {
|
|
common.LogError("Cannot find config for branch '%s'", req.Base.Ref)
|
|
return nil, fmt.Errorf("Cannot find config for branch '%s'", req.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) error {
|
|
git := pr.git
|
|
subList, err := git.GitSubmoduleList(common.DefaultGitPrj, "HEAD")
|
|
if err != nil {
|
|
common.LogError("Error fetching submodule list for PrjGit", err)
|
|
return err
|
|
}
|
|
|
|
for _, pr := range prset.PRs {
|
|
if prset.IsPrjGitPR(pr.PR) {
|
|
continue
|
|
}
|
|
|
|
org, repo, idx := pr.PRComponents()
|
|
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 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, "\n\nreferencing PRs:\n", ref)
|
|
|
|
if revert {
|
|
commitMsg = fmt.Sprintln("auto-created for", repo, "\n\nThis commit was autocreated by", GitAuthor, "\n\nremoving PRs:\n", ref)
|
|
}
|
|
|
|
updateSubmoduleInPR(submodulePath, prHead, git)
|
|
err := git.GitExec(path.Join(common.DefaultGitPrj, submodulePath), "lfs", "fetch")
|
|
common.LogError("lfs fetch err: ", err)
|
|
if err = git.GitExec(path.Join(common.DefaultGitPrj, submodulePath), "lfs", "fsck"); err != nil {
|
|
|
|
found_comment := false
|
|
timeline, terr := common.FetchTimelineSinceLastPush(Gitea, prHead, org, repo, idx)
|
|
if terr != nil {
|
|
common.LogError("lfs fsck error, but timeline fetch failed")
|
|
break
|
|
}
|
|
msgPrefix := "The LFS objects are broken!"
|
|
for _, t := range timeline {
|
|
if t.Type == common.TimelineCommentType_Comment && strings.HasPrefix(t.Body, msgPrefix) {
|
|
found_comment = true
|
|
common.LogError("lfs fsck Comment already found")
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found_comment && !common.IsDryRun {
|
|
Gitea.AddComment(pr.PR, msgPrefix + " Please verify with 'git lfs fsck'")
|
|
}
|
|
common.LogError("lfs fsck failed with: ", err.Error())
|
|
return err
|
|
}
|
|
|
|
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))
|
|
|
|
pr.PR.Head.Sha = id // update the prset
|
|
}
|
|
submodule_found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !submodule_found {
|
|
common.LogInfo("Adding new submodule", repo, "to PrjGit")
|
|
ref := fmt.Sprintf(common.PrPattern, org, repo, idx)
|
|
commitMsg := fmt.Sprintln("Add package", repo, "\n\nThis commit was autocreated by", GitAuthor, "\n\nreferencing PRs:\n", ref)
|
|
|
|
git.GitExecOrPanic(common.DefaultGitPrj, "submodule", "add", "-b", pr.PR.Base.Name, pr.PR.Base.Repo.SSHURL, repo)
|
|
|
|
updateSubmoduleInPR(repo, prHead, git)
|
|
common.PanicOnError(git.GitExec(common.DefaultGitPrj, "commit", "-a", "-m", commitMsg))
|
|
}
|
|
}
|
|
return 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
|
|
}
|
|
if err := pr.SetSubmodulesToMatchPRSet(prset); 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 && !pr.config.NoProjectGitPR {
|
|
if headCommit != newHeadCommit {
|
|
common.PanicOnError(git.GitExec(common.DefaultGitPrj, "push", RemoteName, "+HEAD:"+prjGitPRbranch))
|
|
}
|
|
|
|
title, desc := PrjGitDescription(prset)
|
|
pr, err, isNew := Gitea.CreatePullRequestIfNotExist(PrjGit, prjGitPRbranch, PrjGitBranch, title, desc)
|
|
if err != nil {
|
|
common.LogError("Error creating PrjGit PR:", err)
|
|
return err
|
|
}
|
|
org := PrjGit.Owner.UserName
|
|
repo := PrjGit.Name
|
|
idx := pr.Index
|
|
if isNew {
|
|
Gitea.SetLabels(org, repo, idx, []string{prset.Config.Label(common.Label_StagingAuto)})
|
|
}
|
|
Gitea.UpdatePullRequest(org, repo, idx, &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. %v", s)
|
|
}
|
|
}
|
|
conflict = git.GitExec(common.DefaultGitPrj, "rebase", "--skip")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
var updatePrjGitError_requeue error = errors.New("Commits do not match. Requeing after 5 seconds.")
|
|
|
|
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
|
|
if len(prset.PRs) == 1 {
|
|
if len(PrjGitPR.RemoteName) == 0 {
|
|
PrjGitPR.RemoteName, _ = git.GitClone(common.DefaultGitPrj, "", PrjGitPR.PR.Base.Repo.SSHURL)
|
|
}
|
|
git.GitExecOrPanic(common.DefaultGitPrj, "fetch", PrjGitPR.RemoteName, PrjGitPR.PR.Head.Sha)
|
|
common.LogDebug("Only project git in PR. Nothing to update.")
|
|
return nil
|
|
}
|
|
|
|
PrjGit := PrjGitPR.PR.Base.Repo
|
|
prjGitPRbranch := PrjGitPR.PR.Head.Name
|
|
if PrjGitPR.PR.Base.RepoID != PrjGitPR.PR.Head.RepoID {
|
|
// permission check, if submission comes from foreign repo
|
|
if !PrjGitPR.PR.AllowMaintainerEdit {
|
|
common.LogError("Warning: source and target branch are in different repositories. We may not have the right permissions...")
|
|
// Gitea.AddComment(PrjGitPR.PR, "This PR does not allow maintainer changes, but referenced package branch has changed!")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// PrjGitPR.RemoteName, err = git.GitClone(common.DefaultGitPrj, prjGitPRbranch, PrjGit.SSHURL)
|
|
PrjGitPR.RemoteName, err = git.GitClone(common.DefaultGitPrj, PrjGitPR.PR.Head.Ref, PrjGitPR.PR.Head.Repo.SSHURL)
|
|
common.PanicOnError(err)
|
|
git.GitExecOrPanic(common.DefaultGitPrj, "fetch", PrjGitPR.RemoteName, PrjGitBranch)
|
|
|
|
forcePush := false
|
|
// trust Gitea here on mergeability
|
|
if !PrjGitPR.PR.Mergeable {
|
|
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
|
|
}
|
|
if err := pr.SetSubmodulesToMatchPRSet(prset); 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
|
|
}
|
|
|
|
PrjGitTitle, PrjGitBody := PrjGitDescription(prset)
|
|
if PrjGitPR.PR.User.UserName == CurrentUser.UserName {
|
|
if PrjGitPR.PR.Title != PrjGitTitle || PrjGitPR.PR.Body != PrjGitBody {
|
|
common.LogDebug("New title:", PrjGitTitle)
|
|
common.LogDebug(PrjGitBody)
|
|
}
|
|
} else {
|
|
// TODO: find our first comment in timeline
|
|
|
|
}
|
|
|
|
if !common.IsDryRun {
|
|
if headCommit != PrjGitPR.PR.Head.Sha {
|
|
common.LogError("HeadCommit:", headCommit, "is not what's expected from the PR:", PrjGitPR.PR.Head.Ref, " Requeing.")
|
|
return updatePrjGitError_requeue
|
|
}
|
|
if headCommit != newHeadCommit {
|
|
params := []string{"push", PrjGitPR.RemoteName, "+HEAD:" + prjGitPRbranch}
|
|
if forcePush {
|
|
params = slices.Insert(params, 1, "-f")
|
|
}
|
|
common.PanicOnError(git.GitExec(common.DefaultGitPrj, params...))
|
|
PrjGitPR.PR.Head.Sha = newHeadCommit
|
|
Gitea.SetLabels(PrjGit.Owner.UserName, PrjGit.Name, PrjGitPR.PR.Index, []string{prset.Config.Label("PR/updated")})
|
|
}
|
|
|
|
// update PR
|
|
isPrTitleSame := func(CurrentTitle, NewTitle string) bool {
|
|
ctlen := len(CurrentTitle)
|
|
for _, suffix := range []string{"...", "…"} {
|
|
slen := len(suffix)
|
|
if ctlen > 250 && strings.HasSuffix(CurrentTitle, suffix) && len(NewTitle) > ctlen {
|
|
NewTitle = NewTitle[0:ctlen-slen] + suffix
|
|
if CurrentTitle == NewTitle {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return CurrentTitle == NewTitle
|
|
}
|
|
if !pr.config.NoProjectGitPR && PrjGitPR.PR.User.UserName == CurrentUser.UserName && (PrjGitPR.PR.Body != PrjGitBody || !isPrTitleSame(PrjGitPR.PR.Title, PrjGitTitle)) {
|
|
Gitea.UpdatePullRequest(PrjGit.Owner.UserName, PrjGit.Name, PrjGitPR.PR.Index, &models.EditPullRequestOption{
|
|
RemoveDeadline: true,
|
|
Title: PrjGitTitle,
|
|
Body: PrjGitBody,
|
|
})
|
|
}
|
|
}
|
|
|
|
// remove closed PRs from prset
|
|
prset.RemoveClosedPRs()
|
|
return nil
|
|
}
|
|
|
|
func (pr *PRProcessor) Process(req *models.PullRequest) error {
|
|
config := pr.config
|
|
git := pr.git
|
|
|
|
// requests against project are not handled here
|
|
common.LogInfo("processing opened PR:", req.URL)
|
|
prOrg := req.Base.Repo.Owner.UserName
|
|
prRepo := req.Base.Repo.Name
|
|
prNo := req.Index
|
|
|
|
common.LogError(req)
|
|
|
|
prset, err := common.FetchPRSet(CurrentUser.UserName, Gitea, prOrg, prRepo, prNo, 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 {
|
|
if req.State != "open" {
|
|
common.LogDebug("This PR is closed and no ProjectGit PR. Ignoring.")
|
|
return nil
|
|
}
|
|
common.LogDebug("Missing PrjGit. Need to create one under branch", prjGitPRbranch)
|
|
|
|
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" {
|
|
if prjGitPR.PR.HasMerged {
|
|
// update branches in project
|
|
prjGitPR.RemoteName, err = git.GitClone(common.DefaultGitPrj, prjGitPRbranch, prjGitPR.PR.Base.Repo.SSHURL)
|
|
common.PanicOnError(err)
|
|
|
|
old_pkgs, err := git.GitSubmoduleList(common.DefaultGitPrj, prjGitPR.PR.MergeBase)
|
|
common.PanicOnError(err)
|
|
new_pkgs, err := git.GitSubmoduleList(common.DefaultGitPrj, prjGitPRbranch)
|
|
common.PanicOnError(err)
|
|
|
|
pkgs := make(map[string]string)
|
|
for pkg, old_commit := range old_pkgs {
|
|
if new_commit, found := new_pkgs[pkg]; found {
|
|
// pkg modified
|
|
if new_commit != old_commit {
|
|
pkgs[pkg] = new_commit
|
|
}
|
|
} else { // not found, pkg removed
|
|
pkgs[pkg] = ""
|
|
}
|
|
}
|
|
for pkg, commit := range new_pkgs {
|
|
if _, found := old_pkgs[pkg]; !found {
|
|
// pkg added
|
|
pkgs[pkg] = commit
|
|
}
|
|
}
|
|
|
|
PrjGitSubmoduleCheck(config, git, common.DefaultGitPrj, pkgs)
|
|
}
|
|
|
|
// manually merge or close entire prset that is still open
|
|
for _, pr := range prset.PRs {
|
|
if pr.PR.State == "open" {
|
|
org, repo, idx := pr.PRComponents()
|
|
if prjGitPR.PR.HasMerged {
|
|
Gitea.AddComment(pr.PR, "This PR is merged via the associated Project PR.")
|
|
err = Gitea.ManualMergePR(org, repo, idx, pr.PR.Head.Sha, false)
|
|
if _, ok := err.(*repository.RepoMergePullRequestConflict); !ok {
|
|
common.PanicOnError(err)
|
|
}
|
|
// } else {
|
|
// Gitea.AddComment(pr.PR, "Closing here because the associated Project PR has been closed.")
|
|
// Gitea.UpdatePullRequest(org, repo, idx, &models.EditPullRequestOption{
|
|
// State: "closed",
|
|
// })
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if len(prset.PRs) > 1 {
|
|
for _, pr := range prset.PRs {
|
|
if prset.IsPrjGitPR(pr.PR) {
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
if err = pr.UpdatePrjGitPR(prset); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if prjGitPR == nil {
|
|
prjGitPR, err = prset.GetPrjGitPR()
|
|
if err == common.PRSet_PrjGitMissing && config.NoProjectGitPR {
|
|
// we could be waiting for other tooling to create the
|
|
// project git PR. In meantime, we can assign some
|
|
// reviewers here.
|
|
} else 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
|
|
|
|
// TODO: this is broken...
|
|
if pr, err := prset.GetPrjGitPR(); err == nil && false {
|
|
common.LogDebug("Submodule parse begin")
|
|
orig_subs, err := git.GitSubmoduleList(common.DefaultGitPrj, pr.PR.MergeBase)
|
|
common.PanicOnError(err)
|
|
new_subs, err := git.GitSubmoduleList(common.DefaultGitPrj, "HEAD")
|
|
common.PanicOnError(err)
|
|
common.LogDebug("Submodule parse done")
|
|
|
|
reset_submodule := func(submodule, sha string) {
|
|
updateSubmoduleInPR(submodule, sha, git)
|
|
}
|
|
|
|
common.LogDebug("Checking we only change linked commits")
|
|
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
|
|
} else if pr.PR.Base.Repo.Name == path {
|
|
common.LogError(path, "-- commits not match", commit, pr.PR.Head.Sha)
|
|
}
|
|
}
|
|
if !found {
|
|
reset_submodule(path, old)
|
|
}
|
|
}
|
|
}
|
|
|
|
stats, err := git.GitStatus(common.DefaultGitPrj)
|
|
common.LogDebug("Check Done", len(stats), "changes")
|
|
common.PanicOnError(err)
|
|
if len(stats) > 0 {
|
|
git.GitExecOrPanic(common.DefaultGitPrj, "commit", "-a", "-m", "Sync submodule updates with PR-set")
|
|
git.GitExecQuietOrPanic(common.DefaultGitPrj, "submodule", "deinit", "--all", "--force")
|
|
if !common.IsDryRun {
|
|
git.GitExecOrPanic(common.DefaultGitPrj, "push")
|
|
}
|
|
}
|
|
}
|
|
|
|
if prjGitPR != nil {
|
|
common.LogDebug(" num of reviewers:", len(prjGitPR.PR.RequestedReviewers))
|
|
} else {
|
|
common.LogInfo("* No prjgit")
|
|
}
|
|
maintainers, err := common.FetchProjectMaintainershipData(Gitea, config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// update prset if we should build it or not
|
|
if prjGitPR != nil {
|
|
if file, err := git.GitCatFile(common.DefaultGitPrj, prjGitPR.PR.Head.Sha, "staging.config"); err == nil {
|
|
prset.HasAutoStaging = (file != nil)
|
|
common.LogDebug(" -> automatic staging enabled?:", prset.HasAutoStaging)
|
|
}
|
|
}
|
|
|
|
// 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 && prjGitPR != nil && 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")
|
|
}
|
|
|
|
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)
|
|
}
|
|
} else {
|
|
err = prset.AssignReviewers(Gitea, maintainers)
|
|
}
|
|
return err
|
|
}
|
|
|
|
type RequestProcessor struct {
|
|
configuredRepos map[string][]*common.AutogitConfig
|
|
recursive int
|
|
}
|
|
|
|
func (w *RequestProcessor) Process(pr *models.PullRequest) error {
|
|
configs, ok := w.configuredRepos[pr.Base.Repo.Owner.UserName]
|
|
if !ok {
|
|
return fmt.Errorf("no config found for org %s", pr.Base.Repo.Owner.UserName)
|
|
}
|
|
return ProcesPullRequest(pr, configs)
|
|
}
|
|
|
|
func ProcesPullRequest(pr *models.PullRequest, configs []*common.AutogitConfig) error {
|
|
if len(configs) < 1 {
|
|
// ignoring pull request against unconfigured project (could be just regular sources?)
|
|
return nil
|
|
}
|
|
|
|
PRProcessor, err := AllocatePRProcessor(pr, configs)
|
|
if err != nil {
|
|
log.Error(err)
|
|
return err
|
|
}
|
|
defer PRProcessor.git.Close()
|
|
|
|
return PRProcessor.Process(pr)
|
|
}
|
|
|
|
func (w *RequestProcessor) ProcessFunc(request *common.Request) (err error) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
common.LogInfo("panic cought --- recovered")
|
|
common.LogError(string(debug.Stack()))
|
|
}
|
|
w.recursive--
|
|
}()
|
|
|
|
w.recursive++
|
|
if w.recursive > 3 {
|
|
common.LogError("Recursion limit reached... something is wrong with this PR?")
|
|
return nil
|
|
}
|
|
|
|
var pr *models.PullRequest
|
|
if req, ok := request.Data.(*common.PullRequestWebhookEvent); ok {
|
|
pr, err = Gitea.GetPullRequest(req.Pull_Request.Base.Repo.Owner.Username, req.Pull_Request.Base.Repo.Name, req.Pull_Request.Number)
|
|
if err != nil {
|
|
common.LogError("Cannot find PR for issue:", req.Pull_Request.Base.Repo.Owner.Username, req.Pull_Request.Base.Repo.Name, req.Pull_Request.Number)
|
|
return err
|
|
}
|
|
} else if req, ok := request.Data.(*common.IssueCommentWebhookEvent); ok {
|
|
pr, err = Gitea.GetPullRequest(req.Repository.Owner.Username, req.Repository.Name, int64(req.Issue.Number))
|
|
if err != nil {
|
|
common.LogError("Cannot find PR for issue:", req.Repository.Owner.Username, req.Repository.Name, int64(req.Issue.Number))
|
|
return err
|
|
}
|
|
} else if req, ok := request.Data.(*common.IssueWebhookEvent); ok {
|
|
issue, err := Gitea.GetIssue(req.Repository.Owner.Username, req.Repository.Name, int64(req.Issue.Number))
|
|
if err != nil {
|
|
common.LogError("Cannot find issue for issue event:", req.Repository.Owner.Username, req.Repository.Name, int64(req.Issue.Number))
|
|
return err
|
|
}
|
|
configs, ok := w.configuredRepos[req.Repository.Owner.Username]
|
|
if !ok {
|
|
common.LogError("*** Cannot find config for org:", req.Repository.Owner.Username)
|
|
return nil
|
|
}
|
|
processor := &IssueProcessor{
|
|
issue: issue,
|
|
}
|
|
return processor.ProcessIssue(configs)
|
|
} else {
|
|
common.LogError("*** Invalid data format for PR processing.")
|
|
return fmt.Errorf("*** Invalid data format for PR processing.")
|
|
}
|
|
|
|
configs, ok := w.configuredRepos[pr.Base.Repo.Owner.UserName]
|
|
if !ok {
|
|
common.LogError("*** Cannot find config for org:", pr.Base.Repo.Owner.UserName)
|
|
}
|
|
if err = ProcesPullRequest(pr, configs); err == updatePrjGitError_requeue {
|
|
time.Sleep(time.Second * 5)
|
|
return w.ProcessFunc(request)
|
|
}
|
|
return err
|
|
}
|