• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

moosetechnology / GitProjectHealth / 10702519471

04 Sep 2024 01:19PM UTC coverage: 18.182%. First build
10702519471

Pull #49

github

web-flow
Merge 7aeba7371 into 5b5dd6942
Pull Request #49: Feature/project into catalogue

1 of 95 new or added lines in 3 files covered. (1.05%)

1590 of 8745 relevant lines covered (18.18%)

0.18 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

33.14
/src/GitLabHealth-Model-Importer/GLHModelImporter.class.st
1
Class {
2
        #name : #GLHModelImporter,
3
        #superclass : #Object,
4
        #instVars : [
5
                'glhModel',
6
                'glhApi',
7
                'withFiles',
8
                'withCommitDiffs',
9
                'withCommitsSince',
10
                'withInitialCommits',
11
                'withInitialMergeRequest',
12
                'generalReader',
13
                'userCatalogue'
14
        ],
15
        #classVars : [
16
                'currentImporter'
17
        ],
18
        #category : #'GitLabHealth-Model-Importer'
19
}
20

21
{ #category : #accessing }
22
GLHModelImporter class >> current [
×
23

×
24
        ^ currentImporter
×
25
]
×
26

27
{ #category : #'private - api' }
28
GLHModelImporter >> addCommits: commitsList toRepository: aProjectRepository [
×
29
        "I take a list of GLHCommit. But some might have been parsed but are already on the model..."
×
30

×
31
        "I return the list of added commits"
×
32

×
33
        | existingCommits newlyFoundCommit |
×
34
        existingCommits := aProjectRepository mooseModel allWithType:
×
35
                                   GLHCommit.
×
36
        newlyFoundCommit := commitsList reject: [ :commitParsed |
×
37
                                    existingCommits anySatisfy: [ :existingCommit |
×
38
                                            existingCommit id = commitParsed id ] ].
×
39
        aProjectRepository mooseModel addAll: newlyFoundCommit.
×
40
        aProjectRepository commits addAll: newlyFoundCommit.
×
41
        ^ newlyFoundCommit
×
42
]
×
43

44
{ #category : #private }
45
GLHModelImporter >> addGroupResultToModel: groupResult [
1✔
46
        |group|
1✔
47
        group := self glhModel add: groupResult unless: self blockOnIdEquality.
1✔
48
        self glhModel
1✔
49
                addAll: group projects
1✔
50
                unless: self blockOnIdEquality.
1✔
51
        ^ group 
1✔
52
]
1✔
53

54
{ #category : #accessing }
55
GLHModelImporter >> beWithFiles [
×
56
        withFiles := true
×
57
]
×
58

59
{ #category : #accessing }
60
GLHModelImporter >> beWithouFiles [
×
61
        withFiles := false
×
62
]
×
63

64
{ #category : #'as yet unclassified' }
65
GLHModelImporter >> blockEqualityOn: aSymbol [
1✔
66
        ^ [ :existing :new |
1✔
67
          (existing perform: aSymbol) = (new perform: aSymbol) ]
1✔
68
]
1✔
69

70
{ #category : #equality }
71
GLHModelImporter >> blockForDiffEquality [
1✔
72
        ^ [ :existing :new |
1✔
73
                        existing diffString size = new diffString size and: [
1✔
74
                                existing diffString = new diffString ] ]
1✔
75
]
1✔
76

77
{ #category : #equality }
78
GLHModelImporter >> blockOnIdEquality [
1✔
79

1✔
80
        ^ [ :existing :new |
1✔
81
          existing id = new id ]
1✔
82
]
1✔
83

84
{ #category : #equality }
85
GLHModelImporter >> blockOnNameEquality [
1✔
86

1✔
87
        ^ self blockEqualityOn: #name
1✔
88
]
1✔
89

90
{ #category : #commit }
91
GLHModelImporter >> chainsCommitsFrom: commitsCollection [
×
92

×
93
        | dic |
×
94
        
×
95
        ('Chains ', commitsCollection size printString , ' commits') recordInfo.
×
96
        
×
97
        dic := ((self glhModel allWithType: GLHCommit) collect: [ :commit |
×
98
                        commit id -> commit ]) asSet asDictionary.
×
99

×
100
        commitsCollection do: [ :commit |
×
101
                commit parent_ids do: [ :parentId | 
×
102
                        dic
×
103
                                at: parentId
×
104
                                ifPresent: [ :parentCommit |
×
105
                                        parentCommit childCommits
×
106
                                                add: commit
×
107
                                                unless: self blockOnIdEquality ]
×
108
                                ifAbsent: [  ] ] ].
×
109
        ^ commitsCollection
×
110
]
×
111

112
{ #category : #'private - api' }
113
GLHModelImporter >> completeImportProject: aGLHProject [
1✔
114

1✔
115
        | importedProject |
1✔
116
        ('Complete import of project: ' , aGLHProject id printString)
1✔
117
                recordInfo.
1✔
118
        aGLHProject repository ifNotNil: [ ^ aGLHProject ].
1✔
119
        
1✔
120
        importedProject := self glhModel
1✔
121
                                   add: aGLHProject
1✔
122
                                   unless: self blockOnIdEquality.
1✔
123

1✔
124
        self importPipelinesOfProject: importedProject.
1✔
125

1✔
126
        "aGLHProject creator: (self importUser: aGLHProject creator_id)."
1✔
127

1✔
128
        (self importUser: importedProject creator_id) addCreatedProject:
1✔
129
                importedProject.
1✔
130

1✔
131

1✔
132
        importedProject repository: GLHRepository new.
1✔
133
        self glhModel add: importedProject repository.
1✔
134
        self importRepository: importedProject repository.
1✔
135

1✔
136
        ^ importedProject
1✔
137
]
1✔
138

139
{ #category : #'as yet unclassified' }
140
GLHModelImporter >> completeImportedCommit: aCommit [
×
141

×
142
        ('completing commit: ' , aCommit short_id printString) recordInfo.
×
143
        self importCreatorOfCommit: aCommit.
×
144

×
145
        self withCommitDiffs ifTrue: [
×
146
                | diffs |
×
147
                aCommit diffs ifEmpty: [
×
148
                        diffs := self importDiffOfCommit: aCommit.
×
149
                        self glhModel addAll: diffs unless: self blockForDiffEquality ] ].
×
150

×
151
        ^ aCommit
×
152
]
×
153

154
{ #category : #'private - configure reader' }
155
GLHModelImporter >> configureReaderForCommit: reader [
1✔
156

1✔
157
          reader for: GLHCommit do: [ :mapping |
1✔
158
                mapping mapInstVars:
1✔
159
                        #( id short_id title author_name author_email committer_name
1✔
160
                           committer_email message web_url ).
1✔
161
                (mapping mapInstVar: #authored_date) valueSchema: DateAndTime.
1✔
162
                (mapping mapInstVar: #committed_date) valueSchema: DateAndTime.
1✔
163
                (mapping mapInstVar: #created_at) valueSchema: DateAndTime.
1✔
164
                (mapping mapInstVar: #parent_ids) valueSchema: #ArrayOfIds.
1✔
165
                mapping
1✔
166
                        mapProperty: 'stats'
1✔
167
                        getter: [ :el | "Not used" ]
1✔
168
                        setter: [ :commit :value |
1✔
169
                                commit deletions: (value at: #deletions).
1✔
170
                                commit additions: (value at: #additions) ] ].
1✔
171

1✔
172
        reader for: DateAndTime customDo: [ :mapping |
1✔
173
                mapping decoder: [ :string | DateAndTime fromString: string ] ].
1✔
174

1✔
175
        reader
1✔
176
                for: #ArrayOfIds
1✔
177
                customDo: [ :mapping | mapping decoder: [ :string | string ] ].
1✔
178
  
1✔
179
        reader
1✔
180
                for: #ArrayOfCommit
1✔
181
                customDo: [ :customMappting |
1✔
182
                customMappting listOfElementSchema: GLHCommit ].
1✔
183

1✔
184
]
1✔
185

186
{ #category : #'private - configure reader' }
187
GLHModelImporter >> configureReaderForDiffs: reader [
1✔
188

1✔
189
        reader for: GLHDiff do: [ :mapping |
1✔
190
                mapping mapInstVars:
1✔
191
                        #( deleted_file new_file new_path old_path renamed_file ).
1✔
192
                mapping mapInstVar: #diffString to: #diff ].
1✔
193

1✔
194
        reader
1✔
195
                for: #ArrayOfDiffs
1✔
196
                customDo: [ :customMappting |
1✔
197
                customMappting listOfElementSchema: GLHDiff ].
1✔
198
        ^ reader
1✔
199
]
1✔
200

201
{ #category : #'private - configure reader' }
202
GLHModelImporter >> configureReaderForGroup: reader [
1✔
203

1✔
204
        reader for: GLHGroup do: [ :mapping |
1✔
205
                mapping mapInstVars.
1✔
206
                (mapping mapInstVar: #projects) valueSchema: #ArrayOfProjects ].
1✔
207
        reader mapInstVarsFor: GLHProject.
1✔
208
        reader
1✔
209
                for: #ArrayOfProjects
1✔
210
                customDo: [ :customMappting |
1✔
211
                customMappting listOfElementSchema: GLHProject ].
1✔
212
        reader
1✔
213
                for: #ArrayOfGroups
1✔
214
                customDo: [ :customMappting |
1✔
215
                customMappting listOfElementSchema: GLHGroup ]
1✔
216
]
1✔
217

218
{ #category : #private }
219
GLHModelImporter >> convertApiFileAsFile: aAPIFile [
×
220

×
221
        aAPIFile type = 'tree' ifTrue: [ 
×
222
                ^ GLHFileDirectory new
×
223
                          name: aAPIFile name;
×
224
                          yourself ].
×
225
        ^ GLHFileBlob new
×
226
                  name: aAPIFile name;
×
227
                  yourself
×
228
]
×
229

230
{ #category : #'as yet unclassified' }
231
GLHModelImporter >> detectEntityType: aType overAttribut: aSelector equalTo: value [
×
232

×
233
        ^ (self glhModel allWithType: aType) detect: [ :entity |
×
234
                  (entity perform: aSelector) = value ] ifNone: [ nil ]. 
×
235
]
×
236

237
{ #category : #'as yet unclassified' }
238
GLHModelImporter >> filterCommitChanges: aCollection [
1✔
239

1✔
240
        ^ aCollection reject: [ :line |
1✔
241
                  | trimmedLine |
1✔
242
                  trimmedLine := line trimLeft.
1✔
243
                  (trimmedLine beginsWith: '---') or: [
1✔
244
                          (trimmedLine beginsWith: '+++') or: [
1✔
245
                                  trimmedLine beginsWith: '\ No newline at end of file' ] ] ]
1✔
246
]
1✔
247

248
{ #category : #accessing }
249
GLHModelImporter >> glhApi [
1✔
250

1✔
251
        ^ glhApi
1✔
252
]
1✔
253

254
{ #category : #accessing }
255
GLHModelImporter >> glhApi: anObject [
1✔
256

1✔
257
        glhApi := anObject
1✔
258
]
1✔
259

260
{ #category : #accessing }
261
GLHModelImporter >> glhModel [
1✔
262

1✔
263
        ^ glhModel
1✔
264
]
1✔
265

266
{ #category : #accessing }
267
GLHModelImporter >> glhModel: anObject [
1✔
268

1✔
269
        glhModel := anObject
1✔
270
]
1✔
271

272
{ #category : #'as yet unclassified' }
273
GLHModelImporter >> importActiveHumanUsers [
×
274

×
275
        | newlyFoundUser page foundUsers |
×
276
        page := 0.
×
277
        foundUsers := OrderedCollection new.
×
278
        newlyFoundUser := { true }.
×
279
        [ newlyFoundUser isNotEmpty ] whileTrue: [
×
280
                | results |
×
281
                page := page + 1.
×
282
                ('import users page ' , page printString) recordInfo.
×
283
                results := self glhApi
×
284
                                   usersHuman: true
×
285
                                   active: true
×
286
                                   withoutProjectBots: true
×
287
                                   perPage: 100
×
288
                                   page: page.
×
289

×
290
                newlyFoundUser := self parseUsersResult: results.
×
291
                "newlyFoundCommit do: [ :c | c repository: aProject repository ]."
×
292

×
293
                foundUsers addAll:
×
294
                        (self glhModel
×
295
                                 addAll: newlyFoundUser
×
296
                                 unless: self blockOnIdEquality) ].
×
297

×
298

×
299
        ^ foundUsers
×
300
]
×
301

302
{ #category : #api }
303
GLHModelImporter >> importAllGroups [
×
304

×
305
        | page foundGroups newlyFoundGroups |
×
306
        page := 0.
×
307
        foundGroups := OrderedCollection new.
×
308
        newlyFoundGroups := { true }.
×
309
        [ newlyFoundGroups isNotEmpty ] whileTrue: [
×
310
                | results |
×
311
                page := page + 1.
×
312
                results := self glhApi listGroupsWithTopLevelOnly: true page: page.
×
313

×
314
                newlyFoundGroups := generalReader
×
315
                                            on: results readStream;
×
316
                                            nextAs: #ArrayOfGroups.
×
317
                foundGroups addAll: newlyFoundGroups ].
×
318
        ^ foundGroups
×
319
]
×
320

321
{ #category : #'as yet unclassified' }
322
GLHModelImporter >> importAndLoadLatestsCommitsOfProject: aGLHProject [
×
323

×
324
        | commits |
×
325
        self completeImportProject: aGLHProject.
×
326
        commits := self importLastestCommitsOfProject: aGLHProject.
×
327
        commits do: [ :commit | self completeImportedCommit: commit ].
×
328
        self chainsCommitsFrom: commits.
×
329
        ^ commits
×
330
]
×
331

332
{ #category : #'private - api' }
333
GLHModelImporter >> importCommit: aCommitID ofProject: aGLHProject [
×
334

×
335
        | result parsedResult |
×
336
        (self glhModel allWithType: GLHCommit) asOrderedCollection
×
337
                detect: [ :commit | commit id = aCommitID ]
×
338
                ifFound: [ :commit | ^ commit ].
×
339
        result := self glhApi
×
340
                          commit: aCommitID
×
341
                          ofProject: aGLHProject id
×
342
                          withStat: false.
×
343
        parsedResult := self parseCommitResult: result.
×
344
        self
×
345
                addCommits: { parsedResult }
×
346
                toRepository: aGLHProject repository.
×
347
        ^ parsedResult
×
348
]
×
349

350
{ #category : #'as yet unclassified' }
351
GLHModelImporter >> importCommitOfProject: anProject withId: anID [
×
352

×
353
        | commit result |
×
354
        anID ifNil: [ ^ nil ].
×
355

×
356
        ('looking for commit ' , anID printString , ' in project : '
×
357
         , anProject id printString) recordInfo.
×
358

×
359
        commit := (self
×
360
                           detectEntityType: GLHCommit
×
361
                           overAttribut: #id
×
362
                           equalTo: anID) ifNil: [
×
363
                          result := self glhApi commit: anID ofProject: anProject id.
×
364
                          commit := (self parseCommitsResult: '[' , result , ']')
×
365
                                            first.
×
366

×
367
                          self glhModel add: commit unless: self blockOnIdEquality.
×
368
                          commit repository: anProject repository.
×
369

×
370
                          commit ].
×
371

×
372
        self withCommitDiffs ifTrue: [ self importDiffOfCommit: commit ].
×
373

×
374
        ^ commit
×
375
]
×
376

377
{ #category : #'as yet unclassified' }
378
GLHModelImporter >> importCommitsFollowing: aCommit upToDays: aNumberOfDay [
×
379
        "import the 'n' commits of a project starting from an initial 'aCommit' commit. 
×
380
        Lazy import does not import the entities inside the model"
×
381

×
382
        | date |
×
383
        date := aCommit created_at asDateAndTime.
×
384

×
385
        ^ self
×
386
                  importCommitsOfBranch: aCommit branch
×
387
                  forRefName: aCommit branch name
×
388
                  since: date
×
389
                  until: (date + aNumberOfDay day)
×
390
]
×
391

392
{ #category : #commit }
393
GLHModelImporter >> importCommitsOProject: aProject since: fromDate until: toDate [
×
394

×
395
        | newlyFoundCommit page foundCommit |
×
396
        page := 0.
×
397
        foundCommit := OrderedCollection new.
×
398
        newlyFoundCommit := { true }.
×
399
        [ newlyFoundCommit isNotEmpty ] whileTrue: [
×
400
                | results |
×
401
                page := page + 1.
×
402
                ('import commit page ' , page printString) recordInfo.
×
403
                results := self glhApi
×
404
                                   commitsOfProject: aProject id
×
405
                                   forRefName: nil
×
406
                                   since:
×
407
                                   (fromDate ifNotNil: [ fromDate asDateAndTime asString ])
×
408
                                   until:
×
409
                                   (toDate ifNotNil: [ toDate asDateAndTime asString ])
×
410
                                   path: nil
×
411
                                   author: nil
×
412
                                   all: true
×
413
                                   with_stats: true
×
414
                                   firstParent: nil
×
415
                                   order: nil
×
416
                                   trailers: nil
×
417
                                   perPage: 100
×
418
                                   page: page.
×
419

×
420
                newlyFoundCommit := self parseCommitsResult: results.
×
421
                "newlyFoundCommit do: [ :c | c repository: aProject repository ]."
×
422

×
423
                foundCommit addAll: (aProject repository commits
×
424
                        addAll: newlyFoundCommit
×
425
                        unless: self blockOnIdEquality). ].
×
426

×
427

×
428
        ^ self glhModel addAll: foundCommit unless: self blockOnIdEquality
×
429
]
×
430

431
{ #category : #commit }
432
GLHModelImporter >> importCommitsOfBranch: aGLHBranch forRefName: refName since: fromDate [
×
433

×
434
        ^ self
×
435
                  importCommitsOfBranch: aGLHBranch
×
436
                  forRefName: aGLHBranch name
×
437
                  since: fromDate
×
438
                  until: nil
×
439
]
×
440

441
{ #category : #commit }
442
GLHModelImporter >> importCommitsOfBranch: aGLHBranch forRefName: refName since: fromDate until: toDate [
×
443

×
444
        | newlyFoundCommit page foundCommit|
×
445
        page := 0.
×
446
        foundCommit := OrderedCollection new. 
×
447
        newlyFoundCommit := { true }.
×
448
        [ newlyFoundCommit isNotEmpty ] whileTrue: [
×
449
                | results |
×
450
                page := page + 1.
×
451
                ('import commit page ' , page printString) recordInfo.
×
452
                results := self glhApi
×
453
                                   commitsOfProject: aGLHBranch repository project id
×
454
                                   forRefName: aGLHBranch name
×
455
                                   since:
×
456
                                   (fromDate ifNotNil: [ fromDate asDateAndTime asString ])
×
457
                                   until:
×
458
                                   (toDate ifNotNil: [ toDate asDateAndTime asString ])
×
459
                                   path: nil
×
460
                                   author: nil
×
461
                                   all: nil
×
462
                                   with_stats: nil
×
463
                                   firstParent: nil
×
464
                                   order: nil
×
465
                                   trailers: nil
×
466
                                   perPage: 100
×
467
                                   page: page.
×
468

×
469
                newlyFoundCommit := self parseCommitsResult: results.
×
470
        aGLHBranch commits
×
471
                        addAll: newlyFoundCommit
×
472
                        unless: self blockOnIdEquality.
×
473
        foundCommit addAll: newlyFoundCommit.  
×
474
                        ].
×
475

×
476
        self glhModel
×
477
                addAll: aGLHBranch commits
×
478
                unless: self blockOnIdEquality.
×
479

×
480
        "self withCommitDiffs ifTrue: [
×
481
                aGLHBranch commits: [ :commit | self importDiffOfCommit: commit ] ]."
×
482

×
483
        ^ foundCommit
×
484
]
×
485

486
{ #category : #commit }
487
GLHModelImporter >> importCommitsOfBranch: aGLHBranch forRefName: refName until: toDate [
×
488

×
489
        ^ self
×
490
                  importCommitsOfBranch: aGLHBranch
×
491
                  forRefName: aGLHBranch name
×
492
                  since: nil
×
493
                  until: toDate
×
494
]
×
495

496
{ #category : #'as yet unclassified' }
497
GLHModelImporter >> importContributedProjectsOfUser: aGLHUser [
×
498

×
NEW
499
        | newlyFoundElmts page foundElmts remaningProjects|
×
500
        page := 0.
×
501
        foundElmts := OrderedCollection new.
×
502
        newlyFoundElmts := { true }.
×
503
        [ newlyFoundElmts isNotEmpty ] whileTrue: [
×
504
                | results |
×
505
                page := page + 1.
×
506
                ('import contributed project of user ' , aGLHUser name , ' page '
×
507
                 , page printString) recordInfo.
×
508
                results := self glhApi 
×
509
                                   contributedProjectsOfUserId: aGLHUser id
×
510
                                   orderBy: 'last_activity_at'
×
511
                                   simple: true
×
512
                                   sort: 'desc'
×
513
                                   perPage: 100
×
514
                                   page: page.
×
515

×
516
                newlyFoundElmts := self parseArrayOfProject: results.
×
517

×
518
                foundElmts addAll:
×
519
                        (self glhModel
×
520
                                 addAll: newlyFoundElmts
×
521
                                 unless: self blockOnIdEquality) ].
×
NEW
522
        
×
NEW
523
        
×
NEW
524
        remaningProjects := self importProjects: ((foundElmts collect: #id) difference: ((self userCatalogue atId: aGLHUser id) at: #contributedProjects)).
×
525

×
526
        aGLHUser contributedProjects
×
NEW
527
                addAll: (foundElmts, remaningProjects)
×
528
                unless: self blockOnIdEquality.
×
NEW
529
                
×
NEW
530
        self userCatalogue addUser: aGLHUser withProjects: (aGLHUser contributedProjects collect: #id).
×
531

×
532
        ^ foundElmts
×
533
]
×
534

535
{ #category : #'as yet unclassified' }
536
GLHModelImporter >> importCreatorOfCommit: aCommit [
×
537

×
538
        aCommit commitCreator ifNil: [
×
539
                                
×
540
                aCommit commitCreator:
×
541
                
×
542
                        (self importUserByUsername: aCommit author_name) ]
×
543
]
×
544

545
{ #category : #api }
546
GLHModelImporter >> importDiffOfCommit: aCommit [
1✔
547

1✔
548
        | result diffsResult |
1✔
549
        aCommit diffs ifNotEmpty: [
1✔
550
                'Diff already importer: ' , aCommit short_id printString recordInfo.
1✔
551
                ^ aCommit diffs ].
1✔
552
        ('Import diff of commit: ' , aCommit short_id printString) recordInfo.
1✔
553
        result := self glhApi
1✔
554
                          commitDiff: aCommit id
1✔
555
                          ofProject: aCommit repository project id
1✔
556
                          unidiff: true.
1✔
557
        diffsResult := self newParseDiffResult: result.
1✔
558

1✔
559
        ^ aCommit diffs addAll: diffsResult unless: self blockForDiffEquality.
1✔
560

1✔
561
]
1✔
562

563
{ #category : #'private - api' }
564
GLHModelImporter >> importDirectoryFiles: aDirectoryFile OfBranch: aBranch [
×
565

×
566
        | result files apiFiles |
×
567
        result := self glhApi
×
568
                          treeOfRepository: aBranch repository project id
×
569
                          ofBranch: aBranch name
×
570
                          andPath: aDirectoryFile path , '/'.
×
571
        apiFiles := self parseFileTreeResult: result.
×
572
        files := apiFiles collect: [ :apiFile |
×
573
                         self convertApiFileAsFile: apiFile ].
×
574
        files do: [ :file |
×
575
                
×
576
                self glhModel add: file.
×
577
                aDirectoryFile addFile: file ].
×
578
        files
×
579
                select: [ :file | file isKindOf: GLHFileDirectory ]
×
580
                thenCollect: [ :file |
×
581
                self importDirectoryFiles: file OfBranch: aBranch ]
×
582
]
×
583

584
{ #category : #'private - api' }
585
GLHModelImporter >> importFilesOfBranch: aBranch [
×
586

×
587
        | result files apiFiles |
×
588
        result := self glhApi
×
589
                          treeOfRepository: aBranch repository project id
×
590
                          ofBranch: aBranch name
×
591
                          andPath: nil.
×
592
        apiFiles := self parseFileTreeResult: result.
×
593
        files := apiFiles collect: [ :apiFile | 
×
594
                         self convertApiFileAsFile: apiFile ].
×
595
        files do: [ :file | 
×
596
                self glhModel add: file.
×
597
                aBranch addFile: file ].
×
598
        files
×
599
                select: [ :file | file isKindOf: GLHFileDirectory ]
×
600
                thenCollect: [ :file | 
×
601
                self importDirectoryFiles: file OfBranch: aBranch ]
×
602
]
×
603

604
{ #category : #api }
605
GLHModelImporter >> importGroup: aGroupID [
1✔
606

1✔
607
        | result groupResult |
1✔
608
        ('Import group: ' , aGroupID printString) recordInfo.
1✔
609

1✔
610
        result := self glhApi group: aGroupID.
1✔
611
        groupResult := self parseGroupResult: result.
1✔
612
        groupResult := self addGroupResultToModel: groupResult.
1✔
613

1✔
614
        groupResult projects do: [ :project |
1✔
615
                self completeImportProject: project ].
1✔
616

1✔
617
        (self subGroupsOf: aGroupID) do: [ :subGroup |
1✔
618
                
1✔
619
                groupResult subGroups
1✔
620
                        add: (self importGroup: subGroup id)
1✔
621
                        unless: self blockOnIdEquality ].
1✔
622
        ^ groupResult
1✔
623
]
1✔
624

625
{ #category : #api }
626
GLHModelImporter >> importJobsOf: aPipeline [
×
627

×
628
        | result jobs |
×
629
        result := self glhApi
×
630
                          jobsOfProject: aPipeline project id
×
631
                          ofPipelines: aPipeline id.
×
632
        jobs := self parseJobsResult: result ofProject: aPipeline project.
×
633
        jobs do: [ :job | aPipeline addJob: job ].
×
634
        self glhModel addAll: jobs
×
635

×
636
]
×
637

638
{ #category : #'private - api' }
639
GLHModelImporter >> importLastestCommitsOfProject: aGLHProject [
1✔
640
        "limited to the last 50 commits"
1✔
641

1✔
642
        | results parsedResults |
1✔
643
        results := self glhApi
1✔
644
                           commitsOfProject: aGLHProject id
1✔
645
                           forRefName: nil
1✔
646
                           since: nil
1✔
647
                           until: nil
1✔
648
                           path: nil
1✔
649
                           author: nil
1✔
650
                           all: nil
1✔
651
                           with_stats: true
1✔
652
                           firstParent: nil
1✔
653
                           order: nil
1✔
654
                           trailers: nil
1✔
655
                           perPage: 50
1✔
656
                           page: nil.
1✔
657
        parsedResults := self parseCommitsResult: results.
1✔
658
        parsedResults := self glhModel addAll: parsedResults unless: self blockOnIdEquality.
1✔
659

1✔
660
        aGLHProject repository commits addAll: parsedResults unless: self blockOnIdEquality.
1✔
661
        "parsedResults do: [ :commit |
1✔
662
                commit repository: aGLHProject repository ]."
1✔
663

1✔
664
        self withCommitDiffs ifTrue: [
1✔
665
                parsedResults do: [ :commit | self importDiffOfCommit: commit ] ].
1✔
666

1✔
667
        ^ parsedResults
1✔
668
]
1✔
669

670
{ #category : #commit }
671
GLHModelImporter >> importParentCommitsOfCommit: aGLHCommit since: aDate [
×
672

×
673
        | parentsIds commits |
×
674
        commits := OrderedCollection new.
×
675
        aGLHCommit created_at asDateAndTime < aDate asDateAndTime ifTrue: [
×
676
                 
×
677
                ^ commits
×
678
                          add: aGLHCommit;
×
679
                          yourself ].
×
680

×
681
        parentsIds := aGLHCommit parent_ids.
×
682

×
683
        commits addAll: (parentsIds collect: [ :id |
×
684
                         self
×
685
                                 importCommitOfProject: aGLHCommit repository project
×
686
                                 withId: id ]).
×
687

×
688

×
689
        ^ (commits collect: [ :parentCommit |
×
690
                   self importParentCommitsOfCommit: parentCommit since: aDate ])
×
691
                  flatten
×
692
]
×
693

694
{ #category : #'private - api' }
695
GLHModelImporter >> importPipelinesOfProject: aGLHProject [
1✔
696

1✔
697
        (self pipelinesOf: aGLHProject id) do: [ :pipeline |
1✔
698
                self glhModel add: pipeline unless: self blockOnIdEquality .
1✔
699
                aGLHProject pipelines add: pipeline unless: self blockOnIdEquality]
1✔
700
]
1✔
701

702
{ #category : #projects }
703
GLHModelImporter >> importProject: aProjectID [
×
704

×
705
        | result projectResult |
×
706
        ('Import project with id: ' , aProjectID printString) recordInfo.
×
707

×
708
        result := self glhApi project: aProjectID.
×
709
        projectResult := self parseProjectResult: result.
×
710

×
711
        ^ self completeImportProject: projectResult
×
712
]
×
713

714
{ #category : #imports }
715
GLHModelImporter >> importProjects [
×
716

×
717
        ^ self importProjectsSince: nil
×
718
]
×
719

720
{ #category : #projects }
721
GLHModelImporter >> importProjects: aCollectionOfProjectID [
×
722

×
723
        ^ aCollectionOfProjectID collect: [ :id | self importProject: id ]
×
724
]
×
725

726
{ #category : #imports }
727
GLHModelImporter >> importProjectsSince: since [
×
728
        "heavy import of all projects"
×
729

×
730
        "copy import from commits"
×
731

×
732
        | newlyFoundProjects page foundProject amount |
×
733
        ('import all Projects since: ' , since printString) recordInfo.
×
734

×
735
        "number of projects per page"
×
736
        amount := 100.
×
737
        page := 0.
×
738
        foundProject := OrderedCollection new.
×
739
        newlyFoundProjects := { true }.
×
740
        [ newlyFoundProjects isNotEmpty ] whileTrue: [
×
741
                | results |
×
742
                page := page + 1.
×
743
                ('import projects page #' , page printString) recordInfo.
×
744

×
745
                results := self glhApi projects: amount since: since page: page.
×
746

×
747
                newlyFoundProjects := self glhModel
×
748
                                              addAll: (self parseArrayOfProject: results)
×
749
                                              unless: self blockOnIdEquality.
×
750
                foundProject addAll: newlyFoundProjects ].
×
751

×
752
        ^ foundProject
×
753
]
×
754

755
{ #category : #'private - api' }
756
GLHModelImporter >> importRepository: aGLHRepository [
1✔
757

1✔
758
        | resultBranches branches |
1✔
759
        [
1✔
760
        ('import the repository of project ' , aGLHRepository project name)
1✔
761
                recordInfo.
1✔
762

1✔
763
        resultBranches := self glhApi branchesOfRepository:
1✔
764
                                  aGLHRepository project id.
1✔
765
        branches := self parseBranchesResult: resultBranches.
1✔
766

1✔
767
        'import the branches of project ' recordInfo.
1✔
768

1✔
769
        branches := aGLHRepository branches
1✔
770
                            addAll: branches
1✔
771
                            unless: self blockOnNameEquality.
1✔
772
        branches := self glhModel
1✔
773
                            addAll: branches
1✔
774
                            unless: self blockOnNameEquality.
1✔
775

1✔
776

1✔
777
        self withFiles ifTrue: [
1✔
778
                branches do: [ :branch | self importFilesOfBranch: branch ] ] ]
1✔
779
                on: NeoJSONParseError
1✔
780
                do: [
1✔
781
                self inform: aGLHRepository project name , ' has no repository' ]
1✔
782
]
1✔
783

784
{ #category : #'private - api' }
785
GLHModelImporter >> importUser: aUserID [
1✔
786

1✔
787
        | result userResult |
1✔
788
        (self glhModel allWithType: GLHUser)
1✔
789
                detect: [ :user | user id = aUserID ]
1✔
790
                ifFound: [ :user | ^ user ].
1✔
791
        ('Import user: ' , aUserID printString) recordInfo.
1✔
792
        result := self glhApi user: aUserID.
1✔
793
        userResult := self parseUserResult: result.
1✔
794
        ^ self glhModel add: userResult unless: self blockOnIdEquality
1✔
795
]
1✔
796

797
{ #category : #user }
798
GLHModelImporter >> importUserByUsername: anUsername [
×
799

×
800
        | dicUsername resultUser |
×
801
        dicUsername := ((self glhModel allWithType: GLHUser) collect: [ :user |
×
802
                                user username -> user ]) asSet asDictionary.
×
803

×
804
        dicUsername addAll: self userCatalogue collectUsernames.
×
805

×
806

×
807
        resultUser := dicUsername
×
808
                              at: anUsername
×
809
                              ifAbsent: [ "thus we have to import this new user"
×
810
                                      | result userId searchResult |
×
811
                                      ('Import user with username: '
×
812
                                       , anUsername printString) recordInfo.
×
813
                                      result := self glhApi usersSearchByUsername:
×
814
                                                        anUsername.
×
815
                                      searchResult := NeoJSONReader fromString: result.
×
816

×
817
                                      (searchResult class = Dictionary and: [
×
818
                                               (searchResult at: #message) includesSubstring:
×
819
                                                       '403 Forbidden' ])
×
820
                                              ifTrue: [ "if the result is an 403 error we fake a new user"
×
821
                                                      self glhModel
×
822
                                                              add: (GLHUser new
×
823
                                                                               username: anUsername;
×
824
                                                                               name: anUsername;
×
825
                                                                               yourself)
×
826
                                                              unless: [ :nu :ou | nu username = ou username ] ]
×
827
                                              ifFalse: [
×
828
                                                      searchResult
×
829
                                                              ifEmpty: [ "results can be empty thus we force a new user with the info we have "
×
830
                                                                      self glhModel
×
831
                                                                              add: (GLHUser new
×
832
                                                                                               username: anUsername;
×
833
                                                                                               name: anUsername;
×
834
                                                                                               yourself)
×
835
                                                                              unless: [ :nu :ou | nu username = ou username ] ]
×
836
                                                              ifNotEmpty: [ "because we may already have the researched user, we look by ID in the model"
×
837
                                                                      userId := searchResult first at: #id.
×
838
                                                                      (self glhModel allWithType: GLHUser)
×
839
                                                                              detect: [ :user | user id = userId ]
×
840
                                                                              ifNone: [ self importUser: userId ] ] ] ].
×
841

×
842
        self userCatalogue addUser: resultUser withName: anUsername.
×
843

×
844
        ^ resultUser
×
845
]
×
846

847
{ #category : #initialization }
848
GLHModelImporter >> initReader [
1✔
849

1✔
850
        generalReader := NeoJSONReader new.
1✔
851
        self configureReaderForCommit: generalReader.
1✔
852
        self configureReaderForGroup: generalReader.
1✔
853
        self configureReaderForDiffs: generalReader. 
1✔
854
]
1✔
855

856
{ #category : #initialization }
857
GLHModelImporter >> initialize [
1✔
858

1✔
859
        withFiles := false.
1✔
860
        withCommitDiffs := false.
1✔
861
        withInitialCommits := false.
1✔
862
        withInitialMergeRequest := false.
1✔
863
        withCommitsSince := (Date today - 1 week) asDateAndTime.
1✔
864
        userCatalogue := GLHUserCatalogueV2 new
1✔
865
                                 anImporter: self;
1✔
866
                                 yourself.
1✔
867
        self initReader
1✔
868
]
1✔
869

870
{ #category : #importer }
871
GLHModelImporter >> loadAllProjectsFromRepositorySoftware [
×
872
        "heavy import that load all the active project inside the model. Only import the project entities"
×
873
        |projects|
×
874
        
×
875
        projects := self glhApi projects. 
×
876
]
×
877

878
{ #category : #'as yet unclassified' }
879
GLHModelImporter >> makeGlobal [
×
880
        currentImporter := self
×
881
]
×
882

883
{ #category : #private }
884
GLHModelImporter >> newParseCommitResult: result [
×
885

×
886
        generalReader  on: result readStream.
×
887

×
888
        ^ generalReader nextAs: GLHCommit
×
889
]
×
890

891
{ #category : #private }
892
GLHModelImporter >> newParseDiffResult: result [
1✔
893

1✔
894
        generalReader on: result readStream.
1✔
895
        ^ generalReader nextAs: #ArrayOfDiffs
1✔
896
]
1✔
897

898
{ #category : #parsing }
899
GLHModelImporter >> parseArrayOfProject: arrayOfProjects [
×
900

×
901
        | reader |
×
902
        reader := NeoJSONReader on: arrayOfProjects readStream.
×
903
        reader
×
904
                for: #ArrayOfProjects
×
905
                customDo: [ :customMappting |
×
906
                customMappting listOfElementSchema: GLHProject ].
×
907
        reader for: GLHProject do: [ :mapping |
×
908
                mapping mapInstVar: #name to: #name.
×
909
                mapping mapInstVar: #description to: #description.
×
910
                mapping mapInstVar: #id to: #id.
×
911
                mapping mapInstVar: #archived to: #archived.
×
912
                mapping mapInstVar: #web_url to: #html_url.
×
913
                mapping mapInstVar: #topics to: #topics ].
×
914
        ^ reader nextAs: #ArrayOfProjects
×
915
]
×
916

917
{ #category : #private }
918
GLHModelImporter >> parseBranchesResult: result [
1✔
919

1✔
920
        | reader |
1✔
921
        reader := NeoJSONReader on: result readStream.
1✔
922
        reader mapInstVarsFor: GLHBranch.
1✔
923
        reader
1✔
924
                for: #ArrayOfBranch
1✔
925
                customDo: [ :customMappting | 
1✔
926
                customMappting listOfElementSchema: GLHBranch ].
1✔
927
        ^ reader nextAs: #ArrayOfBranch
1✔
928
]
1✔
929

930
{ #category : #private }
931
GLHModelImporter >> parseCommitResult: result [
×
932

×
933
        | reader |
×
934
        reader := NeoJSONReader on: result readStream.
×
935

×
936
        reader for: GLHCommit do: [ :mapping |
×
937
                mapping mapInstVars:
×
938
                        #( id short_id title author_name author_email committer_name
×
939
                           committer_email message web_url ).
×
940
                (mapping mapInstVar: #authored_date) valueSchema: DateAndTime.
×
941
                (mapping mapInstVar: #committed_date) valueSchema: DateAndTime.
×
942
                (mapping mapInstVar: #created_at) valueSchema: DateAndTime.
×
943
                (mapping mapInstVar: #parent_ids) valueSchema: #ArrayOfIds.
×
944
                mapping
×
945
                        mapProperty: 'stats'
×
946
                        getter: [ :el | "Not used" ]
×
947
                        setter: [ :commit :value |
×
948
                                commit deletions: (value at: #deletions).
×
949
                                commit additions: (value at: #additions) ] ].
×
950

×
951
        reader for: DateAndTime customDo: [ :mapping |
×
952
                mapping decoder: [ :string | DateAndTime fromString: string ] ].
×
953

×
954
        reader
×
955
                for: #ArrayOfIds
×
956
                customDo: [ :mapping | mapping decoder: [ :string | string ] ].
×
957

×
958

×
959
        ^ reader nextAs: GLHCommit
×
960
]
×
961

962
{ #category : #private }
963
GLHModelImporter >> parseCommitsResult: result [
1✔
964

1✔
965
        | reader |
1✔
966
        reader := NeoJSONReader on: result readStream.
1✔
967

1✔
968
          reader for: GLHCommit do: [ :mapping |
1✔
969
                mapping mapInstVars:
1✔
970
                        #( id short_id title author_name author_email committer_name
1✔
971
                           committer_email message web_url ).
1✔
972
                (mapping mapInstVar: #authored_date) valueSchema: DateAndTime.
1✔
973
                (mapping mapInstVar: #committed_date) valueSchema: DateAndTime.
1✔
974
                (mapping mapInstVar: #created_at) valueSchema: DateAndTime.
1✔
975
                (mapping mapInstVar: #parent_ids) valueSchema: #ArrayOfIds.
1✔
976
                mapping
1✔
977
                        mapProperty: 'stats'
1✔
978
                        getter: [ :el | "Not used" ]
1✔
979
                        setter: [ :commit :value |
1✔
980
                                commit deletions: (value at: #deletions).
1✔
981
                                commit additions: (value at: #additions) ] ].
1✔
982

1✔
983
        reader for: DateAndTime customDo: [ :mapping |
1✔
984
                mapping decoder: [ :string | DateAndTime fromString: string ] ].
1✔
985

1✔
986
        reader
1✔
987
                for: #ArrayOfIds
1✔
988
                customDo: [ :mapping | mapping decoder: [ :string | string ] ].
1✔
989
  
1✔
990
        reader
1✔
991
                for: #ArrayOfCommit
1✔
992
                customDo: [ :customMappting |
1✔
993
                customMappting listOfElementSchema: GLHCommit ].
1✔
994

1✔
995
        ^ reader nextAs: #ArrayOfCommit
1✔
996
]
1✔
997

998
{ #category : #private }
999
GLHModelImporter >> parseFileTreeResult: aResult [
×
1000

×
1001
        | reader |
×
1002
        reader := NeoJSONReader on: aResult readStream.
×
1003
        reader mapInstVarsFor: GLHApiFile.
×
1004
        reader
×
1005
                for: #ArrayOfFile
×
1006
                customDo: [ :customMappting | 
×
1007
                customMappting listOfElementSchema: GLHApiFile ].
×
1008
        ^ reader nextAs: #ArrayOfFile
×
1009
]
×
1010

1011
{ #category : #private }
1012
GLHModelImporter >> parseGroupResult: aResult [
1✔
1013

1✔
1014
        | reader |
1✔
1015

1✔
1016
        reader := NeoJSONReader on: aResult readStream.
1✔
1017
        reader for: GLHGroup do: [ :mapping |
1✔
1018
                mapping mapInstVars.
1✔
1019
                (mapping mapInstVar: #projects) valueSchema: #ArrayOfProjects ].
1✔
1020
        reader mapInstVarsFor: GLHProject.
1✔
1021
        reader
1✔
1022
                for: #ArrayOfProjects
1✔
1023
                customDo: [ :customMappting |
1✔
1024
                customMappting listOfElementSchema: GLHProject ].
1✔
1025
        ^ reader nextAs: GLHGroup
1✔
1026
]
1✔
1027

1028
{ #category : #private }
1029
GLHModelImporter >> parseJobsResult: result ofProject: aProject [
×
1030

×
1031
        | reader |
×
1032
        reader := NeoJSONReader on: result readStream.
×
1033
        reader for: GLHJob do: [ :mapping |
×
1034
                mapping mapInstVars: #( id allow_failure web_url name ).
×
1035

×
1036
                mapping
×
1037
                        mapProperty: #user
×
1038
                        getter: [ :object | #ignore ]
×
1039
                        setter: [ :object :value |
×
1040
                        object user: (self importUser: (value at: #id)) ].
×
1041

×
1042
                mapping
×
1043
                        mapProperty: #commit
×
1044
                        getter: [ :object | #ignore ]
×
1045
                        setter: [ :object :value |
×
1046
                                value ifNotNil: [
×
1047
                                        object commit:
×
1048
                                                (self importCommit: (value at: #id) ofProject: aProject) ] ].
×
1049

×
1050
                mapping
×
1051
                        mapProperty: #duration
×
1052
                        getter: [ :object | #ignore ]
×
1053
                        setter: [ :object :value |
×
1054
                        value ifNotNil: [ object duration: value seconds ] ] ].
×
1055

×
1056
        reader
×
1057
                for: #ArrayOfGLHJob
×
1058
                customDo: [ :customMappting |
×
1059
                customMappting listOfElementSchema: GLHJob ].
×
1060
        ^ reader nextAs: #ArrayOfGLHJob
×
1061
]
×
1062

1063
{ #category : #private }
1064
GLHModelImporter >> parsePipelinesResult: result [
1✔
1065

1✔
1066
        | reader |
1✔
1067
        
1✔
1068
        (result includesSubstring: '{"message":"40' )ifTrue: [ ^ {  } ].
1✔
1069
        
1✔
1070
        reader := NeoJSONReader on: result readStream.
1✔
1071
        reader mapInstVarsFor: GLHPipeline.
1✔
1072
        reader for: GLHPipeline do: [ :mapping |
1✔
1073
                mapping
1✔
1074
                        mapProperty: #created_at
1✔
1075
                        getter: [ :object | #ignore ]
1✔
1076
                        setter: [ :object :value |
1✔
1077
                        object runDate: (DateAndTime fromString: value) ] ].
1✔
1078
        reader
1✔
1079
                for: #ArrayOfPipelines
1✔
1080
                customDo: [ :customMappting |
1✔
1081
                customMappting listOfElementSchema: GLHPipeline ].
1✔
1082
        ^ reader nextAs: #ArrayOfPipelines
1✔
1083
]
1✔
1084

1085
{ #category : #parsing }
1086
GLHModelImporter >> parseProjectResult: aResult [ 
×
1087
                | reader |
×
1088
        reader := NeoJSONReader on: aResult readStream.
×
1089
        reader for: GLHProject do: [ :mapping |
×
1090
                mapping mapInstVars. ].
×
1091
"        reader mapInstVarsFor: GLHProject."
×
1092

×
1093
        ^ reader nextAs: GLHProject
×
1094
]
×
1095

×
1096
{ #category : #private }
×
1097
GLHModelImporter >> parseSubGroupResult: aResult [
×
1098

×
1099
        | reader |
×
1100
        reader := NeoJSONReader on: aResult readStream.
×
1101
        self configureReaderForGroup: reader.
×
1102
        ^ reader nextAs: #ArrayOfGroups
×
1103
]
×
1104

×
1105
{ #category : #private }
×
1106
GLHModelImporter >> parseUserResult: result [
×
1107

×
1108
        | reader |
×
1109
        reader := NeoJSONReader on: result readStream.
×
1110
        reader mapInstVarsFor: GLHUser.
×
1111
        ^ reader nextAs: GLHUser
×
1112
]
×
1113

×
1114
{ #category : #private }
×
1115
GLHModelImporter >> parseUsersResult: result [
×
1116

×
1117
        | reader |
×
1118
        reader := NeoJSONReader on: result readStream.
×
1119

×
1120
        reader mapInstVarsFor: GLHUser.
×
1121

×
1122
        reader
×
1123
                for: #ArrayOfUser
×
1124
                customDo: [ :customMappting |
×
1125
                customMappting listOfElementSchema: GLHUser ].
×
1126

×
1127
        ^ reader nextAs: #ArrayOfUser
×
1128
]
×
1129

×
1130
{ #category : #'private - api' }
×
1131
GLHModelImporter >> pipelinesOf: aProjectID [
×
1132

×
1133
        | result |
×
1134
        ('Search pipelines of: ' , aProjectID printString) recordInfo.
×
1135
        result := self glhApi pipelinesOfProject: aProjectID.
×
1136
        ^ self parsePipelinesResult: result .
×
1137
]
×
1138

×
1139
{ #category : #'private - api' }
×
1140
GLHModelImporter >> subGroupsOf: aGroupID [
×
1141

×
1142
        | results parsedResult result page |
×
1143
        ('Search subgroup of: ' , aGroupID printString) recordInfo.
×
1144
        results := OrderedCollection new.
×
1145
        page := 0.
×
1146
        
×
1147
        parsedResult := { true }.
×
1148
        [ parsedResult size > 0 ] whileTrue: [ 
×
1149
                result := self glhApi subgroupsOfGroup: aGroupID page: page.
×
1150
                parsedResult := self parseSubGroupResult: result.
×
1151
                results addAll: parsedResult.
×
1152
                                page := page + 1. ].
×
1153
        
×
1154
        ^ results
×
1155
]
×
1156

×
1157
{ #category : #accessing }
×
1158
GLHModelImporter >> userCatalogue [
×
1159
        ^ userCatalogue 
×
1160
]
×
1161

×
1162
{ #category : #accessing }
×
1163
GLHModelImporter >> userCatalogue: aGLHUserCatalogue [
×
1164

×
1165
        userCatalogue := aGLHUserCatalogue.
×
1166
        aGLHUserCatalogue anImporter: self. 
×
1167
]
×
1168

×
1169
{ #category : #accessing }
×
1170
GLHModelImporter >> withCommitDiffs [
×
1171

×
1172
        ^ withCommitDiffs
×
1173
]
×
1174

×
1175
{ #category : #accessing }
×
1176
GLHModelImporter >> withCommitDiffs: anObject [
×
1177

×
1178
        withCommitDiffs := anObject
×
1179
]
×
1180

×
1181
{ #category : #accessing }
×
1182
GLHModelImporter >> withFiles [
×
1183
        ^ withFiles
×
1184
]
×
1185

×
1186
{ #category : #accessing }
×
1187
GLHModelImporter >> withFiles: aBoolean [
×
1188
        withFiles := aBoolean
×
1189
]
×
1190

×
1191
{ #category : #accessing }
×
1192
GLHModelImporter >> withInitialCommits: boolean [
×
1193
        withInitialCommits := boolean 
×
1194
]
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc