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 }