2024-12-02 10:26:51 +01:00
package main
2024-12-04 08:55:40 +01:00
import (
2024-12-17 23:33:43 +01:00
"bufio"
"errors"
2025-01-29 17:29:09 +01:00
"fmt"
2025-02-04 17:44:49 +01:00
"os"
"path"
2024-12-17 23:33:43 +01:00
"slices"
"strings"
"src.opensuse.org/autogits/common"
2024-12-04 08:55:40 +01:00
"src.opensuse.org/autogits/common/gitea-generated/models"
)
2024-12-17 23:33:43 +01:00
type PRInfo struct {
2025-02-18 17:42:55 +01:00
pr * models . PullRequest
reviews * PRReviews
2024-12-17 23:33:43 +01:00
}
2025-01-03 00:46:40 +01:00
type PRSet struct {
2024-12-18 17:30:00 +01:00
prs [ ] PRInfo
config * common . AutogitConfig
2024-12-17 23:33:43 +01:00
}
2025-02-18 17:42:55 +01:00
func readPRData ( gitea common . GiteaPRFetcher , pr * models . PullRequest , currentSet [ ] PRInfo , config * common . AutogitConfig ) ( [ ] PRInfo , error ) {
2024-12-17 23:33:43 +01:00
for _ , p := range currentSet {
2025-02-18 17:42:55 +01:00
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 {
2024-12-17 23:33:43 +01:00
return nil , nil
}
}
retSet := [ ] PRInfo { PRInfo { pr : pr } }
2025-02-18 17:42:55 +01:00
// 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 )
2024-12-17 23:33:43 +01:00
}
}
return retSet , nil
2024-12-04 08:55:40 +01:00
}
2025-01-03 00:46:40 +01:00
func FetchPRSet ( gitea common . GiteaPRFetcher , org , repo string , num int64 , config * common . AutogitConfig ) ( * PRSet , error ) {
2025-02-18 17:42:55 +01:00
var pr * models . PullRequest
var err error
2025-02-17 17:49:02 +01:00
if org != config . Organization || repo != config . GitProjectName {
2025-02-18 17:42:55 +01:00
if pr , err = gitea . GetAssociatedPrjGitPR ( config . Organization , config . GitProjectName , org , repo , num ) ; err != nil {
2025-02-17 17:49:02 +01:00
return nil , err
}
2025-02-18 17:42:55 +01:00
if pr == nil {
if pr , err = gitea . GetPullRequest ( org , repo , num ) ; err != nil {
return nil , err
}
2025-02-17 17:49:02 +01:00
}
2025-02-18 17:42:55 +01:00
} else {
if pr , err = gitea . GetPullRequest ( org , repo , num ) ; err != nil {
2025-02-17 17:49:02 +01:00
return nil , err
}
2025-02-18 17:42:55 +01:00
}
prs , err := readPRData ( gitea , pr , nil , config )
if err != nil {
return nil , err
2024-12-17 23:33:43 +01:00
}
2025-01-03 00:46:40 +01:00
return & PRSet { prs : prs , config : config } , nil
2024-12-18 17:30:00 +01:00
}
2025-02-04 17:44:49 +01:00
func ( rs * PRSet ) IsPrjGitPR ( pr * models . PullRequest ) bool {
return pr . Base . Repo . Name == rs . config . GitProjectName && pr . Base . Repo . Owner . UserName == rs . config . Organization
}
2025-01-03 00:46:40 +01:00
func ( rs * PRSet ) GetPrjGitPR ( ) ( * models . PullRequest , error ) {
2025-01-02 13:46:59 +01:00
var ret * models . PullRequest
2024-12-18 17:30:00 +01:00
for _ , prinfo := range rs . prs {
2025-02-04 17:44:49 +01:00
if rs . IsPrjGitPR ( prinfo . pr ) {
2025-01-02 13:46:59 +01:00
if ret == nil {
ret = prinfo . pr
} else {
return nil , errors . New ( "Multiple PrjGit PRs in one review set" )
}
2024-12-18 17:30:00 +01:00
}
}
2025-01-02 13:46:59 +01:00
if ret != nil {
return ret , nil
}
2024-12-18 17:30:00 +01:00
return nil , errors . New ( "No PrjGit PR found" )
2024-12-17 23:33:43 +01:00
}
2024-12-09 00:39:55 +01:00
2025-01-03 00:46:40 +01:00
func ( rs * PRSet ) IsConsistent ( ) bool {
2025-01-02 13:46:59 +01:00
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
}
2025-02-18 17:42:55 +01:00
next_rs :
2025-01-02 13:46:59 +01:00
for _ , prinfo := range rs . prs {
if prjpr == prinfo . pr {
continue
}
2025-02-18 17:42:55 +01:00
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
}
2025-01-02 13:46:59 +01:00
}
2025-02-18 17:42:55 +01:00
return false
2025-01-02 13:46:59 +01:00
}
return true
}
2025-01-02 14:44:31 +01:00
2025-02-18 17:42:55 +01:00
func ( rs * PRSet ) AssignReviewers ( gitea common . GiteaReviewFetcherAndRequester , maintainers MaintainershipData ) error {
2025-02-14 17:13:51 +01:00
configReviewers := ParseReviewers ( rs . config . Reviewers )
for _ , pr := range rs . prs {
reviewers := [ ] string { }
if rs . IsPrjGitPR ( pr . pr ) {
reviewers = configReviewers . Prj
2025-02-19 00:06:54 +01:00
if len ( rs . prs ) == 1 {
reviewers = slices . Concat ( reviewers , maintainers . ListProjectMaintainers ( ) )
}
2025-02-14 17:13:51 +01:00
} else {
2025-02-19 00:06:54 +01:00
pkg := pr . pr . Base . Repo . Name
reviewers = slices . Concat ( configReviewers . Pkg , maintainers . ListProjectMaintainers ( ) , maintainers . ListPackageMaintainers ( pkg ) )
2025-02-14 17:13:51 +01:00
}
// 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 )
}
2025-02-17 17:49:02 +01:00
// remove reviewers that were already requested and are not stale
2025-02-18 17:42:55 +01:00
reviews , err := FetchGiteaReviews ( gitea , reviewers , pr . pr . Base . Repo . Owner . UserName , pr . pr . Base . Repo . Name , pr . pr . Index )
if err != nil {
return err
2025-02-17 17:49:02 +01:00
}
2025-02-18 17:42:55 +01:00
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 ++
}
}
2025-02-17 17:49:02 +01:00
2025-02-14 17:13:51 +01:00
// 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
}
2025-02-19 10:51:49 +01:00
func ( rs * PRSet ) IsApproved ( gitea common . GiteaPRChecker , maintainers MaintainershipData ) bool {
2025-02-05 18:30:08 +01:00
configReviewers := ParseReviewers ( rs . config . Reviewers )
2025-01-11 21:37:59 +01:00
is_reviewed := false
for _ , pr := range rs . prs {
2025-02-05 18:30:08 +01:00
var reviewers [ ] string
2025-02-19 10:51:49 +01:00
var pkg string
2025-02-05 18:30:08 +01:00
if rs . IsPrjGitPR ( pr . pr ) {
reviewers = configReviewers . Prj
2025-02-19 10:51:49 +01:00
pkg = ""
2025-02-05 18:30:08 +01:00
} else {
reviewers = configReviewers . Pkg
2025-02-19 10:51:49 +01:00
pkg = pr . pr . Base . Repo . Name
2025-02-05 18:30:08 +01:00
}
r , err := FetchGiteaReviews ( gitea , reviewers , pr . pr . Base . Repo . Owner . UserName , pr . pr . Base . Repo . Name , pr . pr . Index )
2025-01-11 21:37:59 +01:00
if err != nil {
return false
}
2025-02-18 12:40:32 +01:00
is_reviewed = r . IsApproved ( )
2025-01-11 21:37:59 +01:00
if ! is_reviewed {
return false
}
2025-02-19 10:51:49 +01:00
if is_reviewed = maintainers . IsApproved ( pkg , r . reviews ) ; ! is_reviewed {
return false
}
2025-01-11 21:37:59 +01:00
}
return is_reviewed
2025-01-02 14:44:31 +01:00
}
2025-01-27 17:43:50 +01:00
func ( rs * PRSet ) Merge ( ) error {
prjgit , err := rs . GetPrjGitPR ( )
if err != nil {
return err
}
2025-01-31 17:39:46 +01:00
gh := common . GitHandlerGeneratorImpl { }
2025-01-27 17:43:50 +01:00
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 )
2025-01-29 17:29:09 +01:00
git . GitExecOrPanic ( common . DefaultGitPrj , "fetch" , "origin" , prjgit . Base . Sha , prjgit . Head . Sha )
2025-01-27 17:43:50 +01:00
// if other changes merged, check if we have conflicts
2025-01-29 17:29:09 +01:00
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 )
}
/ *
2025-01-31 17:39:46 +01:00
rev := git . GitExecWithOutputOrPanic ( common . DefaultGitPrj , "rev-list" , "-1" , "HEAD" )
if rev != prjgit . Base . Sha {
panic ( "FIXME" )
}
* /
2025-02-18 17:42:55 +01:00
msg := "merging"
2025-01-27 17:43:50 +01:00
2025-01-29 17:29:09 +01:00
err = git . GitExec ( common . DefaultGitPrj , "merge" , "--no-ff" , "-m" , msg , prjgit . Head . Sha )
if err != nil {
2025-01-31 17:39:46 +01:00
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 )
}
2025-02-04 17:44:49 +01:00
s1 , err := git . GitExecWithOutput ( common . DefaultGitPrj , "cat-file" , "blob" , s . States [ 0 ] )
2025-01-31 17:39:46 +01:00
if err != nil {
return fmt . Errorf ( "Failed fetching data during .gitmodules merge resoulution: %w" , err )
}
2025-02-04 17:44:49 +01:00
s2 , err := git . GitExecWithOutput ( common . DefaultGitPrj , "cat-file" , "blob" , s . States [ 1 ] )
2025-01-31 17:39:46 +01:00
if err != nil {
return fmt . Errorf ( "Failed fetching data during .gitmodules merge resoulution: %w" , err )
}
2025-02-04 17:44:49 +01:00
s3 , err := git . GitExecWithOutput ( common . DefaultGitPrj , "cat-file" , "blob" , s . States [ 2 ] )
2025-01-31 17:39:46 +01:00
if err != nil {
return fmt . Errorf ( "Failed fetching data during .gitmodules merge resoulution: %w" , err )
}
2025-02-04 17:44:49 +01:00
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 ( ) , common . 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 )
}
os . CopyFS ( "/tmp/test" , os . DirFS ( git . GetPath ( ) ) )
git . GitExecOrPanic ( common . DefaultGitPrj , "add" , ".gitmodules" )
git . GitExecOrPanic ( common . DefaultGitPrj , "-c" , "core.editor=true" , "merge" , "--continue" )
2025-01-31 17:39:46 +01:00
}
}
2025-02-04 17:44:49 +01:00
}
2025-01-31 17:39:46 +01:00
2025-02-04 17:44:49 +01:00
// FF all non-prj git
for _ , prinfo := range rs . prs {
if rs . IsPrjGitPR ( prinfo . pr ) {
continue
2025-01-31 17:39:46 +01:00
}
2025-02-04 17:44:49 +01:00
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 )
2025-01-29 17:29:09 +01:00
}
2025-02-04 17:44:49 +01:00
// push changes
2025-01-27 17:43:50 +01:00
git . GitExecOrPanic ( common . DefaultGitPrj , "push" , "origin" )
2025-02-04 17:44:49 +01:00
for _ , prinfo := range rs . prs {
if rs . IsPrjGitPR ( prinfo . pr ) {
continue
}
git . GitExecOrPanic ( prinfo . pr . Base . Name , "push" , "origin" )
}
2025-01-27 17:43:50 +01:00
return nil
}