2025-02-20 19:25:36 +01:00

335 lines
9.8 KiB
Go

package main
import (
"bufio"
"errors"
"fmt"
"os"
"path"
"slices"
"strings"
"src.opensuse.org/autogits/common"
"src.opensuse.org/autogits/common/gitea-generated/models"
)
type PRInfo struct {
pr *models.PullRequest
reviews *PRReviews
}
type PRSet struct {
prs []PRInfo
config *common.AutogitConfig
}
func readPRData(gitea common.GiteaPRFetcher, pr *models.PullRequest, currentSet []PRInfo, config *common.AutogitConfig) ([]PRInfo, error) {
for _, p := range currentSet {
if pr.Index == p.pr.Index && pr.Base.Repo.Name == p.pr.Base.Repo.Name && pr.Base.Repo.Owner.UserName == p.pr.Base.Repo.Owner.UserName {
return nil, nil
}
}
retSet := []PRInfo{PRInfo{pr: pr}}
// only need to extact there on PrjGit PR
if pr.Base.Repo.Name == config.GitProjectName && pr.Base.Repo.Owner.UserName == config.Organization {
_, refPRs := common.ExtractDescriptionAndPRs(bufio.NewScanner(strings.NewReader(pr.Body)))
for _, prdata := range refPRs {
pr, err := gitea.GetPullRequest(prdata.Org, prdata.Repo, prdata.Num)
if err != nil {
return nil, err
}
data, err := readPRData(gitea, pr, slices.Concat(currentSet, retSet), config)
if err != nil {
return nil, err
}
retSet = slices.Concat(retSet, data)
}
}
return retSet, nil
}
func FetchPRSet(gitea common.GiteaPRFetcher, org, repo string, num int64, config *common.AutogitConfig) (*PRSet, error) {
var pr *models.PullRequest
var err error
if org != config.Organization || repo != config.GitProjectName {
if pr, err = gitea.GetAssociatedPrjGitPR(config.Organization, config.GitProjectName, org, repo, num); err != nil {
return nil, err
}
if pr == nil {
if pr, err = gitea.GetPullRequest(org, repo, num); err != nil {
return nil, err
}
}
} else {
if pr, err = gitea.GetPullRequest(org, repo, num); err != nil {
return nil, err
}
}
prs, err := readPRData(gitea, pr, nil, config)
if err != nil {
return nil, err
}
return &PRSet{prs: prs, config: config}, nil
}
func (rs *PRSet) IsPrjGitPR(pr *models.PullRequest) bool {
return pr.Base.Repo.Name == rs.config.GitProjectName && pr.Base.Repo.Owner.UserName == rs.config.Organization
}
func (rs *PRSet) GetPrjGitPR() (*models.PullRequest, error) {
var ret *models.PullRequest
for _, prinfo := range rs.prs {
if rs.IsPrjGitPR(prinfo.pr) {
if ret == nil {
ret = prinfo.pr
} else {
return nil, errors.New("Multiple PrjGit PRs in one review set")
}
}
}
if ret != nil {
return ret, nil
}
return nil, errors.New("No PrjGit PR found")
}
func (rs *PRSet) IsConsistent() bool {
prjpr, err := rs.GetPrjGitPR()
if err != nil {
return false
}
_, prjpr_set := common.ExtractDescriptionAndPRs(bufio.NewScanner(strings.NewReader(prjpr.Body)))
if len(prjpr_set) != len(rs.prs)-1 { // 1 to many mapping
return false
}
next_rs:
for _, prinfo := range rs.prs {
if prjpr == prinfo.pr {
continue
}
for _, pr := range prjpr_set {
if prinfo.pr.Base.Repo.Owner.UserName == pr.Org && prinfo.pr.Base.Repo.Name == pr.Repo && prinfo.pr.Index == pr.Num {
continue next_rs
}
}
return false
}
return true
}
func (rs *PRSet) AssignReviewers(gitea common.GiteaReviewFetcherAndRequester, maintainers common.MaintainershipData) error {
configReviewers := ParseReviewers(rs.config.Reviewers)
for _, pr := range rs.prs {
reviewers := []string{}
if rs.IsPrjGitPR(pr.pr) {
reviewers = configReviewers.Prj
if len(rs.prs) == 1 {
reviewers = slices.Concat(reviewers, maintainers.ListProjectMaintainers())
}
} else {
pkg := pr.pr.Base.Repo.Name
reviewers = slices.Concat(configReviewers.Pkg, maintainers.ListProjectMaintainers(), maintainers.ListPackageMaintainers(pkg))
}
// submitters do not need to review their own work
if idx := slices.Index(reviewers, pr.pr.User.UserName); idx != -1 {
reviewers = slices.Delete(reviewers, idx, idx+1)
}
// remove reviewers that were already requested and are not stale
reviews, err := FetchGiteaReviews(gitea, reviewers, pr.pr.Base.Repo.Owner.UserName, pr.pr.Base.Repo.Name, pr.pr.Index)
if err != nil {
return err
}
for idx := 0; idx < len(reviewers); {
user := reviewers[idx]
if reviews.HasPendingReviewBy(user) || reviews.IsReviewedBy(user) {
reviewers = slices.Delete(reviewers, idx, idx+1)
} else {
idx++
}
}
// get maintainers associated with the PR too
if len(reviewers) > 0 {
if _, err := gitea.RequestReviews(pr.pr, reviewers...); err != nil {
return fmt.Errorf("Cannot create reviews on %s/%s#%d for [%s]: %w", pr.pr.Base.Repo.Owner.UserName, pr.pr.Base.Repo.Name, pr.pr.Index, strings.Join(reviewers, ", "), err)
}
}
}
return nil
}
func (rs *PRSet) IsApproved(gitea common.GiteaPRChecker, maintainers common.MaintainershipData) bool {
configReviewers := ParseReviewers(rs.config.Reviewers)
is_reviewed := false
for _, pr := range rs.prs {
var reviewers []string
var pkg string
if rs.IsPrjGitPR(pr.pr) {
reviewers = configReviewers.Prj
pkg = ""
} else {
reviewers = configReviewers.Pkg
pkg = pr.pr.Base.Repo.Name
}
r, err := FetchGiteaReviews(gitea, reviewers, pr.pr.Base.Repo.Owner.UserName, pr.pr.Base.Repo.Name, pr.pr.Index)
if err != nil {
return false
}
is_reviewed = r.IsApproved()
if !is_reviewed {
return false
}
if is_reviewed = maintainers.IsApproved(pkg, r.reviews); !is_reviewed {
return false
}
}
return is_reviewed
}
func (rs *PRSet) Merge() error {
prjgit, err := rs.GetPrjGitPR()
if err != nil {
return err
}
gh := common.GitHandlerGeneratorImpl{}
git, err := gh.CreateGitHandler(GitAuthor, GitEmail, prjgit.Base.Name)
if err != nil {
return err
}
git.GitExecOrPanic("", "clone", "--depth", "1", prjgit.Base.Repo.SSHURL, common.DefaultGitPrj)
git.GitExecOrPanic(common.DefaultGitPrj, "fetch", "origin", prjgit.Base.Sha, prjgit.Head.Sha)
// if other changes merged, check if we have conflicts
rev := strings.TrimSpace(git.GitExecWithOutputOrPanic(common.DefaultGitPrj, "merge-base", "HEAD", prjgit.Base.Sha, prjgit.Head.Sha))
if rev != prjgit.Base.Sha {
return fmt.Errorf("Base.Sha (%s) not yet merged into project-git. Aborting merge.", prjgit.Base.Sha)
}
/*
rev := git.GitExecWithOutputOrPanic(common.DefaultGitPrj, "rev-list", "-1", "HEAD")
if rev != prjgit.Base.Sha {
panic("FIXME")
}
*/
msg := "merging"
err = git.GitExec(common.DefaultGitPrj, "merge", "--no-ff", "-m", msg, prjgit.Head.Sha)
if err != nil {
status, statusErr := git.GitStatus(common.DefaultGitPrj)
if statusErr != nil {
return fmt.Errorf("Failed to merge: %w . Status also failed: %w", err, statusErr)
}
// we can only resolve conflicts with .gitmodules
for _, s := range status {
if s.Status == common.GitStatus_Unmerged {
if s.Path != ".gitmodules" {
return err
}
submodules, err := git.GitSubmoduleList(common.DefaultGitPrj, "MERGE_HEAD")
if err != nil {
return fmt.Errorf("Failed to fetch submodules during merge resolution: %w", err)
}
s1, err := git.GitExecWithOutput(common.DefaultGitPrj, "cat-file", "blob", s.States[0])
if err != nil {
return fmt.Errorf("Failed fetching data during .gitmodules merge resoulution: %w", err)
}
s2, err := git.GitExecWithOutput(common.DefaultGitPrj, "cat-file", "blob", s.States[1])
if err != nil {
return fmt.Errorf("Failed fetching data during .gitmodules merge resoulution: %w", err)
}
s3, err := git.GitExecWithOutput(common.DefaultGitPrj, "cat-file", "blob", s.States[2])
if err != nil {
return fmt.Errorf("Failed fetching data during .gitmodules merge resoulution: %w", err)
}
subs1, err := common.ParseSubmodulesFile(strings.NewReader(s1))
if err != nil {
return fmt.Errorf("Failed parsing submodule file [%s] in merge: %w", s.States[0], err)
}
subs2, err := common.ParseSubmodulesFile(strings.NewReader(s2))
if err != nil {
return fmt.Errorf("Failed parsing submodule file [%s] in merge: %w", s.States[0], err)
}
subs3, err := common.ParseSubmodulesFile(strings.NewReader(s3))
if err != nil {
return fmt.Errorf("Failed parsing submodule file [%s] in merge: %w", s.States[0], err)
}
// merge from subs3 (target), subs1 (orig), subs2 (2-nd base that is missing from target base)
// this will update submodules
mergedSubs := slices.Concat(subs1, subs2, subs3)
var filteredSubs []common.Submodule = make([]common.Submodule, 0, max(len(subs1), len(subs2), len(subs3)))
nextSub:
for subName := range submodules {
for i := range mergedSubs {
if path.Base(mergedSubs[i].Path) == subName {
filteredSubs = append(filteredSubs, mergedSubs[i])
continue nextSub
}
}
return fmt.Errorf("Cannot find submodule for path: %s", subName)
}
out, err := os.Create(path.Join(git.GetPath(), common.DefaultGitPrj, ".gitmodules"))
if err != nil {
return fmt.Errorf("Can't open .gitmodules for writing: %w", err)
}
if err = common.WriteSubmodules(filteredSubs, out); err != nil {
return fmt.Errorf("Can't write .gitmodules: %w", err)
}
if out.Close(); err != nil {
return fmt.Errorf("Can't close .gitmodules: %w", err)
}
os.CopyFS("/tmp/test", os.DirFS(git.GetPath()))
git.GitExecOrPanic(common.DefaultGitPrj, "add", ".gitmodules")
git.GitExecOrPanic(common.DefaultGitPrj, "-c", "core.editor=true", "merge", "--continue")
}
}
}
// FF all non-prj git
for _, prinfo := range rs.prs {
if rs.IsPrjGitPR(prinfo.pr) {
continue
}
git.GitExecOrPanic("", "clone", prinfo.pr.Base.Repo.SSHURL, prinfo.pr.Base.Name)
git.GitExecOrPanic(prinfo.pr.Base.Name, "fetch", "origin", prinfo.pr.Head.Sha)
git.GitExecOrPanic(prinfo.pr.Base.Name, "merge", "--ff", prinfo.pr.Head.Sha)
}
// push changes
git.GitExecOrPanic(common.DefaultGitPrj, "push", "origin")
for _, prinfo := range rs.prs {
if rs.IsPrjGitPR(prinfo.pr) {
continue
}
git.GitExecOrPanic(prinfo.pr.Base.Name, "push", "origin")
}
return nil
}