Skip to content

Commit ac3806b

Browse files
Golang version WIP
Signed-off-by: Lukasz Gryglicki <[email protected]> Assisted by [OpenAI](https://platform.openai.com/) Assisted by [GitHub Copilot](https://github.com/features/copilot)
1 parent 817bda4 commit ac3806b

File tree

3 files changed

+371
-158
lines changed

3 files changed

+371
-158
lines changed

cla-backend-go/github/github_repository.go

Lines changed: 369 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
118231
var GithubUserCache = NewCache(24 * time.Hour)
232+
var ModelUserCache = NewUserCache(24 * time.Hour)
233+
var ModelProjectUserCache = NewProjectUserCache(24 * time.Hour)
119234

120235
func 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+
605974
func 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

Comments
 (0)