Timeline events will contain Reviews and ReviewRequests and ReviewDismissed events. We need to handle this at event parsing time and not to punt this to the query functions later on. If the last event is an actual review, we use this. If no review, check if last event associated with the reviewer is Dismissed or Requested Review but not if a dismissed Review preceeds it.
776 lines
24 KiB
Go
776 lines
24 KiB
Go
package common
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"regexp"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"src.opensuse.org/autogits/common/gitea-generated/client/repository"
|
|
"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
|
|
HasAutoStaging bool
|
|
}
|
|
|
|
func (prinfo *PRInfo) PRComponents() (org string, repo string, idx int64) {
|
|
org = prinfo.PR.Base.Repo.Owner.UserName
|
|
repo = prinfo.PR.Base.Repo.Name
|
|
idx = prinfo.PR.Index
|
|
return
|
|
}
|
|
|
|
func (prinfo *PRInfo) RemoveReviewers(gitea GiteaUnreviewTimelineFetcher, Reviewers []string, BotUser string) {
|
|
org, repo, idx := prinfo.PRComponents()
|
|
tl, err := gitea.GetTimeline(org, repo, idx)
|
|
if err != nil {
|
|
LogError("Failed to fetch timeline for", PRtoString(prinfo.PR), err)
|
|
}
|
|
|
|
// find review request for each reviewer
|
|
ReviewersToUnrequest := Reviewers
|
|
ReviewersAlreadyChecked := []string{}
|
|
|
|
for _, tlc := range tl {
|
|
if tlc.Type == TimelineCommentType_ReviewRequested && tlc.Assignee != nil {
|
|
user := tlc.Assignee.UserName
|
|
|
|
if idx := slices.Index(ReviewersToUnrequest, user); idx >= 0 && !slices.Contains(ReviewersAlreadyChecked, user) {
|
|
if tlc.User != nil && tlc.User.UserName == BotUser {
|
|
ReviewersAlreadyChecked = append(ReviewersAlreadyChecked, user)
|
|
continue
|
|
}
|
|
ReviewersToUnrequest = slices.Delete(ReviewersToUnrequest, idx, idx+1)
|
|
if len(Reviewers) == 0 {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
LogDebug("Unrequesting reviewes for", PRtoString(prinfo.PR), ReviewersToUnrequest)
|
|
err = gitea.UnrequestReview(org, repo, idx, ReviewersToUnrequest...)
|
|
if err != nil {
|
|
LogError("Failed to unrequest reviewers for", PRtoString(prinfo.PR), err)
|
|
}
|
|
}
|
|
|
|
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(botUser string, gitea GiteaPRTimelineReviewFetcher, org, repo string, num int64, config *AutogitConfig) (*models.PullRequest, error) {
|
|
timeline, err := gitea.GetTimeline(org, repo, num)
|
|
if err != nil {
|
|
LogError("Failed to fetch timeline for", org, repo, "#", num, err)
|
|
return nil, err
|
|
}
|
|
|
|
prjGitOrg, prjGitRepo, prjGitBranch := config.GetPrjGit()
|
|
|
|
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 {
|
|
|
|
if !config.NoProjectGitPR {
|
|
if issue.User.UserName != botUser {
|
|
continue
|
|
}
|
|
}
|
|
|
|
pr, err := gitea.GetPullRequest(prjGitOrg, prjGitRepo, issue.Index)
|
|
if err != nil {
|
|
switch err.(type) {
|
|
case *repository.RepoGetPullRequestNotFound: // deleted?
|
|
continue
|
|
default:
|
|
LogDebug("PrjGit RefIssue fetch error from timeline", issue.Index, err)
|
|
continue
|
|
}
|
|
}
|
|
|
|
LogDebug("found ref PR on timeline:", PRtoString(pr))
|
|
if pr.Base.Name != prjGitBranch {
|
|
LogDebug(" -> not matching:", pr.Base.Name, prjGitBranch)
|
|
continue
|
|
}
|
|
|
|
_, prs := ExtractDescriptionAndPRs(bufio.NewScanner(strings.NewReader(item.RefIssue.Body)))
|
|
for _, pr := range prs {
|
|
if pr.Org == org && pr.Repo == repo && pr.Num == num {
|
|
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 GiteaPRTimelineReviewFetcher, org, repo string, num int64, config *AutogitConfig) (*PRSet, error) {
|
|
var pr *models.PullRequest
|
|
var err error
|
|
|
|
gitea.ResetTimelineCache(org, repo, num)
|
|
|
|
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(user, gitea, org, repo, num, config); 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
|
|
}
|
|
|
|
for _, pr := range prs {
|
|
org, repo, idx := pr.PRComponents()
|
|
gitea.ResetTimelineCache(org, repo, idx)
|
|
reviews, err := FetchGiteaReviews(gitea, org, repo, idx)
|
|
if err != nil {
|
|
LogError("Error fetching reviews for", PRtoString(pr.PR), ":", err)
|
|
}
|
|
pr.Reviews = reviews
|
|
}
|
|
|
|
return &PRSet{
|
|
PRs: prs,
|
|
Config: config,
|
|
BotUser: user,
|
|
}, nil
|
|
}
|
|
|
|
func (prset *PRSet) RemoveReviewers(gitea GiteaUnreviewTimelineFetcher, reviewers []string) {
|
|
for _, prinfo := range prset.PRs {
|
|
prinfo.RemoveReviewers(gitea, reviewers, prset.BotUser)
|
|
}
|
|
}
|
|
|
|
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 strings.EqualFold(prinfo.PR.Base.Repo.Owner.UserName, pr.Org) && strings.EqualFold(prinfo.PR.Base.Repo.Name, pr.Repo) && prinfo.PR.Index == pr.Num {
|
|
continue next_rs
|
|
}
|
|
}
|
|
LogDebug(" PR: ", PRtoString(prinfo.PR), "not found in project git PRSet")
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (rs *PRSet) FindMissingAndExtraReviewers(maintainers MaintainershipData, idx int) (missing, extra []string) {
|
|
configReviewers := ParseReviewers(rs.Config.Reviewers)
|
|
|
|
// remove reviewers that were already requested and are not stale
|
|
prjMaintainers := maintainers.ListProjectMaintainers(nil)
|
|
LogDebug("project maintainers:", prjMaintainers)
|
|
|
|
pr := rs.PRs[idx]
|
|
if rs.IsPrjGitPR(pr.PR) {
|
|
missing = slices.Concat(configReviewers.Prj, configReviewers.PrjOptional)
|
|
if rs.HasAutoStaging {
|
|
missing = append(missing, Bot_BuildReview)
|
|
}
|
|
LogDebug("PrjGit submitter:", pr.PR.User.UserName)
|
|
// only need project maintainer reviews if:
|
|
// * not created by a bot and has other PRs, or
|
|
// * not created by maintainer
|
|
noReviewPRCreators := []string{}
|
|
if !rs.Config.ReviewRequired {
|
|
noReviewPRCreators = prjMaintainers
|
|
}
|
|
if len(rs.PRs) > 1 {
|
|
noReviewPRCreators = append(noReviewPRCreators, rs.BotUser)
|
|
}
|
|
if slices.Contains(noReviewPRCreators, pr.PR.User.UserName) || pr.Reviews.IsReviewedByOneOf(prjMaintainers...) {
|
|
LogDebug("Project already reviewed by a project maintainer, remove rest")
|
|
// do not remove reviewers if they are also maintainers
|
|
prjMaintainers = slices.DeleteFunc(prjMaintainers, func(m string) bool { return slices.Contains(missing, m) })
|
|
extra = slices.Concat(prjMaintainers, []string{rs.BotUser})
|
|
} else {
|
|
// if bot not created PrjGit or prj maintainer, we need to add project reviewers here
|
|
if slices.Contains(noReviewPRCreators, pr.PR.User.UserName) {
|
|
LogDebug("No need for project maintainers")
|
|
extra = slices.Concat(prjMaintainers, []string{rs.BotUser})
|
|
} else {
|
|
LogDebug("Adding prjMaintainers to PrjGit")
|
|
missing = append(missing, prjMaintainers...)
|
|
}
|
|
}
|
|
} else {
|
|
pkg := pr.PR.Base.Repo.Name
|
|
pkgMaintainers := maintainers.ListPackageMaintainers(pkg, nil)
|
|
Maintainers := slices.Concat(prjMaintainers, pkgMaintainers)
|
|
noReviewPkgPRCreators := []string{}
|
|
if !rs.Config.ReviewRequired {
|
|
noReviewPkgPRCreators = pkgMaintainers
|
|
}
|
|
|
|
LogDebug("packakge maintainers:", Maintainers)
|
|
|
|
missing = slices.Concat(configReviewers.Pkg, configReviewers.PkgOptional)
|
|
if slices.Contains(noReviewPkgPRCreators, pr.PR.User.UserName) || pr.Reviews.IsReviewedByOneOf(Maintainers...) {
|
|
// submitter is maintainer or already reviewed
|
|
LogDebug("Package reviewed by maintainer (or subitter is maintainer), remove the rest of them")
|
|
// do not remove reviewers if they are also maintainers
|
|
Maintainers = slices.DeleteFunc(Maintainers, func(m string) bool { return slices.Contains(missing, m) })
|
|
extra = slices.Concat(Maintainers, []string{rs.BotUser})
|
|
} else {
|
|
// maintainer review is missing
|
|
LogDebug("Adding package maintainers to package git")
|
|
missing = append(missing, pkgMaintainers...)
|
|
}
|
|
}
|
|
|
|
slices.Sort(missing)
|
|
missing = slices.Compact(missing)
|
|
|
|
slices.Sort(extra)
|
|
extra = slices.Compact(extra)
|
|
|
|
// submitters cannot review their own work
|
|
if idx := slices.Index(missing, pr.PR.User.UserName); idx != -1 {
|
|
missing = slices.Delete(missing, idx, idx+1)
|
|
}
|
|
|
|
LogDebug("PR: ", PRtoString(pr.PR))
|
|
LogDebug(" preliminary add reviewers for PR:", missing)
|
|
LogDebug(" preliminary rm reviewers for PR:", extra)
|
|
|
|
// remove missing reviewers that are already done or already pending
|
|
for idx := 0; idx < len(missing); {
|
|
user := missing[idx]
|
|
if pr.Reviews.HasPendingReviewBy(user) || pr.Reviews.IsReviewedBy(user) {
|
|
missing = slices.Delete(missing, idx, idx+1)
|
|
LogDebug(" removing done/pending reviewer:", user)
|
|
} else {
|
|
idx++
|
|
}
|
|
}
|
|
|
|
// remove extra reviews that are actually only pending, and only pending by us
|
|
for idx := 0; idx < len(extra); {
|
|
user := extra[idx]
|
|
rr := pr.Reviews.FindReviewRequester(user)
|
|
if rr != nil && rr.User.UserName == rs.BotUser && pr.Reviews.HasPendingReviewBy(user) {
|
|
// good to remove this review
|
|
idx++
|
|
} else {
|
|
// this review should not be considered as extra by us
|
|
LogDebug(" - cannot find? to remove", user)
|
|
if rr != nil {
|
|
LogDebug(" ", rr.User.UserName, "vs.", rs.BotUser, pr.Reviews.HasPendingReviewBy(user))
|
|
}
|
|
extra = slices.Delete(extra, idx, idx+1)
|
|
}
|
|
}
|
|
|
|
LogDebug(" add reviewers for PR:", missing)
|
|
LogDebug(" rm reviewers for PR:", extra)
|
|
|
|
return missing, extra
|
|
}
|
|
|
|
func (rs *PRSet) AssignReviewers(gitea GiteaReviewFetcherAndRequesterAndUnrequester, maintainers MaintainershipData) error {
|
|
for idx, pr := range rs.PRs {
|
|
missingReviewers, extraReviewers := rs.FindMissingAndExtraReviewers(maintainers, idx)
|
|
|
|
if len(missingReviewers) > 0 {
|
|
LogDebug(" Requesting reviews from:", missingReviewers)
|
|
if !IsDryRun {
|
|
for _, r := range missingReviewers {
|
|
if _, err := gitea.RequestReviews(pr.PR, r); err != nil {
|
|
LogError("Cannot create reviews on", PRtoString(pr.PR), "for user:", r, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if len(extraReviewers) > 0 {
|
|
LogDebug(" UnRequesting reviews from:", extraReviewers)
|
|
if !IsDryRun {
|
|
for _, r := range extraReviewers {
|
|
org, repo, idx := pr.PRComponents()
|
|
if err := gitea.UnrequestReview(org, repo, idx, r); err != nil {
|
|
LogError("Cannot unrequest reviews on", PRtoString(pr.PR), "for user:", r, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (rs *PRSet) RemoveClosedPRs() {
|
|
rs.PRs = slices.DeleteFunc(rs.PRs, func(pr *PRInfo) bool {
|
|
return pr.PR.State != "open"
|
|
})
|
|
}
|
|
|
|
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 {
|
|
// Groups are expanded here because any group member can issue "merge ok" to the BotUser
|
|
groups := rs.Config.ReviewGroups
|
|
prjgit, err := rs.GetPrjGitPR()
|
|
if err == nil && prjgit != nil {
|
|
reviewers := slices.Concat(configReviewers.Prj, maintainers.ListProjectMaintainers(groups))
|
|
LogDebug("Fetching reviews for", prjgit.PR.Base.Repo.Owner.UserName, prjgit.PR.Base.Repo.Name, prjgit.PR.Index)
|
|
r, err := FetchGiteaReviews(gitea, 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
|
|
}
|
|
r.SetRequiredReviewers(reviewers)
|
|
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, groups))
|
|
LogDebug("Fetching reviews for", pr.PR.Base.Repo.Owner.UserName, pr.PR.Base.Repo.Name, pr.PR.Index)
|
|
r, err := FetchGiteaReviews(gitea, 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
|
|
}
|
|
r.SetRequiredReviewers(reviewers)
|
|
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
|
|
if rs.HasAutoStaging {
|
|
reviewers = append(reviewers, Bot_BuildReview)
|
|
}
|
|
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, pr.PR.Base.Repo.Owner.UserName, pr.PR.Base.Repo.Name, pr.PR.Index)
|
|
if err != nil {
|
|
LogError("Cannot fetch gitea reaviews for PR:", err)
|
|
return false
|
|
}
|
|
r.SetRequiredReviewers(reviewers)
|
|
|
|
is_manually_reviewed_ok = r.IsApproved()
|
|
LogDebug("PR to", pr.PR.Base.Repo.Name, "reviewed?", is_manually_reviewed_ok)
|
|
if !is_manually_reviewed_ok {
|
|
if GetLoggingLevel() > LogLevelInfo {
|
|
LogDebug("missing reviewers:", r.MissingReviews())
|
|
}
|
|
return false
|
|
}
|
|
|
|
if need_maintainer_review := !rs.IsPrjGitPR(pr.PR) || pr.PR.User.UserName != rs.BotUser; need_maintainer_review {
|
|
// Do not expand groups here, as the group-review-bot will ACK if group has reviewed.
|
|
if is_manually_reviewed_ok = maintainers.IsApproved(pkg, r.Reviews, pr.PR.User.UserName, nil); !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
|
|
|
|
_, _, prjgitBranch := rs.Config.GetPrjGit()
|
|
remote, err := git.GitClone(DefaultGitPrj, prjgitBranch, 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 {
|
|
panic("Can't handle conflicts yet")
|
|
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.
|
|
newRepoIssues := make(map[int64]string) // issue index -> org/repo
|
|
|
|
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
|
|
}
|
|
|
|
isNewRepo := false
|
|
for _, l := range prinfo.PR.Labels {
|
|
if l.Name == Label_NewRepository {
|
|
isNewRepo = true
|
|
break
|
|
}
|
|
}
|
|
|
|
br := rs.Config.Branch
|
|
if len(br) == 0 {
|
|
// if branch is unspecified, take it from the PR as it
|
|
// matches default branch already
|
|
br = prinfo.PR.Base.Name
|
|
} else if br != prinfo.PR.Base.Name {
|
|
panic(prinfo.PR.Base.Name + " is expected to match " + br)
|
|
}
|
|
|
|
if isNewRepo {
|
|
// Extract issue reference from body: "See issue #XYZ"
|
|
rx := regexp.MustCompile(`See issue #(\d+)`)
|
|
if matches := rx.FindStringSubmatch(prinfo.PR.Body); len(matches) > 1 {
|
|
if issueIdx, err := strconv.ParseInt(matches[1], 10, 64); err == nil {
|
|
// We need to know which project git this issue belongs to.
|
|
// Since the PR set is linked to a ProjectGit, we can use its org/repo.
|
|
prjGitOrg, prjGitRepo, _ := rs.Config.GetPrjGit()
|
|
newRepoIssues[issueIdx] = prjGitOrg + "/" + prjGitRepo
|
|
}
|
|
}
|
|
}
|
|
|
|
prinfo.RemoteName, err = git.GitClone(repo.Name, br, repo.SSHURL)
|
|
PanicOnError(err)
|
|
git.GitExecOrPanic(repo.Name, "fetch", prinfo.RemoteName, head.Sha)
|
|
|
|
if isNewRepo {
|
|
LogInfo("Force-pushing new repository branch", br, "to", head.Sha)
|
|
// we don't merge, we just set the branch to this commit
|
|
} else {
|
|
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
|
|
|
|
isNewRepo := false
|
|
for _, l := range prinfo.PR.Labels {
|
|
if l.Name == Label_NewRepository {
|
|
isNewRepo = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !IsDryRun {
|
|
if isNewRepo {
|
|
git.GitExecOrPanic(repo.Name, "push", "-f", prinfo.RemoteName, prinfo.PR.Head.Sha+":"+prinfo.PR.Base.Name)
|
|
} else {
|
|
git.GitExecOrPanic(repo.Name, "push", prinfo.RemoteName)
|
|
}
|
|
} else {
|
|
LogInfo("*** WOULD push", repo.Name, "to", prinfo.RemoteName)
|
|
}
|
|
}
|
|
|
|
// Close referencing issues
|
|
if !IsDryRun {
|
|
for issueIdx, prjPath := range newRepoIssues {
|
|
parts := strings.Split(prjPath, "/")
|
|
if len(parts) == 2 {
|
|
LogInfo("Closing issue", prjPath+"#"+strconv.FormatInt(issueIdx, 10))
|
|
gitea.UpdateIssue(parts[0], parts[1], issueIdx, &models.EditIssueOption{
|
|
State: "closed",
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|