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

moosetechnology / GitProjectHealth / 13807399403

12 Mar 2025 08:58AM UTC coverage: 66.731% (+0.2%) from 66.508%
13807399403

Pull #145

github

web-flow
Merge 41a250ee3 into 2c2d8c5e1
Pull Request #145: Code addition merge rerquest project metric

6833 of 8097 new or added lines in 90 files covered. (84.39%)

1 existing line in 1 file now uncovered.

11746 of 17602 relevant lines covered (66.73%)

0.67 hits per line

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

93.08
/src/GitLabHealth-Model-Analysis/GitAnalyzer.class.st
1
Class {
2
        #name : #GitAnalyzer,
3
        #superclass : #Object,
4
        #instVars : [
5
                'glModel',
6
                'fromCommit',
7
                'glhImporter',
8
                'onProject',
9
                'maxChildCommits'
10
        ],
11
        #category : #'GitLabHealth-Model-Analysis'
12
}
13

14
{ #category : #analyze }
15
GitAnalyzer >> analyseChurn [
1✔
16

1✔
17
        | commitFiles totalContribution childCommits access |
1✔
18
        access := ('' join: {
1✔
19
                                   'churn'.
1✔
20
                                   maxChildCommits }) asSymbol.
1✔
21

1✔
22
        ^ fromCommit cacheAt: access ifAbsentPut: [
1✔
23
                  ('GitAnalyzer, analyse chrun onProject: ' , onProject printString)
1✔
24
                          recordInfo.
1✔
25
                  childCommits := Set new.
1✔
26
                  totalContribution := self
1✔
27
                                               visitChildCommits: fromCommit childCommits
1✔
28
                                               toStoreThemIn: childCommits
1✔
29
                                               upto: self maxChildCommits.
1✔
30
                  totalContribution add: fromCommit. 
1✔
31
                  totalContribution := totalContribution sum: [ :commit | "nil if merge request commit"
1✔
32
                                               commit additions ifNil: [ 0 ] ].
1✔
33
                  commitFiles := self
1✔
34
                                         impactedFilesInFollowUpCommitsOf: fromCommit
1✔
35
                                         withMaxCommits: self maxChildCommits.
1✔
36
                        "a churned line is a line added on top of an already added line"
1✔
37
                  (self computeChurnOnFiles: commitFiles)
1✔
38
                          at: #totalContribution ifAbsentPut: totalContribution;
1✔
39
                          yourself ]
1✔
40
]
1✔
41

42
{ #category : #analyze }
43
GitAnalyzer >> analyseCommentContribution [
1✔
44

1✔
45
        | numberOfComments |
1✔
46
        ('GitAnalyzer, analyse comment contributions onProject: '
1✔
47
         , onProject printString) recordInfo.
1✔
48
        numberOfComments := 0.
1✔
49
        
1✔
50
        fromCommit diffs do: [ :diff |
1✔
51
                diff diffRanges do: [ :range |
1✔
52
                        range changes do: [ :change |
1✔
53
                                ((change isKindOf: GLHAddition) and: [
1✔
54
                                         (change sourceCode withoutPrefix: '+') trimLeft
1✔
55
                                                 beginsWithAnyOf: { '#'. '//'. '/*'. '*'. '*/' } ]) ifTrue: [
1✔
56
                                        numberOfComments := numberOfComments + 1 ] ] ] ].
1✔
57
        ^ numberOfComments
1✔
58
]
1✔
59

60
{ #category : #commit }
61
GitAnalyzer >> analyseCommitContribution [
1✔
62
        
1✔
63
        
1✔
64
        ('GitAnalyzer, analyse commit contribution of: ', fromCommit printString )
1✔
65
                recordInfo.
1✔
66
        
1✔
67
        ^ { (#addition -> fromCommit additions).
1✔
68
          (#deletion -> fromCommit deletions). } asDictionary 
1✔
69
]
1✔
70

71
{ #category : #analyze }
NEW
72
GitAnalyzer >> analyseCommitFrequencyFromCommits: initialCommits [
×
NEW
73

×
NEW
74
        | commits response |
×
NEW
75
        ('GitAnalyzer, analyse commit Frequency on: ' , onProject printString)
×
NEW
76
                recordInfo.
×
NEW
77

×
NEW
78
        response := {
×
NEW
79
                            (#numberOfCommit -> nil).
×
NEW
80
                            (#frequency -> nil) } asDictionary.
×
NEW
81

×
NEW
82

×
NEW
83
        commits := self arrangeCommitsByDate: initialCommits.
×
NEW
84
        commits := (commits associations sortAscending: [ :entry |
×
NEW
85
                            entry key asDate ]) asOrderedDictionary.
×
NEW
86

×
NEW
87
        ^ commits
×
NEW
88
]
×
89

90
{ #category : #analyze }
91
GitAnalyzer >> analyseCommitFrequencySince: since until: until [ 
1✔
92

1✔
93
        | commits response |
1✔
94
        
1✔
95
        ('GitAnalyzer, analyse commit Frequency on: ', onProject printString )
1✔
96
                recordInfo.
1✔
97
        
1✔
98
        response := {
1✔
99
                            (#numberOfCommit -> nil).
1✔
100
                            (#frequency -> nil) } asDictionary.
1✔
101

1✔
102
        commits := glhImporter
1✔
103
                           importCommitsOfProject: onProject
1✔
104
                           since: since
1✔
105
                           until: until.
1✔
106

1✔
107
        commits := self arrangeCommitsByDate: commits.
1✔
108
        commits := (commits associations sortAscending: [ :entry |
1✔
109
                            entry key asDate ]) asOrderedDictionary.
1✔
110

1✔
111
        ^ commits
1✔
112
]
1✔
113

114
{ #category : #analyze }
115
GitAnalyzer >> analyseDelayUntilFirstChurn [
1✔
116
        "return the first commit that modify the same lines of code as the fromCommit"
1✔
117

1✔
118
        | churn res access|
1✔
119
        
1✔
120
        access := ('' join: {
1✔
121
                                   'amandment'.
1✔
122
                                   maxChildCommits }) asSymbol.
1✔
123
        
1✔
124
        ^ fromCommit cacheAt: access ifPresent: [ :v | v ] ifAbsentPut: [
1✔
125
        
1✔
126
        ('GitAnalyzer, analyse amandment onProject: ', onProject printString )
1✔
127
                recordInfo.
1✔
128
        
1✔
129
        churn := self analyseChurn.
1✔
130

1✔
131
        res := self firstAmandmentFromChrun: churn.
1✔
132
         res]
1✔
133
]
1✔
134

135
{ #category : #analyze }
136
GitAnalyzer >> analyseMergeResquestValidation: aGLHPMergeRequest [
1✔
137

1✔
138
        | creationDate mergedDate closedDate currentDate  response |
1✔
139
        ('GitAnalyzer, analyse merge request delay of: '
1✔
140
         , aGLHPMergeRequest printString) recordInfo.
1✔
141

1✔
142
        ^ aGLHPMergeRequest 
1✔
143
                  cacheAt: #mergeRequestValidation
1✔
144
                  ifPresent: [ :v | v ]
1✔
145
                  ifAbsentPut: [
1✔
146
                          response := {
1✔
147
                                              (#id_merge_resquest -> aGLHPMergeRequest iid).
1✔
148
                                              (#id_merge_commit -> nil).
1✔
149
                                              (#created_at -> aGLHPMergeRequest created_at).
1✔
150
                                              (#open_duration -> nil).
1✔
151
                                
1✔
152
                                              (#merged_at -> nil).
1✔
153
                                              (#duration -> nil).
1✔
154
                                
1✔
155
                                              (#closed_at -> nil).
1✔
156
                                              (#close_duration -> nil).
1✔
157
                                              
1✔
158
                                              (#status -> aGLHPMergeRequest merge_status) }
1✔
159
                                              asDictionary.
1✔
160

1✔
161
                          creationDate := aGLHPMergeRequest created_at asDateAndTime.
1✔
162
            currentDate := DateAndTime now.  "Get the current timestamp"
1✔
163
            "Handle merged case"
1✔
164
            mergedDate := aGLHPMergeRequest merged_at ifNil: [ nil ].
1✔
165
            mergedDate ifNotNil: [
1✔
166
                response  
1✔
167
                    at: #duration put: mergedDate - creationDate;  
1✔
168
                    at: #id_merge_commit put: aGLHPMergeRequest merge_commit_sha;  
1✔
169
                    at: #merged_at put: aGLHPMergeRequest merged_at.  
1✔
170
            ].
1✔
171

1✔
172
           "Handle closed case"
1✔
173
            closedDate := aGLHPMergeRequest closed_at ifNil: [ nil ].
1✔
174
            closedDate ifNotNil: [
1✔
175
                response  
1✔
176
                    at: #close_duration put: closedDate - creationDate;  
1✔
177
                    at: #closed_at put: aGLHPMergeRequest closed_at.
1✔
178
            ].
1✔
179

1✔
180
            "Handle open case (niether merged nor closed)"
1✔
181
            (mergedDate isNil and: closedDate isNil) ifTrue: [
1✔
182
                response at: #open_duration put: currentDate - creationDate.
1✔
183
            ].
1✔
184

1✔
185
            response
1✔
186
        ].
1✔
187
]
1✔
188

189
{ #category : #filter }
190
GitAnalyzer >> arrangeCommitsByDate: commits [
1✔
191

1✔
192
        | date2commits |
1✔
193
        date2commits := Dictionary new.
1✔
194

1✔
195
        commits do: [ :commit |
1✔
196
                | date |
1✔
197
                date := commit created_at asDate.
1✔
198
                date2commits
1✔
199
                        at: date printString
1✔
200
                        ifPresent: [ :v | v add: commit ]
1✔
201
                        ifAbsentPut: [
1✔
202
                                OrderedCollection new
1✔
203
                                        add: commit;
1✔
204
                                        yourself ] ].
1✔
205
        ^ date2commits
1✔
206
]
1✔
207

208
{ #category : #churn }
209
GitAnalyzer >> computeChurnOnFiles: aCollection [
1✔
210

1✔
211
        | changesDic perFileChanges churns initialAuthor followingAuthors|
1✔
212
        "1 -> (a GLPHEChange -> NumberOfChurnDetected)"
1✔
213
        changesDic := Dictionary new.
1✔
214
        initialAuthor := fromCommit commitCreator. 
1✔
215
        
1✔
216

1✔
217
        perFileChanges := aCollection associations collect: [ :assoc |
1✔
218
                                  assoc key
1✔
219
                                  -> (self computeSpecificChurnOf: assoc value) ].
1✔
220

1✔
221

1✔
222
        churns := perFileChanges collect: [ :assoc |
1✔
223
                          | churnData file aLineOfChanges |
1✔
224
                          file := assoc key.
1✔
225
                          aLineOfChanges := assoc value.
1✔
226
                          churnData := aLineOfChanges values ifEmpty: [  nil ] ifNotEmpty: [
1✔
227
                                  | churnedContribution churnSpecificFromCommit churnOfAuthorOnly |
1✔
228
                                
1✔
229
                                  "the total churn on any LoC affected"
1✔
230
                                  churnedContribution := aLineOfChanges select: [ :a |
1✔
231
                                                                 a value  > 1 ].
1✔
232

1✔
233
                                  "the churn that coccurs specifically on the loc introduced by the initial commit"
1✔
234
                                  churnSpecificFromCommit := churnedContribution select: [ :a |
1✔
235
                                                                     (a key collect: [ :loc |
1✔
236
                                                                              loc diffRange diff commit ])
1✔
237
                                                                             includes: fromCommit ].
1✔
238

1✔
239
                                                "the churn made the author of the initial commits "
1✔
240
                                  churnOfAuthorOnly := churnSpecificFromCommit select: [ :a |
1✔
241
                                                                     (((a key collect: [ :loc |
1✔
242
                                                                              loc diffRange diff commit commitCreator ]) asSet) difference: {initialAuthor} asSet ) isEmpty
1✔
243
                                                                             ].
1✔
244
                                                
1✔
245

1✔
246
                                  {
1✔
247
                                                   (#churnFromInitialCommitLines
1✔
248
                                                    ->
1✔
249
                                                            ((churnSpecificFromCommit sum: #value) - churnSpecificFromCommit size)).
1✔
250
                                                                                (#churnFromCommitCreatorOnly
1✔
251
                                                    ->
1✔
252
                                                            ((churnOfAuthorOnly sum: #value) - churnOfAuthorOnly size)).
1✔
253
                                                   (#churnLoC
1✔
254
                                                    ->
1✔
255
                                                            ((churnedContribution sum: #value)
1✔
256
                                                             - churnedContribution size)) 
1✔
257
                                        } asDictionary ].
1✔
258

1✔
259
                          file -> churnData ].
1✔
260
        churns := churns reject: [ :file2churn | file2churn value isNil ].
1✔
261

1✔
262
        ^ {
1✔
263
                  (#churns -> churns).
1✔
264
                  (#details -> perFileChanges) } asDictionary
1✔
265
]
1✔
266

267
{ #category : #churn }
268
GitAnalyzer >> computeSpecificChurnOf: commit2Changes [
1✔
269

1✔
270
        | changesDic |
1✔
271
        "1 -> (a GLPHEChange -> NumberOfChurnDetected)"
1✔
272
        changesDic := OrderedDictionary new.
1✔
273

1✔
274

1✔
275
        (commit2Changes sortAscending: [ :assoc | assoc key created_at ])
1✔
276
                do: [ :entry |
1✔
277
                        | commit diffRanges |
1✔
278
                        commit := entry key.
1✔
279
                        diffRanges := entry value.
1✔
280

1✔
281
                        diffRanges do: [ :diff |
1✔
282
                                | from |
1✔
283
                                from := (diff originalLineRange
1✔
284
                                                 copyFrom: (diff originalLineRange indexOf: $-) + 1
1✔
285
                                                 to: (diff originalLineRange
1✔
286
                                                                  indexOf: $,
1✔
287
                                                                  ifAbsent: [ diff originalLineRange size + 1 ]) - 1)
1✔
288
                                                asString asNumber.
1✔
289
                                from = 0 ifTrue: [ from := 1 ].
1✔
290
                                self insertDiff: diff into: changesDic startingFrom: from ] ].
1✔
291

1✔
292

1✔
293

1✔
294
        ^ self sortChangeDic: changesDic
1✔
295
]
1✔
296

297
{ #category : #accessing }
298
GitAnalyzer >> firstAmandmentFromChrun: aChurnAnalysis [ 
1✔
299
        |details whereChangesOccurs firstCommitsPerFile|
1✔
300
        
1✔
301
        whereChangesOccurs := (aChurnAnalysis at: #churns ) select: [ :file | (file value at: #churnLoC) > 0 ].
1✔
302
        
1✔
303
        details := whereChangesOccurs collect: [ :file |
1✔
304
                ((aChurnAnalysis at: #details) detect: [ :entry | entry key = file key] )
1✔
305
                 ].
1✔
306
        
1✔
307
        firstCommitsPerFile := details collect: [ :perFile |
1✔
308
                |changes firstCommits first|
1✔
309
                changes := perFile value.
1✔
310
                changes := changes select: [ :line2changes | line2changes value value > 1  ].
1✔
311
                firstCommits := (changes collect: [ :line2changes |  line2changes key second diffRange diff commit ]) values. 
1✔
312
                first := (firstCommits sortAscending: [:c | c created_at ]) first.
1✔
313
                 ].
1✔
314
        
1✔
315

1✔
316
        ^ (firstCommitsPerFile sortAscending: [:c | c created_at ]) ifEmpty: nil ifNotEmpty: [ :v | v first ]  . 
1✔
317

1✔
318
]
1✔
319

320
{ #category : #accessing }
321
GitAnalyzer >> fromCommit: aCommit [
1✔
322
        fromCommit := aCommit. 
1✔
323
]
1✔
324

325
{ #category : #accessing }
326
GitAnalyzer >> glhImporter: anImporter [
1✔
327
        glhImporter := anImporter .
1✔
328
]
1✔
329

330
{ #category : #'as yet unclassified' }
NEW
331
GitAnalyzer >> impactedFilesInFollowUpCommitsOf: aGLHCommit [
×
NEW
332

×
NEW
333
        ^ self
×
NEW
334
                  impactedFilesInFollowUpCommitsOf: aGLHCommit
×
NEW
335
                  withMaxCommits: self maxChildCommits. 
×
NEW
336
]
×
337

338
{ #category : #churn }
339
GitAnalyzer >> impactedFilesInFollowUpCommitsOf: aGLHCommit withMaxCommits: max [
1✔
340

1✔
341
        | commitFiles |
1✔
342
        commitFiles := (fromCommit diffs collect: [ :diff |
1✔
343
                                diff new_path -> (Set new
1✔
344
                                         add: aGLHCommit -> diff diffRanges;
1✔
345
                                         yourself) ]) asDictionary.
1✔
346

1✔
347
        self
1✔
348
                visitChildCommits: fromCommit childCommits
1✔
349
                lookingForFiles: commitFiles upto: max.
1✔
350

1✔
351
        ^ commitFiles
1✔
352
]
1✔
353

354
{ #category : #initialization }
355
GitAnalyzer >> initialize [
1✔
356

1✔
357
        glModel := GLHModel new.
1✔
358
        fromCommit := GLHCommit new.
1✔
359
        glhImporter := GitlabWithMergeRequestModelImporter new.
1✔
360
        onProject := GLHProject new.
1✔
361
        maxChildCommits := -1
1✔
362
]
1✔
363

364
{ #category : #insertion }
365
GitAnalyzer >> insertDiff: aGLPHEDiffRange into: fileChangesDic startingFrom: from [ 
1✔
366
        |index|
1✔
367
        index := from. 
1✔
368
        aGLPHEDiffRange changes do: [ :aChange |
1✔
369
        
1✔
370
                aChange isAddition ifTrue: [ 
1✔
371
                        fileChangesDic at: index ifPresent: [ :current | 
1✔
372
                         
1✔
373
                        current key add: aChange.
1✔
374
                        current value: current value + 1.  ] ifAbsentPut: [((OrderedCollection new add: aChange; yourself) -> 1 ) ].
1✔
375
                         ].
1✔
376
                
1✔
377
                aChange isDeletion ifFalse: [ index := index + 1 ]. 
1✔
378
                
1✔
379
                 ]
1✔
380
]
1✔
381

382
{ #category : #accessing }
383
GitAnalyzer >> maxChildCommit: max [ 
1✔
384
        maxChildCommits := max
1✔
385
]
1✔
386

387
{ #category : #accessing }
388
GitAnalyzer >> maxChildCommits [
1✔
389
        ^ maxChildCommits
1✔
390
]
1✔
391

392
{ #category : #'as yet unclassified' }
393
GitAnalyzer >> onModel: agitHealthModel [
1✔
394
        glModel := agitHealthModel
1✔
395
]
1✔
396

397
{ #category : #accessing }
398
GitAnalyzer >> onProject: aGLHProject [ 
1✔
399
        onProject := aGLHProject
1✔
400
]
1✔
401

402
{ #category : #sorting }
403
GitAnalyzer >> sortChangeDic: aCollection [ 
1✔
404
        ^ (aCollection associations sortAscending: [ :e | e key ] ) asOrderedDictionary 
1✔
405
]
1✔
406

407
{ #category : #visiting }
NEW
408
GitAnalyzer >> visitChildCommits: commits lookingForFiles: commitFiles [
×
NEW
409

×
NEW
410
        ^ self visitChildCommits:  commits lookingForFiles: commitFiles upto: -1 
×
NEW
411
]
×
412

413
{ #category : #visiting }
414
GitAnalyzer >> visitChildCommits: commits lookingForFiles: commitFiles upto: nCommits [
1✔
415

1✔
416
        commits ifEmpty: [ ^ commitFiles ].
1✔
417
        (nCommits = 0) ifTrue: [ ^ commitFiles ].
1✔
418

1✔
419
        commits do: [ :commit |
1✔
420
                | files |
1✔
421
                files := commit diffs collect: [ :diff | diff ].
1✔
422

1✔
423
                files do: [ :diff |
1✔
424
                        commitFiles
1✔
425
                                at: diff new_path
1✔
426
                                ifPresent: [ :v | v add: commit -> diff diffRanges ]
1✔
427
                                ifAbsent: [  ] ].
1✔
428

1✔
429
                self
1✔
430
                        visitChildCommits: commit childCommits
1✔
431
                        lookingForFiles: commitFiles
1✔
432
                        upto: nCommits - 1 ].
1✔
433

1✔
434
        ^ commitFiles
1✔
435
]
1✔
436

437
{ #category : #visiting }
438
GitAnalyzer >> visitChildCommits: commits toStoreThemIn: commitsFound upto: nCommits [
1✔
439

1✔
440
        commits ifEmpty: [ ^ commitsFound ].
1✔
441
        nCommits = 0 ifTrue: [ ^ commitsFound ].
1✔
442

1✔
443
        commits do: [ :commit |
1✔
444
                commitsFound add: commit.
1✔
445

1✔
446
                self
1✔
447
                        visitChildCommits: commit childCommits
1✔
448
                        toStoreThemIn: commitsFound
1✔
449
                        upto: nCommits - 1 ].
1✔
450

1✔
451
        ^ commitsFound
1✔
452
]
1✔
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