@@ -20,6 +20,7 @@ import (
2020 "github.com/sirupsen/logrus"
2121
2222 "github.com/google/go-github/v37/github"
23+ "github.com/linuxfoundation/easycla/cla-backend-go/gen/v1/models"
2324 "github.com/linuxfoundation/easycla/cla-backend-go/logging"
2425)
2526
@@ -115,13 +116,129 @@ func (c *Cache) Cleanup() {
115116 }
116117}
117118
119+ func (c * Cache ) Delete (key [2 ]string ) { c .mu .Lock (); delete (c .data , key ); c .mu .Unlock () }
120+
121+ type userCacheEntry struct {
122+ value * models.User
123+ expiresAt time.Time
124+ }
125+
126+ type UserCache struct {
127+ data map [[3 ]string ]userCacheEntry
128+ mu sync.Mutex
129+ ttl time.Duration
130+ }
131+
132+ func NewUserCache (ttl time.Duration ) * UserCache {
133+ return & UserCache {
134+ data : make (map [[3 ]string ]userCacheEntry ),
135+ ttl : ttl ,
136+ }
137+ }
138+
139+ func (c * UserCache ) Get (key [3 ]string ) (* models.User , bool ) {
140+ c .mu .Lock ()
141+ defer c .mu .Unlock ()
142+ entry , found := c .data [key ]
143+ if ! found || time .Now ().After (entry .expiresAt ) {
144+ if found {
145+ delete (c .data , key )
146+ }
147+ return nil , false
148+ }
149+ return entry .value , true
150+ }
151+
152+ func (c * UserCache ) Set (key [3 ]string , value * models.User ) {
153+ c .mu .Lock ()
154+ defer c .mu .Unlock ()
155+ c .data [key ] = userCacheEntry {
156+ value : value ,
157+ expiresAt : time .Now ().Add (c .ttl ),
158+ }
159+ }
160+
161+ func (c * UserCache ) Cleanup () {
162+ c .mu .Lock ()
163+ defer c .mu .Unlock ()
164+ now := time .Now ()
165+ for k , v := range c .data {
166+ if now .After (v .expiresAt ) {
167+ delete (c .data , k )
168+ }
169+ }
170+ }
171+
172+ func (c * UserCache ) Delete (key [3 ]string ) { c .mu .Lock (); delete (c .data , key ); c .mu .Unlock () }
173+
174+ type projectUserCacheEntry struct {
175+ value * models.User
176+ signed bool
177+ affiliated bool
178+ expiresAt time.Time
179+ }
180+
181+ type ProjectUserCache struct {
182+ data map [[4 ]string ]projectUserCacheEntry
183+ mu sync.Mutex
184+ ttl time.Duration
185+ }
186+
187+ func NewProjectUserCache (ttl time.Duration ) * ProjectUserCache {
188+ return & ProjectUserCache {
189+ data : make (map [[4 ]string ]projectUserCacheEntry ),
190+ ttl : ttl ,
191+ }
192+ }
193+
194+ func (c * ProjectUserCache ) Get (key [4 ]string ) (* models.User , bool , bool , bool ) {
195+ c .mu .Lock ()
196+ defer c .mu .Unlock ()
197+ entry , found := c .data [key ]
198+ if ! found || time .Now ().After (entry .expiresAt ) {
199+ if found {
200+ delete (c .data , key )
201+ }
202+ return nil , false , false , false
203+ }
204+ return entry .value , entry .signed , entry .affiliated , true
205+ }
206+
207+ func (c * ProjectUserCache ) Set (key [4 ]string , value * models.User , signed , affiliated bool ) {
208+ c .mu .Lock ()
209+ defer c .mu .Unlock ()
210+ c .data [key ] = projectUserCacheEntry {
211+ value : value ,
212+ signed : signed ,
213+ affiliated : affiliated ,
214+ expiresAt : time .Now ().Add (c .ttl ),
215+ }
216+ }
217+
218+ func (c * ProjectUserCache ) Cleanup () {
219+ c .mu .Lock ()
220+ defer c .mu .Unlock ()
221+ now := time .Now ()
222+ for k , v := range c .data {
223+ if now .After (v .expiresAt ) {
224+ delete (c .data , k )
225+ }
226+ }
227+ }
228+
229+ func (c * ProjectUserCache ) Delete (key [4 ]string ) { c .mu .Lock (); delete (c .data , key ); c .mu .Unlock () }
230+
118231var GithubUserCache = NewCache (24 * time .Hour )
232+ var ModelUserCache = NewUserCache (24 * time .Hour )
233+ var ModelProjectUserCache = NewProjectUserCache (24 * time .Hour )
119234
120235func init () {
121236 go func () {
122237 for {
123238 time .Sleep (time .Hour )
124239 GithubUserCache .Cleanup ()
240+ ModelUserCache .Cleanup ()
241+ ModelProjectUserCache .Cleanup ()
125242 }
126243 }()
127244}
@@ -602,6 +719,258 @@ func GetCoAuthorCommits(
602719 return summary , found
603720}
604721
722+ func UserKey (id , login , email string ) [3 ]string {
723+ return [3 ]string {id , strings .ToLower (login ), strings .ToLower (strings .TrimSpace (email ))}
724+ }
725+
726+ func ProjectUserKey (projectID , id , login , email string ) [4 ]string {
727+ return [4 ]string {projectID , id , strings .ToLower (login ), strings .ToLower (strings .TrimSpace (email ))}
728+ }
729+
730+ // GetCommitAuthorSignedStatus checks if the commit author has signed the CLA for the given project
731+ func GetCommitAuthorSignedStatus (
732+ ctx context.Context ,
733+ usersService users.Service ,
734+ hasUserSigned func (context.Context , * models.User , string ) (* bool , * bool , error ),
735+ projectID string ,
736+ userSummary * UserCommitSummary ,
737+ signed * []* UserCommitSummary ,
738+ unsigned * []* UserCommitSummary ,
739+ ) {
740+ f := logrus.Fields {
741+ "functionName" : "github.github_repository.GetCommitAuthorsSignedStatuses" ,
742+ "projectID" : projectID ,
743+ "userSummary" : * userSummary ,
744+ }
745+ commitAuthorID := userSummary .GetCommitAuthorID ()
746+ commitAuthorUsername := userSummary .GetCommitAuthorUsername ()
747+ commitAuthorEmail := userSummary .GetCommitAuthorEmail ()
748+
749+ log .WithFields (f ).Debugf ("checking user - sha: %s, user ID: %s, username: %s, email: %s" ,
750+ userSummary .SHA , commitAuthorID , commitAuthorUsername , commitAuthorEmail )
751+
752+ // LG: cache_authors - start
753+ // Per-project cache - also caches per-project signatures status and affiliation
754+ // (project_id, id, login, email) -> (user || None, authorized, affiliated)
755+ projectCacheKey := ProjectUserKey (projectID , commitAuthorID , commitAuthorUsername , commitAuthorEmail )
756+ cachedUser , authorized , affiliated , ok := ModelProjectUserCache .Get (projectCacheKey )
757+ if cachedUser != nil {
758+ log .WithFields (f ).Debugf ("per-project cache: %+v -> (%+v, %v, %v, %v)" , projectCacheKey , * cachedUser , authorized , affiliated , ok )
759+ } else {
760+ log .WithFields (f ).Debugf ("per-project cache: %+v -> (%+v, nil, %v, %v)" , projectCacheKey , authorized , affiliated , ok )
761+ }
762+ if ok {
763+ if cachedUser == nil {
764+ log .WithFields (f ).Debugf ("per-project cache: unsigned, user is null" )
765+ * unsigned = append (* unsigned , userSummary )
766+ return
767+ }
768+ userSummary .Affiliated = affiliated
769+ if authorized {
770+ userSummary .Authorized = authorized
771+ log .WithFields (f ).Debugf ("per-project cache: signed" )
772+ * signed = append (* signed , userSummary )
773+ } else {
774+ log .WithFields (f ).Debugf ("per-project cache: unsigned, authorized is false" )
775+ * unsigned = append (* unsigned , userSummary )
776+ }
777+ return
778+ }
779+ // General cache (without project) - can only cache author details, but not per-project signature details
780+ // (id, login, email) -> (user || None)
781+ cacheKey := UserKey (commitAuthorID , commitAuthorUsername , commitAuthorEmail )
782+ cachedUser , ok = ModelUserCache .Get (cacheKey )
783+ if cachedUser != nil {
784+ log .WithFields (f ).Debugf ("general cache: %+v -> (%+v, %v)" , cacheKey , * cachedUser , ok )
785+ } else {
786+ log .WithFields (f ).Debugf ("general cache: %+v -> (nil, %v)" , cacheKey , ok )
787+ }
788+ if ok {
789+ if cachedUser == nil {
790+ log .WithFields (f ).Debugf ("general cache: unsigned, user is null" )
791+ * unsigned = append (* unsigned , userSummary )
792+ ModelProjectUserCache .Set (projectCacheKey , nil , false , false )
793+ return
794+ }
795+ user := cachedUser
796+ userSigned , companyAffiliation , signedErr := hasUserSigned (ctx , user , projectID )
797+ if signedErr != nil {
798+ log .WithFields (f ).WithError (signedErr ).Warnf ("has user signed error - user: %+v, project: %s" , user , projectID )
799+ log .WithFields (f ).Debugf ("general cache: unsigned, hasUserSigned error" )
800+ * unsigned = append (* unsigned , userSummary )
801+ ModelProjectUserCache .Set (projectCacheKey , user , false , false )
802+ return
803+ }
804+
805+ if companyAffiliation != nil {
806+ userSummary .Affiliated = * companyAffiliation
807+ }
808+
809+ if userSigned != nil {
810+ userSummary .Authorized = * userSigned
811+ if userSummary .Authorized {
812+ log .WithFields (f ).Debugf ("general cache: signed" )
813+ * signed = append (* signed , userSummary )
814+ ModelProjectUserCache .Set (projectCacheKey , user , true , userSummary .Affiliated )
815+ } else {
816+ log .WithFields (f ).Debugf ("general cache: unsigned, authorized is false" )
817+ * unsigned = append (* unsigned , userSummary )
818+ ModelProjectUserCache .Set (projectCacheKey , user , false , userSummary .Affiliated )
819+ }
820+ } else {
821+ log .WithFields (f ).Debugf ("general cache: unsigned, userSigned is null" )
822+ * unsigned = append (* unsigned , userSummary )
823+ ModelProjectUserCache .Set (projectCacheKey , user , false , userSummary .Affiliated )
824+ }
825+ return
826+ }
827+ // LG: cache_authors - end
828+
829+ var user * models.User
830+ var userErr error
831+
832+ if commitAuthorID != "" {
833+ log .WithFields (f ).Debugf ("looking up user by ID: %s" , commitAuthorID )
834+ user , userErr = usersService .GetUserByGitHubID (commitAuthorID )
835+ if userErr != nil {
836+ log .WithFields (f ).WithError (userErr ).Warnf ("unable to get user by github id: %s" , commitAuthorID )
837+ }
838+ if user != nil {
839+ log .WithFields (f ).Debugf ("found user by ID: %s" , commitAuthorID )
840+ }
841+ }
842+ if user == nil && commitAuthorUsername != "" {
843+ log .WithFields (f ).Debugf ("looking up user by username: %s" , commitAuthorUsername )
844+ user , userErr = usersService .GetUserByGitHubUsername (commitAuthorUsername )
845+ if userErr != nil {
846+ log .WithFields (f ).WithError (userErr ).Warnf ("unable to get user by github username: %s" , commitAuthorUsername )
847+ }
848+ if user != nil {
849+ log .WithFields (f ).Debugf ("found user by username: %s" , commitAuthorUsername )
850+ }
851+ }
852+ if user == nil && commitAuthorEmail != "" {
853+ log .WithFields (f ).Debugf ("looking up user by email: %s" , commitAuthorEmail )
854+ user , userErr = usersService .GetUserByEmail (commitAuthorEmail )
855+ if userErr != nil {
856+ log .WithFields (f ).WithError (userErr ).Warnf ("unable to get user by user email: %s" , commitAuthorEmail )
857+ }
858+ if user != nil {
859+ log .WithFields (f ).Debugf ("found user by email: %s" , commitAuthorEmail )
860+ }
861+ }
862+
863+ if user == nil {
864+ log .WithFields (f ).Debugf ("unable to find user for commit author - sha: %s, user ID: %s, username: %s, email: %s" ,
865+ userSummary .SHA , commitAuthorID , commitAuthorUsername , commitAuthorEmail )
866+ log .WithFields (f ).Debugf ("store caches: unsigned, user is null" )
867+ * unsigned = append (* unsigned , userSummary )
868+ ModelProjectUserCache .Set (projectCacheKey , nil , false , false )
869+ ModelUserCache .Set (cacheKey , nil )
870+ return
871+ }
872+
873+ log .WithFields (f ).Debugf ("checking to see if user has signed an ICLA or ECLA for project: %s" , projectID )
874+ userSigned , companyAffiliation , signedErr := hasUserSigned (ctx , user , projectID )
875+ if signedErr != nil {
876+ log .WithFields (f ).WithError (signedErr ).Warnf ("has user signed error - user: %+v, project: %s" , user , projectID )
877+ log .WithFields (f ).Debugf ("store caches: unsigned, hasUserSigned error" )
878+ * unsigned = append (* unsigned , userSummary )
879+ ModelProjectUserCache .Set (projectCacheKey , user , false , false )
880+ ModelUserCache .Set (cacheKey , user )
881+ return
882+ }
883+
884+ if companyAffiliation != nil {
885+ userSummary .Affiliated = * companyAffiliation
886+ }
887+
888+ if userSigned != nil {
889+ userSummary .Authorized = * userSigned
890+ if userSummary .Authorized {
891+ log .WithFields (f ).Debugf ("store caches: signed" )
892+ * signed = append (* signed , userSummary )
893+ ModelProjectUserCache .Set (projectCacheKey , user , true , userSummary .Affiliated )
894+ ModelUserCache .Set (cacheKey , user )
895+ } else {
896+ log .WithFields (f ).Debugf ("store caches: unsigned, authorized is false" )
897+ * unsigned = append (* unsigned , userSummary )
898+ ModelProjectUserCache .Set (projectCacheKey , user , false , userSummary .Affiliated )
899+ ModelUserCache .Set (cacheKey , user )
900+ }
901+ } else {
902+ log .WithFields (f ).Debugf ("store caches: unsigned, userSigned is null" )
903+ * unsigned = append (* unsigned , userSummary )
904+ ModelProjectUserCache .Set (projectCacheKey , user , false , userSummary .Affiliated )
905+ ModelUserCache .Set (cacheKey , user )
906+ }
907+ }
908+
909+ // GetCommitAuthorsSignedStatuses returns two slices of UserCommitSummary - signed and unsigned for the given project and commit authors
910+ func GetCommitAuthorsSignedStatuses (
911+ ctx context.Context ,
912+ usersService users.Service ,
913+ hasUserSigned func (context.Context , * models.User , string ) (* bool , * bool , error ),
914+ projectID string ,
915+ authors []* UserCommitSummary ,
916+ ) ([]* UserCommitSummary , []* UserCommitSummary ) {
917+ f := logrus.Fields {
918+ "functionName" : "github.github_repository.GetCommitAuthorsSignedStatuses" ,
919+ "projectID" : projectID ,
920+ }
921+ signed := make ([]* UserCommitSummary , 0 )
922+ unsigned := make ([]* UserCommitSummary , 0 )
923+
924+ // triage signed and unsigned users
925+ log .WithFields (f ).Debugf ("checking %d commit authors" , len (authors ))
926+ for _ , userSummary := range authors {
927+ if userSummary == nil || ! userSummary .IsValid () {
928+ if userSummary == nil {
929+ log .WithFields (f ).Debugf ("invalid user summary: nil" )
930+ } else {
931+ log .WithFields (f ).Debugf ("invalid user summary: %+v" , * userSummary )
932+ }
933+ unsigned = append (unsigned , userSummary )
934+ continue
935+ }
936+ GetCommitAuthorSignedStatus (ctx , usersService , hasUserSigned , projectID , userSummary , & signed , & unsigned )
937+ }
938+ return signed , unsigned
939+ }
940+
941+ // GetCommitAuthorsSignedStatusesST returns two slices of UserCommitSummary - signed and unsigned for the given project and commit authors
942+ // ST suffix = single threaded version
943+ func GetCommitAuthorsSignedStatusesST (
944+ ctx context.Context ,
945+ usersService users.Service ,
946+ hasUserSigned func (context.Context , * models.User , string ) (* bool , * bool , error ),
947+ projectID string ,
948+ authors []* UserCommitSummary ,
949+ ) ([]* UserCommitSummary , []* UserCommitSummary ) {
950+ f := logrus.Fields {
951+ "functionName" : "github.github_repository.GetCommitAuthorsSignedStatusesST" ,
952+ "projectID" : projectID ,
953+ }
954+ signed := make ([]* UserCommitSummary , 0 )
955+ unsigned := make ([]* UserCommitSummary , 0 )
956+
957+ // triage signed and unsigned users
958+ log .WithFields (f ).Debugf ("checking %d commit authors" , len (authors ))
959+ for _ , userSummary := range authors {
960+ if userSummary == nil || ! userSummary .IsValid () {
961+ if userSummary == nil {
962+ log .WithFields (f ).Debugf ("invalid user summary: nil" )
963+ } else {
964+ log .WithFields (f ).Debugf ("invalid user summary: %+v" , * userSummary )
965+ }
966+ unsigned = append (unsigned , userSummary )
967+ continue
968+ }
969+ GetCommitAuthorSignedStatus (ctx , usersService , hasUserSigned , projectID , userSummary , & signed , & unsigned )
970+ }
971+ return signed , unsigned
972+ }
973+
605974func GetPullRequestCommitAuthors (ctx context.Context , usersService users.Service , installationID int64 , pullRequestID int , owner , repo string , withCoAuthors bool ) ([]* UserCommitSummary , * string , bool , error ) {
606975 f := logrus.Fields {
607976 "functionName" : "github.github_repository.GetPullRequestCommitAuthors" ,
0 commit comments