8
8
* @author TCSCODER
9
9
* @version 1.0
10
10
*/
11
-
12
11
const config = require ( 'config' ) ;
12
+ const uuid = require ( 'uuid' ) . v4 ;
13
13
const _ = require ( 'lodash' ) ;
14
14
const Joi = require ( 'joi' ) ;
15
15
const { Gitlab} = require ( '@gitbeaker/rest' ) ;
@@ -26,11 +26,11 @@ const request = superagentPromise(superagent, Promise);
26
26
// milliseconds per second
27
27
const MS_PER_SECOND = 1000 ;
28
28
29
+ const LOCK_TTL_SECONDS = 20 ;
30
+
31
+ const MAX_RETRY_COUNT = 30 ;
29
32
30
- const USER_ROLE_TO_REDIRECT_URI_MAP = {
31
- owner : config . GITLAB_OWNER_USER_CALLBACK_URL ,
32
- guest : config . GITLAB_GUEST_USER_CALLBACK_URL
33
- } ;
33
+ const COOLDOWN_TIME = 1000 ;
34
34
35
35
/**
36
36
* A schema for a Gitlab user, as stored in the TCX database.
@@ -55,7 +55,9 @@ const USER_SCHEMA = Joi.object().keys({
55
55
username : Joi . string ( ) . optional ( ) ,
56
56
type : Joi . string ( ) . valid ( 'gitlab' ) . required ( ) ,
57
57
id : Joi . string ( ) . optional ( ) ,
58
- role : Joi . string ( ) . valid ( 'owner' , 'guest' ) . required ( )
58
+ role : Joi . string ( ) . valid ( 'owner' , 'guest' ) . required ( ) ,
59
+ lockId : Joi . string ( ) . optional ( ) ,
60
+ lockExpiration : Joi . date ( ) . optional ( )
59
61
} ) . required ( ) ;
60
62
61
63
/**
@@ -107,31 +109,51 @@ class GitlabService {
107
109
* Refresh the user access token if needed
108
110
*/
109
111
async refreshAccessToken ( ) {
110
- const user = this . #user;
111
- if ( user . accessTokenExpiration && new Date ( ) . getTime ( ) > user . accessTokenExpiration . getTime ( ) -
112
- ( config . GITLAB_REFRESH_TOKEN_BEFORE_EXPIRATION * MS_PER_SECOND ) ) {
113
- const query = {
114
- client_id : config . GITLAB_CLIENT_ID ,
115
- client_secret : config . GITLAB_CLIENT_SECRET ,
116
- refresh_token : user . refreshToken ,
117
- grant_type : 'refresh_token' ,
118
- redirect_uri : USER_ROLE_TO_REDIRECT_URI_MAP [ user . role ]
119
- } ;
120
- const refreshTokenResult = await request
121
- . post ( `${ config . GITLAB_API_BASE_URL } /oauth/token` )
122
- . query ( query )
123
- . end ( ) ;
124
- // save user token data
125
- const expiresIn = refreshTokenResult . body . expires_in || config . GITLAB_ACCESS_TOKEN_DEFAULT_EXPIRATION ;
126
- const updates = {
127
- accessToken : refreshTokenResult . body . access_token ,
128
- accessTokenExpiration : new Date ( new Date ( ) . getTime ( ) + expiresIn * MS_PER_SECOND ) ,
129
- refreshToken : refreshTokenResult . body . refresh_token
130
- } ;
131
- _ . assign ( user , updates ) ;
132
- await dbHelper . update ( models . User , user . id , updates ) ;
112
+ const lockId = uuid ( ) . replace ( / - / g, '' ) ;
113
+ let lockedUser ;
114
+ let tries = 0 ;
115
+ try {
116
+ // eslint-disable-next-line no-constant-condition, no-restricted-syntax
117
+ while ( ( tries < MAX_RETRY_COUNT ) && ! ( lockedUser && lockedUser . lockId === lockId ) ) {
118
+ logger . debug ( `[Lock ID: ${ lockId } ][Attempt #${ tries + 1 } ] Acquiring lock on user ${ this . #user. username } .` ) ;
119
+ lockedUser = await dbHelper . acquireLockOnUser ( this . #user. id , lockId , LOCK_TTL_SECONDS * MS_PER_SECOND ) ;
120
+ await new Promise ( ( resolve ) => setTimeout ( resolve , COOLDOWN_TIME ) ) ;
121
+ tries += 1 ;
122
+ }
123
+ if ( ! lockedUser ) {
124
+ throw new Error ( `Failed to acquire lock on user ${ this . #user. id } after ${ tries } attempts.` ) ;
125
+ }
126
+ logger . debug ( `[Lock ID: ${ lockId } ] Acquired lock on user ${ this . #user. id } .` ) ;
127
+ if ( lockedUser . accessTokenExpiration && new Date ( ) . getTime ( ) > lockedUser . accessTokenExpiration . getTime ( ) -
128
+ ( config . GITLAB_REFRESH_TOKEN_BEFORE_EXPIRATION * MS_PER_SECOND ) ) {
129
+ logger . debug ( `[Lock ID: ${ lockId } ] Refreshing access token for user ${ this . #user. id } .` ) ;
130
+ const query = {
131
+ client_id : config . GITLAB_CLIENT_ID ,
132
+ client_secret : config . GITLAB_CLIENT_SECRET ,
133
+ refresh_token : lockedUser . refreshToken ,
134
+ grant_type : 'refresh_token'
135
+ } ;
136
+ const refreshTokenResult = await request
137
+ . post ( `${ config . GITLAB_API_BASE_URL } /oauth/token` )
138
+ . query ( query )
139
+ . end ( ) ;
140
+ // save user token data
141
+ const expiresIn = refreshTokenResult . body . expires_in || config . GITLAB_ACCESS_TOKEN_DEFAULT_EXPIRATION ;
142
+ const updates = {
143
+ accessToken : refreshTokenResult . body . access_token ,
144
+ accessTokenExpiration : new Date ( new Date ( ) . getTime ( ) + expiresIn * MS_PER_SECOND ) ,
145
+ refreshToken : refreshTokenResult . body . refresh_token
146
+ } ;
147
+ _ . assign ( lockedUser , updates ) ;
148
+ await dbHelper . update ( models . User , lockedUser . id , updates ) ;
149
+ }
150
+ return lockedUser ;
151
+ } finally {
152
+ if ( lockedUser ) {
153
+ logger . debug ( `[Lock ID: ${ lockId } ] Releasing lock on user ${ this . #user. id } .` ) ;
154
+ await dbHelper . releaseLockOnUser ( this . #user. id , lockId ) ;
155
+ }
133
156
}
134
- return user ;
135
157
}
136
158
137
159
/**
@@ -433,6 +455,60 @@ class GitlabService {
433
455
Joi . attempt ( { repository} , { repository : Joi . object ( ) . required ( ) } ) ;
434
456
await this . #gitlab. Projects . fork ( repository . id ) ;
435
457
}
458
+
459
+ /**
460
+ * Get the diff patch for a gitlab merge request
461
+ * @param {import('@gitbeaker/rest').MergeRequestSchemaWithBasicLabels } mergeRequest The merge request
462
+ * @returns {Promise<String> } The diff patch
463
+ */
464
+ async getMergeRequestDiffPatches ( mergeRequest ) {
465
+ Joi . attempt ( { mergeRequest} , {
466
+ mergeRequest : Joi . object ( ) . keys ( {
467
+ web_url : Joi . string ( ) . required ( )
468
+ } ) . unknown ( true ) . required ( )
469
+ } ) ;
470
+ const diff = await this . #gitlab. MergeRequests . allDiffs ( mergeRequest . target_project_id , mergeRequest . iid ) ;
471
+ const patchFile = diff . reduce ( ( acc , file ) => {
472
+ // Header
473
+ acc += `diff --git a/${ file . old_path } b/${ file . new_path } \n` ;
474
+ // Index
475
+ if ( file . new_file ) {
476
+ acc += `new file mode ${ file . b_mode } \n` ;
477
+ }
478
+ if ( file . deleted_file ) {
479
+ acc += `deleted file mode ${ file . a_mode } \n` ;
480
+ }
481
+ if ( file . diff && file . diff !== '' ) {
482
+ acc += `--- a/${ file . new_file ? '/dev/null' : file . old_path } \n` ;
483
+ acc += `+++ b/${ file . deleted_file ? '/dev/null' : file . new_path } \n` ;
484
+ acc += file . diff ;
485
+ } else if ( file . renamed_file ) {
486
+ acc += 'similarity index 100%\n' ;
487
+ acc += `rename from ${ file . old_path } \n` ;
488
+ acc += `rename to ${ file . new_path } \n` ;
489
+ }
490
+ return acc ;
491
+ } , '' ) ;
492
+ console . log ( patchFile ) ;
493
+ return patchFile ;
494
+ }
495
+
496
+ /**
497
+ * Get a list of all merge requests for a gitlab repository
498
+ * @param {ProjectSchema } repository The repository
499
+ * @param {Number } userId
500
+ */
501
+ async getOpenMergeRequestsByUser ( repository , userId ) {
502
+ Joi . attempt ( { repository, userId} , {
503
+ repository : Joi . object ( ) . required ( ) ,
504
+ userId : Joi . number ( ) . required ( )
505
+ } ) ;
506
+ return this . #gitlab. MergeRequests . all ( {
507
+ projectId : repository . id ,
508
+ state : 'opened' ,
509
+ authorId : userId
510
+ } ) ;
511
+ }
436
512
}
437
513
438
514
module . exports = GitlabService ;
0 commit comments