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.
523 lines
15 KiB
Go
523 lines
15 KiB
Go
package common
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"slices"
|
|
"strings"
|
|
|
|
"src.opensuse.org/autogits/common/gitea-generated/models"
|
|
)
|
|
|
|
type PRInfo struct {
|
|
PR *models.PullRequest
|
|
Reviews *PRReviews
|
|
RemoteName string
|
|
}
|
|
|
|
type PRSet struct {
|
|
PRs []*PRInfo
|
|
Config *AutogitConfig
|
|
|
|
BotUser string
|
|
}
|
|
|
|
func readPRData(gitea GiteaPRFetcher, pr *models.PullRequest, currentSet []*PRInfo, config *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
|
|
org, repo, _ := config.GetPrjGit()
|
|
if pr.Base.Repo.Name == repo && pr.Base.Repo.Owner.UserName == org {
|
|
_, refPRs := 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
|
|
}
|
|
|
|
var Timeline_RefIssueNotFound error = errors.New("RefIssue not found on the timeline")
|
|
|
|
func LastPrjGitRefOnTimeline(gitea GiteaPRTimelineFetcher, org, repo string, num int64, prjGitOrg, prjGitRepo string) (*models.PullRequest, error) {
|
|
prRefLine := fmt.Sprintf(PrPattern, org, repo, num)
|
|
timeline, err := gitea.GetTimeline(org, repo, num)
|
|
if err != nil {
|
|
LogError("Failed to fetch timeline for", org, repo, "#", num, err)
|
|
return nil, err
|
|
}
|
|
|
|
for idx := len(timeline) - 1; idx >= 0; idx-- {
|
|
item := timeline[idx]
|
|
issue := item.RefIssue
|
|
if item.Type == TimelineCommentType_PullRequestRef &&
|
|
issue != nil &&
|
|
issue.Repository != nil &&
|
|
issue.Repository.Owner == prjGitOrg &&
|
|
issue.Repository.Name == prjGitRepo {
|
|
|
|
lines := SplitLines(item.RefIssue.Body)
|
|
for _, line := range lines {
|
|
if strings.TrimSpace(line) == prRefLine {
|
|
LogDebug("Found PrjGit PR in Timeline:", issue.Index)
|
|
|
|
// found prjgit PR in timeline. Return it
|
|
return gitea.GetPullRequest(prjGitOrg, prjGitRepo, issue.Index)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
LogDebug("PrjGit RefIssue not found on timeline in", org, repo, num)
|
|
return nil, Timeline_RefIssueNotFound
|
|
}
|
|
|
|
func FetchPRSet(user string, gitea GiteaPRTimelineFetcher, org, repo string, num int64, config *AutogitConfig) (*PRSet, error) {
|
|
var pr *models.PullRequest
|
|
var err error
|
|
|
|
prjGitOrg, prjGitRepo, _ := config.GetPrjGit()
|
|
if prjGitOrg == org && prjGitRepo == repo {
|
|
if pr, err = gitea.GetPullRequest(org, repo, num); err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
if pr, err = LastPrjGitRefOnTimeline(gitea, org, repo, num, prjGitOrg, prjGitRepo); err != nil && err != Timeline_RefIssueNotFound {
|
|
return nil, err
|
|
}
|
|
|
|
if pr == nil {
|
|
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,
|
|
BotUser: user,
|
|
}, nil
|
|
}
|
|
|
|
func (rs *PRSet) Find(pr *models.PullRequest) (*PRInfo, bool) {
|
|
for _, p := range rs.PRs {
|
|
if p.PR.Base.RepoID == pr.Base.RepoID &&
|
|
p.PR.Head.Sha == pr.Head.Sha &&
|
|
p.PR.Base.Name == pr.Base.Name {
|
|
return p, true
|
|
}
|
|
}
|
|
|
|
return nil, false
|
|
}
|
|
|
|
func (rs *PRSet) AddPR(pr *models.PullRequest) *PRInfo {
|
|
if pr, found := rs.Find(pr); found {
|
|
return pr
|
|
}
|
|
|
|
prinfo := &PRInfo{
|
|
PR: pr,
|
|
}
|
|
rs.PRs = append(rs.PRs, prinfo)
|
|
return prinfo
|
|
}
|
|
|
|
func (rs *PRSet) IsPrjGitPR(pr *models.PullRequest) bool {
|
|
org, repo, branch := rs.Config.GetPrjGit()
|
|
return pr.Base.Name == branch && pr.Base.Repo.Name == repo && pr.Base.Repo.Owner.UserName == org
|
|
}
|
|
|
|
var PRSet_PrjGitMissing error = errors.New("No PrjGit PR found")
|
|
var PRSet_MultiplePrjGit error = errors.New("Multiple PrjGit PRs in one review set")
|
|
|
|
func (rs *PRSet) GetPrjGitPR() (*PRInfo, error) {
|
|
var ret *PRInfo
|
|
|
|
for _, prinfo := range rs.PRs {
|
|
if rs.IsPrjGitPR(prinfo.PR) {
|
|
if ret == nil {
|
|
ret = prinfo
|
|
} else {
|
|
return nil, PRSet_MultiplePrjGit
|
|
}
|
|
}
|
|
}
|
|
|
|
if ret != nil {
|
|
return ret, nil
|
|
}
|
|
|
|
return nil, PRSet_PrjGitMissing
|
|
}
|
|
|
|
func (rs *PRSet) NeedRecreatingPrjGit(currentBranchHash string) bool {
|
|
pr, err := rs.GetPrjGitPR()
|
|
if err != nil {
|
|
return true
|
|
}
|
|
|
|
return pr.PR.Base.Sha == currentBranchHash
|
|
}
|
|
|
|
func (rs *PRSet) IsConsistent() bool {
|
|
prjpr_info, err := rs.GetPrjGitPR()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
prjpr := prjpr_info.PR
|
|
|
|
_, prjpr_set := ExtractDescriptionAndPRs(bufio.NewScanner(strings.NewReader(prjpr.Body)))
|
|
if len(prjpr_set) != len(rs.PRs)-1 { // 1 to many mapping
|
|
LogDebug("Number of PR from links:", len(prjpr_set), "is not what's expected", len(rs.PRs)-1)
|
|
return false
|
|
}
|
|
|
|
next_rs:
|
|
for _, prinfo := range rs.PRs {
|
|
if prinfo.PR.State != "open" {
|
|
return false
|
|
}
|
|
|
|
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 GiteaReviewFetcherAndRequester, maintainers MaintainershipData) error {
|
|
configReviewers := ParseReviewers(rs.Config.Reviewers)
|
|
|
|
for _, pr := range rs.PRs {
|
|
reviewers := []string{}
|
|
|
|
if rs.IsPrjGitPR(pr.PR) {
|
|
reviewers = slices.Concat(configReviewers.Prj, configReviewers.PrjOptional)
|
|
LogDebug("PrjGit submitter:", pr.PR.User.UserName)
|
|
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), configReviewers.PkgOptional)
|
|
}
|
|
|
|
slices.Sort(reviewers)
|
|
reviewers = slices.Compact(reviewers)
|
|
|
|
// 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)
|
|
}
|
|
|
|
LogDebug("PR: ", pr.PR.Base.Repo.Name, pr.PR.Index)
|
|
LogDebug("reviewers for PR:", reviewers)
|
|
|
|
// 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 {
|
|
LogError("Error fetching reviews:", err)
|
|
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)
|
|
LogDebug("removing reviewer:", user)
|
|
} else {
|
|
idx++
|
|
}
|
|
}
|
|
|
|
// get maintainers associated with the PR too
|
|
if len(reviewers) > 0 {
|
|
LogDebug("Requesting reviews from:", reviewers)
|
|
if !IsDryRun {
|
|
for _, r := range reviewers {
|
|
if _, err := gitea.RequestReviews(pr.PR, r); err != nil {
|
|
LogError("Cannot create reviews on", fmt.Sprintf("%s/%s#%d for [%s]", 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 GiteaPRChecker, maintainers MaintainershipData) bool {
|
|
configReviewers := ParseReviewers(rs.Config.Reviewers)
|
|
|
|
is_manually_reviewed_ok := false
|
|
|
|
if need_manual_review := rs.Config.ManualMergeOnly || rs.Config.ManualMergeProject; need_manual_review {
|
|
prjgit, err := rs.GetPrjGitPR()
|
|
if err == nil && prjgit != nil {
|
|
reviewers := slices.Concat(configReviewers.Prj, maintainers.ListProjectMaintainers())
|
|
LogDebug("Fetching reviews for", prjgit.PR.Base.Repo.Owner.UserName, prjgit.PR.Base.Repo.Name, prjgit.PR.Index)
|
|
r, err := FetchGiteaReviews(gitea, reviewers, prjgit.PR.Base.Repo.Owner.UserName, prjgit.PR.Base.Repo.Name, prjgit.PR.Index)
|
|
if err != nil {
|
|
LogError("Cannot fetch gita reaviews for PR:", err)
|
|
return false
|
|
}
|
|
prjgit.Reviews = r
|
|
if prjgit.Reviews.IsManualMergeOK() {
|
|
is_manually_reviewed_ok = true
|
|
}
|
|
}
|
|
|
|
if !is_manually_reviewed_ok && !rs.Config.ManualMergeProject {
|
|
for _, pr := range rs.PRs {
|
|
if rs.IsPrjGitPR(pr.PR) {
|
|
continue
|
|
}
|
|
|
|
pkg := pr.PR.Base.Repo.Name
|
|
reviewers := slices.Concat(configReviewers.Pkg, maintainers.ListPackageMaintainers(pkg))
|
|
LogDebug("Fetching reviews for", pr.PR.Base.Repo.Owner.UserName, pr.PR.Base.Repo.Name, pr.PR.Index)
|
|
r, err := FetchGiteaReviews(gitea, reviewers, pr.PR.Base.Repo.Owner.UserName, pr.PR.Base.Repo.Name, pr.PR.Index)
|
|
if err != nil {
|
|
LogError("Cannot fetch gita reaviews for PR:", err)
|
|
return false
|
|
}
|
|
pr.Reviews = r
|
|
if !pr.Reviews.IsManualMergeOK() {
|
|
LogInfo("Not approved manual merge. PR:", pr.PR.URL)
|
|
return false
|
|
}
|
|
}
|
|
|
|
is_manually_reviewed_ok = true
|
|
}
|
|
|
|
if !is_manually_reviewed_ok {
|
|
LogInfo("manual merge not ok")
|
|
return 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
|
|
}
|
|
|
|
if strings.HasPrefix(pr.PR.Title, "WIP:") {
|
|
LogInfo("WIP PR. Ignoring")
|
|
return false
|
|
}
|
|
|
|
r, err := FetchGiteaReviews(gitea, reviewers, pr.PR.Base.Repo.Owner.UserName, pr.PR.Base.Repo.Name, pr.PR.Index)
|
|
if err != nil {
|
|
LogError("Cannot fetch gita reaviews for PR:", err)
|
|
return false
|
|
}
|
|
|
|
is_manually_reviewed_ok = r.IsApproved()
|
|
LogDebug(pr.PR.Base.Repo.Name, is_manually_reviewed_ok)
|
|
if !is_manually_reviewed_ok {
|
|
return false
|
|
}
|
|
|
|
if need_maintainer_review := !rs.IsPrjGitPR(pr.PR) || pr.PR.User.UserName != rs.BotUser; need_maintainer_review {
|
|
if is_manually_reviewed_ok = maintainers.IsApproved(pkg, r.reviews, pr.PR.User.UserName); !is_manually_reviewed_ok {
|
|
LogDebug(" not approved?", pkg)
|
|
return false
|
|
}
|
|
} else {
|
|
LogDebug("PrjGit PR -- bot created, no need for review")
|
|
}
|
|
}
|
|
return is_manually_reviewed_ok
|
|
}
|
|
|
|
func (rs *PRSet) Merge(gitea GiteaReviewUnrequester, git Git) error {
|
|
prjgit_info, err := rs.GetPrjGitPR()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
prjgit := prjgit_info.PR
|
|
|
|
remote, err := git.GitClone(DefaultGitPrj, rs.Config.Branch, prjgit.Base.Repo.SSHURL)
|
|
PanicOnError(err)
|
|
git.GitExecOrPanic(DefaultGitPrj, "fetch", remote, prjgit.Head.Sha)
|
|
|
|
// if other changes merged, check if we have conflicts
|
|
/*
|
|
rev := strings.TrimSpace(git.GitExecWithOutputOrPanic(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 := fmt.Sprintf("Merging\n\nPR: %s/%s#%d", prjgit.Base.Repo.Owner.UserName, prjgit.Base.Repo.Name, prjgit.Index)
|
|
|
|
err = git.GitExec(DefaultGitPrj, "merge", "--no-ff", "-m", msg, prjgit.Head.Sha)
|
|
if err != nil {
|
|
status, statusErr := git.GitStatus(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 == GitStatus_Unmerged {
|
|
if s.Path != ".gitmodules" {
|
|
return err
|
|
}
|
|
|
|
submodules, err := git.GitSubmoduleList(DefaultGitPrj, "MERGE_HEAD")
|
|
if err != nil {
|
|
return fmt.Errorf("Failed to fetch submodules during merge resolution: %w", err)
|
|
}
|
|
s1, err := git.GitExecWithOutput(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(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(DefaultGitPrj, "cat-file", "blob", s.States[2])
|
|
if err != nil {
|
|
return fmt.Errorf("Failed fetching data during .gitmodules merge resoulution: %w", err)
|
|
}
|
|
|
|
subs1, err := ParseSubmodulesFile(strings.NewReader(s1))
|
|
if err != nil {
|
|
return fmt.Errorf("Failed parsing submodule file [%s] in merge: %w", s.States[0], err)
|
|
}
|
|
subs2, err := ParseSubmodulesFile(strings.NewReader(s2))
|
|
if err != nil {
|
|
return fmt.Errorf("Failed parsing submodule file [%s] in merge: %w", s.States[0], err)
|
|
}
|
|
subs3, err := 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 []Submodule = make([]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(), DefaultGitPrj, ".gitmodules"))
|
|
if err != nil {
|
|
return fmt.Errorf("Can't open .gitmodules for writing: %w", err)
|
|
}
|
|
if err = 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)
|
|
}
|
|
|
|
git.GitExecOrPanic(DefaultGitPrj, "add", ".gitmodules")
|
|
git.GitExecOrPanic(DefaultGitPrj, "-c", "core.editor=true", "merge", "--continue")
|
|
}
|
|
}
|
|
}
|
|
|
|
// FF all non-prj git and unrequest reviews.
|
|
for _, prinfo := range rs.PRs {
|
|
// remove pending review requests
|
|
repo := prinfo.PR.Base.Repo
|
|
head := prinfo.PR.Head
|
|
id := prinfo.PR.Index
|
|
|
|
reviewers := make([]string, len(prinfo.PR.RequestedReviewers))
|
|
for idx := range prinfo.PR.RequestedReviewers {
|
|
r := prinfo.PR.RequestedReviewers[idx]
|
|
if r != nil {
|
|
reviewers[idx] = r.UserName
|
|
}
|
|
}
|
|
if err := gitea.UnrequestReview(repo.Owner.UserName, repo.Name, id, reviewers...); err != nil {
|
|
LogError("Cannot unrequest reviews in PR:", repo.Owner.UserName, repo.Name, id, reviewers, ": ", err)
|
|
}
|
|
|
|
// PrjGit already merged above, so skip here.
|
|
if rs.IsPrjGitPR(prinfo.PR) {
|
|
continue
|
|
}
|
|
prinfo.RemoteName, err = git.GitClone(repo.Name, rs.Config.Branch, repo.SSHURL)
|
|
PanicOnError(err)
|
|
git.GitExecOrPanic(repo.Name, "fetch", prinfo.RemoteName, head.Sha)
|
|
git.GitExecOrPanic(repo.Name, "merge", "--ff", head.Sha)
|
|
|
|
}
|
|
|
|
// push changes
|
|
if !IsDryRun {
|
|
git.GitExecOrPanic(DefaultGitPrj, "push", remote)
|
|
} else {
|
|
LogInfo("*** WOULD push", DefaultGitPrj, "changes to", remote)
|
|
}
|
|
for _, prinfo := range rs.PRs {
|
|
if rs.IsPrjGitPR(prinfo.PR) {
|
|
continue
|
|
}
|
|
repo := prinfo.PR.Base.Repo
|
|
|
|
if !IsDryRun {
|
|
git.GitExecOrPanic(repo.Name, "push", prinfo.RemoteName)
|
|
} else {
|
|
LogInfo("*** WOULD push", repo.Name, "to", prinfo.RemoteName)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|