@@ -8,11 +8,13 @@ import (
8
8
"context"
9
9
"fmt"
10
10
"io"
11
+ "regexp"
11
12
"strconv"
12
13
"strings"
13
14
14
15
"code.gitea.io/gitea/models/db"
15
16
git_model "code.gitea.io/gitea/models/git"
17
+ org_model "code.gitea.io/gitea/models/organization"
16
18
pull_model "code.gitea.io/gitea/models/pull"
17
19
repo_model "code.gitea.io/gitea/models/repo"
18
20
user_model "code.gitea.io/gitea/models/user"
@@ -887,3 +889,222 @@ func MergeBlockedByOfficialReviewRequests(ctx context.Context, protectBranch *gi
887
889
func MergeBlockedByOutdatedBranch (protectBranch * git_model.ProtectedBranch , pr * PullRequest ) bool {
888
890
return protectBranch .BlockOnOutdatedBranch && pr .CommitsBehind > 0
889
891
}
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