@@ -8,11 +8,13 @@ import (
88 "context"
99 "fmt"
1010 "io"
11+ "regexp"
1112 "strconv"
1213 "strings"
1314
1415 "code.gitea.io/gitea/models/db"
1516 git_model "code.gitea.io/gitea/models/git"
17+ org_model "code.gitea.io/gitea/models/organization"
1618 pull_model "code.gitea.io/gitea/models/pull"
1719 repo_model "code.gitea.io/gitea/models/repo"
1820 user_model "code.gitea.io/gitea/models/user"
@@ -887,3 +889,222 @@ func MergeBlockedByOfficialReviewRequests(ctx context.Context, protectBranch *gi
887889func MergeBlockedByOutdatedBranch (protectBranch * git_model.ProtectedBranch , pr * PullRequest ) bool {
888890 return protectBranch .BlockOnOutdatedBranch && pr .CommitsBehind > 0
889891}
892+
893+ func PullRequestCodeOwnersReview (ctx context.Context , pull * Issue , pr * PullRequest ) error {
894+ files := []string {"CODEOWNERS" , "docs/CODEOWNERS" , ".gitea/CODEOWNERS" }
895+
896+ if pr .IsWorkInProgress () {
897+ return nil
898+ }
899+
900+ if err := pr .LoadBaseRepo (ctx ); err != nil {
901+ return err
902+ }
903+
904+ repo , err := git .OpenRepository (ctx , pr .BaseRepo .RepoPath ())
905+ if err != nil {
906+ return err
907+ }
908+ defer repo .Close ()
909+
910+ branch , err := repo .GetDefaultBranch ()
911+ if err != nil {
912+ return err
913+ }
914+
915+ commit , err := repo .GetBranchCommit (branch )
916+ if err != nil {
917+ return err
918+ }
919+
920+ var data string
921+ for _ , file := range files {
922+ if blob , err := commit .GetBlobByPath (file ); err == nil {
923+ data , err = blob .GetBlobContent ()
924+ if err == nil {
925+ break
926+ }
927+ }
928+ }
929+
930+ rules , _ := GetCodeOwnersFromContent (ctx , data )
931+ changedFiles , err := repo .GetFilesChangedBetween (git .BranchPrefix + pr .BaseBranch , pr .GetGitRefName ())
932+ if err != nil {
933+ return err
934+ }
935+
936+ uniqUsers := make (map [int64 ]* user_model.User )
937+ uniqTeams := make (map [string ]* org_model.Team )
938+ for _ , rule := range rules {
939+ for _ , f := range changedFiles {
940+ if (rule .Rule .MatchString (f ) && ! rule .Negative ) || (! rule .Rule .MatchString (f ) && rule .Negative ) {
941+ for _ , u := range rule .Users {
942+ uniqUsers [u .ID ] = u
943+ }
944+ for _ , t := range rule .Teams {
945+ uniqTeams [fmt .Sprintf ("%d/%d" , t .OrgID , t .ID )] = t
946+ }
947+ }
948+ }
949+ }
950+
951+ for _ , u := range uniqUsers {
952+ if u .ID != pull .Poster .ID {
953+ if _ , err := AddReviewRequest (pull , u , pull .Poster ); err != nil {
954+ log .Warn ("Failed add assignee user: %s to PR review: %s#%d, error: %s" , u .Name , pr .BaseRepo .Name , pr .ID , err )
955+ return err
956+ }
957+ }
958+ }
959+ for _ , t := range uniqTeams {
960+ if _ , err := AddTeamReviewRequest (pull , t , pull .Poster ); err != nil {
961+ log .Warn ("Failed add assignee team: %s to PR review: %s#%d, error: %s" , t .Name , pr .BaseRepo .Name , pr .ID , err )
962+ return err
963+ }
964+ }
965+
966+ return nil
967+ }
968+
969+ // GetCodeOwnersFromContent returns the code owners configuration
970+ // Return empty slice if files missing
971+ // Return warning messages on parsing errors
972+ // We're trying to do the best we can when parsing a file.
973+ // Invalid lines are skipped. Non-existent users and teams too.
974+ func GetCodeOwnersFromContent (ctx context.Context , data string ) ([]* CodeOwnerRule , []string ) {
975+ if len (data ) == 0 {
976+ return nil , nil
977+ }
978+
979+ rules := make ([]* CodeOwnerRule , 0 )
980+ lines := strings .Split (data , "\n " )
981+ warnings := make ([]string , 0 )
982+
983+ for i , line := range lines {
984+ tokens := TokenizeCodeOwnersLine (line )
985+ if len (tokens ) == 0 {
986+ continue
987+ } else if len (tokens ) < 2 {
988+ warnings = append (warnings , fmt .Sprintf ("Line: %d: incorrect format" , i + 1 ))
989+ continue
990+ }
991+ rule , wr := ParseCodeOwnersLine (ctx , tokens )
992+ for _ , w := range wr {
993+ warnings = append (warnings , fmt .Sprintf ("Line: %d: %s" , i + 1 , w ))
994+ }
995+ if rule == nil {
996+ continue
997+ }
998+
999+ rules = append (rules , rule )
1000+ }
1001+
1002+ return rules , warnings
1003+ }
1004+
1005+ type CodeOwnerRule struct {
1006+ Rule * regexp.Regexp
1007+ Negative bool
1008+ Users []* user_model.User
1009+ Teams []* org_model.Team
1010+ }
1011+
1012+ func ParseCodeOwnersLine (ctx context.Context , tokens []string ) (* CodeOwnerRule , []string ) {
1013+ var err error
1014+ rule := & CodeOwnerRule {
1015+ Users : make ([]* user_model.User , 0 ),
1016+ Teams : make ([]* org_model.Team , 0 ),
1017+ Negative : strings .HasPrefix (tokens [0 ], "!" ),
1018+ }
1019+
1020+ warnings := make ([]string , 0 )
1021+
1022+ rule .Rule , err = regexp .Compile (fmt .Sprintf ("^%s$" , strings .TrimPrefix (tokens [0 ], "!" )))
1023+ if err != nil {
1024+ warnings = append (warnings , fmt .Sprintf ("incorrect codeowner regexp: %s" , err ))
1025+ return nil , warnings
1026+ }
1027+
1028+ for _ , user := range tokens [1 :] {
1029+ user = strings .TrimPrefix (user , "@" )
1030+
1031+ // Only @org/team can contain slashes
1032+ if strings .Contains (user , "/" ) {
1033+ s := strings .Split (user , "/" )
1034+ if len (s ) != 2 {
1035+ warnings = append (warnings , fmt .Sprintf ("incorrect codeowner group: %s" , user ))
1036+ continue
1037+ }
1038+ orgName := s [0 ]
1039+ teamName := s [1 ]
1040+
1041+ org , err := org_model .GetOrgByName (ctx , orgName )
1042+ if err != nil {
1043+ warnings = append (warnings , fmt .Sprintf ("incorrect codeowner organization: %s" , user ))
1044+ continue
1045+ }
1046+ teams , err := org .LoadTeams ()
1047+ if err != nil {
1048+ warnings = append (warnings , fmt .Sprintf ("incorrect codeowner team: %s" , user ))
1049+ continue
1050+ }
1051+
1052+ for _ , team := range teams {
1053+ if team .Name == teamName {
1054+ rule .Teams = append (rule .Teams , team )
1055+ }
1056+ }
1057+ } else {
1058+ u , err := user_model .GetUserByName (ctx , user )
1059+ if err != nil {
1060+ warnings = append (warnings , fmt .Sprintf ("incorrect codeowner user: %s" , user ))
1061+ continue
1062+ }
1063+ rule .Users = append (rule .Users , u )
1064+ }
1065+ }
1066+
1067+ if (len (rule .Users ) == 0 ) && (len (rule .Teams ) == 0 ) {
1068+ warnings = append (warnings , "no users/groups matched" )
1069+ return nil , warnings
1070+ }
1071+
1072+ return rule , warnings
1073+ }
1074+
1075+ func TokenizeCodeOwnersLine (line string ) []string {
1076+ if len (line ) == 0 {
1077+ return nil
1078+ }
1079+
1080+ line = strings .TrimSpace (line )
1081+ line = strings .ReplaceAll (line , "\t " , " " )
1082+
1083+ tokens := make ([]string , 0 )
1084+
1085+ escape := false
1086+ token := ""
1087+ for _ , char := range line {
1088+ if escape {
1089+ token += string (char )
1090+ escape = false
1091+ } else if string (char ) == "\\ " {
1092+ escape = true
1093+ } else if string (char ) == "#" {
1094+ break
1095+ } else if string (char ) == " " {
1096+ if len (token ) > 0 {
1097+ tokens = append (tokens , token )
1098+ token = ""
1099+ }
1100+ } else {
1101+ token += string (char )
1102+ }
1103+ }
1104+
1105+ if len (token ) > 0 {
1106+ tokens = append (tokens , token )
1107+ }
1108+
1109+ return tokens
1110+ }
0 commit comments