diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..7f02b553 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Moosetechnology + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/res/testResource.zip b/res/testResource.zip new file mode 100644 index 00000000..ae6ef78f Binary files /dev/null and b/res/testResource.zip differ diff --git a/src/BaselineOfGitLabHealth/BaselineOfGitLabHealth.class.st b/src/BaselineOfGitLabHealth/BaselineOfGitLabHealth.class.st index 2cb40145..7048a179 100644 --- a/src/BaselineOfGitLabHealth/BaselineOfGitLabHealth.class.st +++ b/src/BaselineOfGitLabHealth/BaselineOfGitLabHealth.class.st @@ -49,18 +49,19 @@ BaselineOfGitLabHealth >> defineDependencies: spec [ spec baseline: 'GitlabAPI' - with: [ - spec repository: 'github://Evref-BL/Gitlab-Pharo-API:main' ]. - - spec + with: [ spec repository: 'github://Evref-BL/Gitlab-Pharo-API:main' ]. + + spec baseline: 'GithubAPI' - with: [ - spec repository: 'github://Evref-BL/github-Pharo-API:main' ]. + with: [ spec repository: 'github://Evref-BL/github-Pharo-API:main' ]. spec baseline: 'BitbucketPharoAPI' with: [ - spec repository: 'github://Evref-BL/Bitbucket-Pharo-API:main' ] + spec repository: 'github://Evref-BL/Bitbucket-Pharo-API:main' ]. + spec + baseline: 'GitBridge' + with: [ spec repository: 'github://jecisc/GitBridge:v1.0.2/src' ] ] { #category : #baselines } @@ -102,7 +103,10 @@ BaselineOfGitLabHealth >> defineGroups: spec [ 'GitLabHealth-Visualization' 'GitLabProjectHealth-Model-Importer' 'GitProjectHealth-Model-Importer-Tests' 'BitBucketHealth-Model-Importer' - 'BitBucketHealth-Model-Importer-Tests' ). + 'BitBucketHealth-Model-Importer-Tests' + 'GitProjectHealth-Model-LocalImporter' + 'GitProjectHealth-Model-LocalImporter-Tests' + 'GitProjectHealth-Model-MixedImporter' ). spec group: 'default' with: #( 'Core' ) ] @@ -129,8 +133,8 @@ BaselineOfGitLabHealth >> defineJiraConnector: spec [ { #category : #baselines } BaselineOfGitLabHealth >> definePackages: spec [ - "generic" + spec package: 'GitProjectHealth-Model-Importer' with: [ spec requires: #( 'GitLabHealth-Model' ) ]. @@ -138,6 +142,13 @@ BaselineOfGitLabHealth >> definePackages: spec [ package: 'GitProjectHealth-Model-Importer-Tests' with: [ spec requires: #( 'GitProjectHealth-Model-Importer' ) ]. + "test utility" + spec + package: 'GitProjectHealth-Bridge' + with: [ spec requires: #( 'GitBridge' ) ]. + + + "model" spec package: 'GitLabHealth-Model'; @@ -178,6 +189,21 @@ BaselineOfGitLabHealth >> definePackages: spec [ package: 'GitHubHealth-Model-Importer-Tests' with: [ spec requires: #( 'GitHubHealth-Model-Importer' ) ]. + "local" + spec package: 'GitProjectHealth-Model-LocalImporter' with: [ + spec requires: + #( 'GitProjectHealth-Model-Importer' + 'GitLabHealth-Model-Extension' ) ]. + "mixed" + spec + package: 'GitProjectHealth-Model-MixedImporter' + with: [ spec requires: #( 'GitProjectHealth-Model-Importer' ) ]. + + + + spec package: 'GitProjectHealth-Model-LocalImporter-Tests' with: [ + spec requires: #( 'GitProjectHealth-Model-LocalImporter' + 'GitProjectHealth-Bridge' ) ]. "model extension" spec diff --git a/src/BitBucketHealth-Model-Importer/BitBucketModelImporter.class.st b/src/BitBucketHealth-Model-Importer/BitBucketModelImporter.class.st index 96e46172..945cecde 100644 --- a/src/BitBucketHealth-Model-Importer/BitBucketModelImporter.class.st +++ b/src/BitBucketHealth-Model-Importer/BitBucketModelImporter.class.st @@ -1,9 +1,6 @@ Class { #name : #BitBucketModelImporter, #superclass : #GitModelImporter, - #instVars : [ - 'withInitialCommits' - ], #category : #'BitBucketHealth-Model-Importer' } @@ -754,15 +751,3 @@ BitBucketModelImporter >> parseUserIntoGLHUser: userDictionnary [ id: (userDictionnary at: #id); username: (userDictionnary at: #name) ] - -{ #category : #accessing } -BitBucketModelImporter >> withInitialCommits [ - - ^ withInitialCommits -] - -{ #category : #accessing } -BitBucketModelImporter >> withInitialCommits: anObject [ - - withInitialCommits := anObject -] diff --git a/src/GitHubHealth-Model-Importer/GithubModelImporter.class.st b/src/GitHubHealth-Model-Importer/GithubModelImporter.class.st index 17baa0b5..b53efd6e 100644 --- a/src/GitHubHealth-Model-Importer/GithubModelImporter.class.st +++ b/src/GitHubHealth-Model-Importer/GithubModelImporter.class.st @@ -71,8 +71,8 @@ GithubModelImporter >> completeImportedProject: aGLHProject [ aGLHProject repository: GLHRepository new. self glhModel add: aGLHProject repository. - self importPipelinesOfProject: aGLHProject. - self importBranchesOf: aGLHProject. + self importAllPipelinesOfProject: aGLHProject. + self importBranchesOfProject: aGLHProject. self withCommitsSince ifNotNil: [ :withCommitSince | "If not nil, it means we have to import commit" | commits | commits := self importCommitsOfProject: aGLHProject. @@ -271,7 +271,6 @@ GithubModelImporter >> configureReaderForPipeline: reader [ reader for: GLHPipeline do: [ :mapping | "mapping mapInstVar: #status to: #conclusion." - (mapping mapInstVar: #created_at) valueSchema: DateAndTime. (mapping mapInstVar: #updated_at) valueSchema: DateAndTime. @@ -286,8 +285,7 @@ GithubModelImporter >> configureReaderForPipeline: reader [ mapProperty: #conclusion getter: [ :object | #ignore ] setter: [ :object :value | - (object status isNil and: [ value isNotNil ]) ifTrue: [ - object status: value ] ]. + value ifNotNil: [ object status: value ] ]. mapping mapProperty: #run_started_at @@ -343,6 +341,20 @@ GithubModelImporter >> ensureGroupNamed: aGroupName [ ifNone: [ self glhModel newGroupNamed: aGroupName ] ] +{ #category : #'import - pipelines' } +GithubModelImporter >> importAllPipelinesOfProject: project [ + + "add the pipeline (actions runs) in the project" + + | pipelinesResult ghApiPipelineOverview | + pipelinesResult := self repoApi actions + getLatestForRepo: project name + ofOwner: project group name. + ghApiPipelineOverview := self parsePipelinesResult: pipelinesResult. + ghApiPipelineOverview workflow_runs do: [ :pipeline | + project addPipeline: pipeline ] +] + { #category : #'import - branches' } GithubModelImporter >> importBranchesOf: aGLHProject [ "add the pipeline (actions runs) in the project" @@ -379,7 +391,7 @@ GithubModelImporter >> importBranchesOf: aGLHProject [ ^ branches ] -{ #category : #'import - repositories' } +{ #category : #'import - branches' } GithubModelImporter >> importBranchesOfProject: aGLHProject [ "add the pipeline (actions runs) in the project" @@ -817,15 +829,21 @@ GithubModelImporter >> importPipelinesOfMergeRequest: aGLHMergeRequest [ { #category : #'import - pipelines' } GithubModelImporter >> importPipelinesOfProject: project [ - "add the pipeline (actions runs) in the project" | pipelinesResult ghApiPipelineOverview | + self + deprecated: + 'Use importAllPipelinesOfProject: instead of current one' + on: '1 September 2026' + in: + 'Pharo-12.0.0+SNAPSHOT.build.1571.sha.cf5fcd22e66957962c97dffc58b0393b7f368147 (64 Bit)'. + pipelinesResult := self repoApi actions getLatestForRepo: project name ofOwner: project group name. ghApiPipelineOverview := self parsePipelinesResult: pipelinesResult. - ghApiPipelineOverview workflow_runs do: [ :pipeline | + ghApiPipelineOverview workflow_runs do: [ :pipeline | project addPipeline: pipeline ] ] diff --git a/src/GitLabHealth-Model-Analysis-Tests/GLPHImporterMock.class.st b/src/GitLabHealth-Model-Analysis-Tests/GLPHImporterMock.class.st index f89adf80..b55b18a8 100644 --- a/src/GitLabHealth-Model-Analysis-Tests/GLPHImporterMock.class.st +++ b/src/GitLabHealth-Model-Analysis-Tests/GLPHImporterMock.class.st @@ -56,8 +56,15 @@ GLPHImporterMock >> glhModel: anObject [ ] { #category : #'import - repositories' } -GLPHImporterMock >> importBranchesOf: aGLHProject [ - ^ aGLHProject repository branches . +GLPHImporterMock >> importBranchesOf: aGLHProject [ + + self deprecated: #'It has been deprecated' +] + +{ #category : #'import - repositories' } +GLPHImporterMock >> importBranchesOfProject: aGLHProject [ + + ^ aGLHProject repository branches ] { #category : #'import - commits' } diff --git a/src/GitLabHealth-Model-Analysis-Tests/GitAnalyzerTest.class.st b/src/GitLabHealth-Model-Analysis-Tests/GitAnalyzerTest.class.st index 9eb51a2c..fb8e7e77 100644 --- a/src/GitLabHealth-Model-Analysis-Tests/GitAnalyzerTest.class.st +++ b/src/GitLabHealth-Model-Analysis-Tests/GitAnalyzerTest.class.st @@ -60,17 +60,19 @@ GitAnalyzerTest >> testAnalyseAmandment [ mondayCommit -> tuesdayCommit -> wednesdayCommit " wednesdayCommit := glhImporter - importCommitOfProject: projects first - withId: - 'f298ec312f81032b611a890f993d98096ab3bbc1'. + importCommit: + 'f298ec312f81032b611a890f993d98096ab3bbc1' + ofProject: projects first. mondayCommit := glhImporter - importCommitOfProject: projects first - withId: '8a6f665118e6fcfa468d7cce1a7e944ba65812ab'. + importCommit: + '8a6f665118e6fcfa468d7cce1a7e944ba65812ab' + ofProject: projects first. tuesdayCommit := glhImporter - importCommitOfProject: projects first - withId: '2c7308c727560dd425ef78f9a95ca7650d164bed'. + importCommit: + '2c7308c727560dd425ef78f9a95ca7650d164bed' + ofProject: projects first. myCommits := { mondayCommit. @@ -135,18 +137,19 @@ GitAnalyzerTest >> testChurnMetric [ "commit chain: mondayCommit -> tuesdayCommit -> wednesdayCommit " wednesdayCommit := glhImporter - importCommitOfProject: projects first - withId: - 'f298ec312f81032b611a890f993d98096ab3bbc1'. + importCommit: + 'f298ec312f81032b611a890f993d98096ab3bbc1' + ofProject: projects first. mondayCommit := glhImporter - importCommitOfProject: projects first - withId: '8a6f665118e6fcfa468d7cce1a7e944ba65812ab'. + importCommit: + '8a6f665118e6fcfa468d7cce1a7e944ba65812ab' + ofProject: projects first. tuesdayCommit := glhImporter - importCommitOfProject: projects first - withId: '2c7308c727560dd425ef78f9a95ca7650d164bed'. - + importCommit: + '2c7308c727560dd425ef78f9a95ca7650d164bed' + ofProject: projects first. myCommits := { mondayCommit. diff --git a/src/GitLabHealth-Model-Analysis/ActiveBranchesProjectMetric.class.st b/src/GitLabHealth-Model-Analysis/ActiveBranchesProjectMetric.class.st index d02f6aaa..92a6d6b2 100644 --- a/src/GitLabHealth-Model-Analysis/ActiveBranchesProjectMetric.class.st +++ b/src/GitLabHealth-Model-Analysis/ActiveBranchesProjectMetric.class.st @@ -41,7 +41,5 @@ ActiveBranchesProjectMetric >> description [ { #category : #loading } ActiveBranchesProjectMetric >> load [ - ^ projectBranches := self - loadBranchesSince: (period at: #since) - until: (period at: #until) + ^ projectBranches := self loadAllBranches ] diff --git a/src/GitLabHealth-Model-Analysis/GLHFile.extension.st b/src/GitLabHealth-Model-Analysis/GLHFile.extension.st new file mode 100644 index 00000000..05d6df6f --- /dev/null +++ b/src/GitLabHealth-Model-Analysis/GLHFile.extension.st @@ -0,0 +1,26 @@ +Extension { #name : #GLHFile } + +{ #category : #'*GitLabHealth-Model-Analysis' } +GLHFile >> degreeOfAuthorship [ + + ^ GLPDegreeOfAuthorshipAnalyzer new + glhFile: self; + compute +] + +{ #category : #'*GitLabHealth-Model-Analysis' } +GLHFile >> degreeOfAuthorshipAuthors [ + "the most restrictive computation" + + ^ GLPDegreeOfAuthorshipAnalyzer new + glhFile: self; + computeFileAuthors +] + +{ #category : #'*GitLabHealth-Model-Analysis' } +GLHFile >> mainAuthor [ + + ^ (GLPDegreeOfAuthorshipAnalyzer new + glhFile: self; + compute) +] diff --git a/src/GitLabHealth-Model-Analysis/GLPDegreeOfAuthorshipAnalyzer.class.st b/src/GitLabHealth-Model-Analysis/GLPDegreeOfAuthorshipAnalyzer.class.st new file mode 100644 index 00000000..20e0a97b --- /dev/null +++ b/src/GitLabHealth-Model-Analysis/GLPDegreeOfAuthorshipAnalyzer.class.st @@ -0,0 +1,67 @@ +" +Given a `GLHFile` I provide the normalized DOA values with their user. +I also provide an utility that filters the one considered as real author based on k = 0.75 + +(value comes from the [A Novel Approach for Estimating Truck Factors](https://arxiv.org/pdf/1604.06766) research paper) +" +Class { + #name : #GLPDegreeOfAuthorshipAnalyzer, + #superclass : #Object, + #instVars : [ + 'glhFile', + 'rawResults', + 'normalizedResults' + ], + #category : #'GitLabHealth-Model-Analysis-TruckFactor' +} + +{ #category : #accessing } +GLPDegreeOfAuthorshipAnalyzer >> authorshipThreshold [ + + ^ 0.75 +] + +{ #category : #accessing } +GLPDegreeOfAuthorshipAnalyzer >> baseKnowledge [ + + ^ 3.293 +] + +{ #category : #accessing } +GLPDegreeOfAuthorshipAnalyzer >> compute [ + + | authors | + authors := self glhFile diffs + collect: [ :diff | diff commit commitCreator ] + as: Set. + rawResults := (self glhFile mooseModel allWithType: GLHUser) + collect: [ :author | + author -> (GLPUserDegreeOfAuthorshipAnalyzer new + glhFile: self glhFile; + glhUser: author; + compute) ] + as: Dictionary. + normalizedResults := AIMinMaxNormalizer new normalize: rawResults. + ^ normalizedResults +] + +{ #category : #accessing } +GLPDegreeOfAuthorshipAnalyzer >> computeFileAuthors [ + "we consider a developer as an author of a file if its resulting normalized DOA is greater than a threshold k and its absolute DOA is not lower than a value m." + + ^ self compute associationsSelect: [ :assoc | + assoc value >= self authorshipThreshold and: [ + (rawResults at: assoc key) >= self baseKnowledge ] ] +] + +{ #category : #accessing } +GLPDegreeOfAuthorshipAnalyzer >> glhFile [ + + ^ glhFile +] + +{ #category : #accessing } +GLPDegreeOfAuthorshipAnalyzer >> glhFile: anObject [ + + glhFile := anObject +] diff --git a/src/GitLabHealth-Model-Analysis/GLPTruckFactor.class.st b/src/GitLabHealth-Model-Analysis/GLPTruckFactor.class.st new file mode 100644 index 00000000..9ae6e4b6 --- /dev/null +++ b/src/GitLabHealth-Model-Analysis/GLPTruckFactor.class.st @@ -0,0 +1,109 @@ +" +I compute the truckfactor based on the files under directories +" +Class { + #name : #GLPTruckFactor, + #superclass : #Object, + #instVars : [ + 'directories', + 'tfInfo' + ], + #category : #'GitLabHealth-Model-Analysis-TruckFactor' +} + +{ #category : #'as yet unclassified' } +GLPTruckFactor >> blobFiles [ + + ^ self directories flatCollect: [ :directory | + directory allToScope: GLHFileBlob ] +] + +{ #category : #'as yet unclassified' } +GLPTruckFactor >> computeCoverageOf: repositoryFilesSize from: authorsMap [ + | authorsSet | + authorsSet := Set new. + + authorsMap valuesDo: [:files | + files do: [:file | + authorsSet add: file. + authorsSet size = repositoryFilesSize ifTrue: [ ^ 1.0 ] ] ]. + + ^ authorsSet size asFloat / repositoryFilesSize +] + +{ #category : #accessing } +GLPTruckFactor >> directories [ + + ^ directories +] + +{ #category : #accessing } +GLPTruckFactor >> directories: anObject [ + + directories := anObject +] + +{ #category : #accessing } +GLPTruckFactor >> filesAuthorMap [ + + | users authorsDictionary | + authorsDictionary := Dictionary new. + users := directories anyOne mooseModel allWithType: GLHUser. + self blobFiles + select: [ :fileBlob | fileBlob diffs isNotEmpty ] + thenCollect: [ :fileBlob | + fileBlob degreeOfAuthorshipAuthors associationsDo: [ :assoc | + (authorsDictionary at: assoc key ifAbsentPut: [ Set new ]) add: + fileBlob ] ]. + ^ authorsDictionary +] + +{ #category : #initialization } +GLPTruckFactor >> initialize [ + + super initialize. + tfInfo := GLPTruckFactorInfo new +] + +{ #category : #removing } +GLPTruckFactor >> removeTopAuthorOf: repFilesSize from: authorsMap [ + + | biggerNumber biggerDev | + biggerNumber := 0. + biggerDev := nil. + + authorsMap associationsDo: [ :assoc | + assoc value size > biggerNumber ifTrue: [ + biggerNumber := assoc value size. + biggerDev := assoc key ]. + + (biggerDev notNil and: [ + assoc value size = biggerNumber and: [ + (assoc key commits flatCollect: #diffs) size + > (biggerDev commits flatCollect: #diffs) size ] ]) ifTrue: [ + biggerDev := assoc key ] ]. + + tfInfo developers add: biggerDev. + authorsMap removeKey: biggerDev +] + +{ #category : #accessing } +GLPTruckFactor >> truckFactor [ + + | authorsMap numberOfFiles factor coverage | + authorsMap := self filesAuthorMap. + numberOfFiles := self blobFiles size. + factor := 0. + coverage := 1.0. + [ + authorsMap isNotEmpty and: [ + 0.5 < (self computeCoverageOf: numberOfFiles from: authorsMap) ] ] + whileTrue: [ + self removeTopAuthorOf: numberOfFiles from: authorsMap. + factor := factor + 1 ]. + tfInfo coverage: + (self computeCoverageOf: numberOfFiles from: authorsMap). + tfInfo truckFactor: factor. + tfInfo totalFiles: numberOfFiles. + ^ tfInfo +] diff --git a/src/GitLabHealth-Model-Analysis/GLPTruckFactorInfo.class.st b/src/GitLabHealth-Model-Analysis/GLPTruckFactorInfo.class.st new file mode 100644 index 00000000..2382540f --- /dev/null +++ b/src/GitLabHealth-Model-Analysis/GLPTruckFactorInfo.class.st @@ -0,0 +1,104 @@ +Class { + #name : #GLPTruckFactorInfo, + #superclass : #Object, + #instVars : [ + 'truckFactor', + 'totalFiles', + 'coverage', + 'developers' + ], + #category : #'GitLabHealth-Model-Analysis-TruckFactor' +} + +{ #category : #accessing } +GLPTruckFactorInfo >> coverage [ + + ^ coverage +] + +{ #category : #accessing } +GLPTruckFactorInfo >> coverage: anObject [ + + coverage := anObject +] + +{ #category : #accessing } +GLPTruckFactorInfo >> developers [ + + ^ developers +] + +{ #category : #accessing } +GLPTruckFactorInfo >> developers: anObject [ + + developers := anObject +] + +{ #category : #initialization } +GLPTruckFactorInfo >> initialize [ + + super initialize. + developers := OrderedCollection new +] + +{ #category : #initialization } +GLPTruckFactorInfo >> printOn: stream [ + + stream + nextPutAll: '*TF*'; + nextPut: $;; + print: truckFactor; + nextPut: $;; + print: totalFiles; + cr. + + coverage isNaN ifTrue: [ coverage := 0 ]. + + stream + nextPutAll: '*Coverage*'; + nextPut: $;; + print: (coverage * 100 roundTo: 0.01); + nextPut: $;; + print: (totalFiles * coverage) rounded; + cr. + + developers do: [ :developer | + | devFiles | + " devFiles := developer authorshipFiles size. +""stream + nextPutAll: repository; + nextPut: $;; + nextPutAll: developer name; + nextPut: $;; + print: ((devFiles asFloat / totalFiles) * 100) roundTo: 0.01; + nextPut: $;; + print: devFiles; + cr" + ]. + + stream +] + +{ #category : #accessing } +GLPTruckFactorInfo >> totalFiles [ + + ^ totalFiles +] + +{ #category : #accessing } +GLPTruckFactorInfo >> totalFiles: anObject [ + + totalFiles := anObject +] + +{ #category : #accessing } +GLPTruckFactorInfo >> truckFactor [ + + ^ truckFactor +] + +{ #category : #accessing } +GLPTruckFactorInfo >> truckFactor: anObject [ + + truckFactor := anObject +] diff --git a/src/GitLabHealth-Model-Analysis/GLPUserDegreeOfAuthorshipAnalyzer.class.st b/src/GitLabHealth-Model-Analysis/GLPUserDegreeOfAuthorshipAnalyzer.class.st new file mode 100644 index 00000000..0b0a4a57 --- /dev/null +++ b/src/GitLabHealth-Model-Analysis/GLPUserDegreeOfAuthorshipAnalyzer.class.st @@ -0,0 +1,102 @@ +" +I am used by `GLPDegreeOfAuthorshipAnalyzer` + +Based on the paper [A Novel Approach for Estimating Truck Factors](https://arxiv.org/pdf/1604.06766), I compute the DOA between a file and a author based on prexisting computed data by GitProjectHealth. + +Here is the algorithm: + +```language=txt +DOA(md, fp) = 3.293 + 1.098 ×FA(md, fp) + 0.164× DL(md, fp)−0.321 ×ln(1 + AC (md, fp)) + +From the equation, DOA depends on three factors: (i) first authorship (FA): if md originally created f, FA is 1; otherwise it is 0; (ii) number of deliveries (DL): number of changes in f made by md; and (iii) number of acceptances (AC): number of changes in f made by any developer, except md. +``` + +Magic numbers were computed by the authors. + +The paper [Algorithms for Estimating Truck Factors: A Comparative study](https://homepages.dcc.ufmg.br/~mtov/pub/2019-sqj.pdf) consider AVL algorithm (on which is based DOA) correct. So DOA seems good + +## Usage + +```st + +GLPDegreeOfAuthorshipAnalyzer new + glhFile: glhFile; + glhUser: glhUser; + compute +``` +" +Class { + #name : #GLPUserDegreeOfAuthorshipAnalyzer, + #superclass : #Object, + #instVars : [ + 'glhFile', + 'glhUser', + 'baseKnowledge' + ], + #category : #'GitLabHealth-Model-Analysis-TruckFactor' +} + +{ #category : #accessing } +GLPUserDegreeOfAuthorshipAnalyzer >> authorCreatedFile [ + "Paper: Should be 1 if yes. 0 if not" + + "Us: we consider that the author of the first commit on a file is the creator of the file" + + (self glhFile diffs sorted: [ :diff :b | + diff commit committed_date < b commit committed_date ]) first + commit commitCreator = self glhUser + ifTrue: [ ^ 1 ] + ifFalse: [ ^ 0 ] +] + +{ #category : #accessing } +GLPUserDegreeOfAuthorshipAnalyzer >> compute [ + + ^ baseKnowledge + (1.098 * self authorCreatedFile) + + (0.164 * self numberOfChageMadeByOriginalAuthorDL) + - (0.321 * (1 + self numberOfChangeMadeByOtherAuthors) ln) +] + +{ #category : #accessing } +GLPUserDegreeOfAuthorshipAnalyzer >> glhFile [ + + ^ glhFile +] + +{ #category : #accessing } +GLPUserDegreeOfAuthorshipAnalyzer >> glhFile: anObject [ + + glhFile := anObject +] + +{ #category : #accessing } +GLPUserDegreeOfAuthorshipAnalyzer >> glhUser [ + + ^ glhUser +] + +{ #category : #accessing } +GLPUserDegreeOfAuthorshipAnalyzer >> glhUser: anObject [ + + glhUser := anObject +] + +{ #category : #initialization } +GLPUserDegreeOfAuthorshipAnalyzer >> initialize [ + super initialize. + baseKnowledge := 3.293 +] + +{ #category : #accessing } +GLPUserDegreeOfAuthorshipAnalyzer >> numberOfChageMadeByOriginalAuthorDL [ + + ^ self glhFile diffs count: [ :diff | + diff commit commitCreator = self glhUser ] +] + +{ #category : #accessing } +GLPUserDegreeOfAuthorshipAnalyzer >> numberOfChangeMadeByOtherAuthors [ + + ^ self glhFile diffs count: [ :diff | + diff commit commitCreator ~= self glhUser ] +] diff --git a/src/GitLabHealth-Model-Analysis/InactiveBranchesProjectMetric.class.st b/src/GitLabHealth-Model-Analysis/InactiveBranchesProjectMetric.class.st index 62bc8ec7..7df4b053 100644 --- a/src/GitLabHealth-Model-Analysis/InactiveBranchesProjectMetric.class.st +++ b/src/GitLabHealth-Model-Analysis/InactiveBranchesProjectMetric.class.st @@ -39,7 +39,5 @@ InactiveBranchesProjectMetric >> description [ { #category : #loading } InactiveBranchesProjectMetric >> load [ - ^ projectBranches := self - loadBranchesSince: (period at: #since) - until: (period at: #until) + ^ projectBranches := self loadAllBranches ] diff --git a/src/GitLabHealth-Model-Analysis/Metric.class.st b/src/GitLabHealth-Model-Analysis/Metric.class.st index 2ce23f7f..b05182ff 100644 --- a/src/GitLabHealth-Model-Analysis/Metric.class.st +++ b/src/GitLabHealth-Model-Analysis/Metric.class.st @@ -51,8 +51,7 @@ Metric >> foundSuccessorOf: commits andCompleteImportForMax: commitLimit [ visitChildCommits: c childCommits toStoreThemIn: analyzingCommits upto: commitLimit. - analyzingCommits add: c. - ]. + analyzingCommits add: c ]. i := 0. size := analyzingCommits size. diff --git a/src/GitLabHealth-Model-Analysis/ProjectMetric.class.st b/src/GitLabHealth-Model-Analysis/ProjectMetric.class.st index a5e2b9de..f627e77a 100644 --- a/src/GitLabHealth-Model-Analysis/ProjectMetric.class.st +++ b/src/GitLabHealth-Model-Analysis/ProjectMetric.class.st @@ -18,6 +18,15 @@ ProjectMetric class >> isAbstract [ ^ self == ProjectMetric ] +{ #category : #load } +ProjectMetric >> loadAllBranches [ + + | branches | + branches := glhImporter importBranchesOfProject: project. + branches do: [ :branch | glhImporter importRefCommitOfBranch: branch ]. + ^ branches +] + { #category : #loading } ProjectMetric >> loadAllTags [ @@ -27,12 +36,8 @@ ProjectMetric >> loadAllTags [ { #category : #load } ProjectMetric >> loadBranchesSince: since until: until [ - | branches | - branches := glhImporter importBranchesOf: project. - - - branches do: [ :branch | glhImporter importRefCommitOfBranch: branch ]. - ^ branches + ^ self loadAllBranches select: [ :branch | + branch commits first committed_date between: since and: until ] ] { #category : #loading } @@ -47,15 +52,13 @@ ProjectMetric >> loadCommitsSince: since until: until [ until: until. "download commits unless project cache is not empty" - allCommits := project repository - cacheAt: cacheSymbol - ifAbsentPut: [ - | foundCommits | - foundCommits := glhImporter - importCommitsOfProject: project - since: since - until: until. - foundCommits ]. + allCommits := project repository cacheAt: cacheSymbol ifAbsentPut: [ + | foundCommits | + foundCommits := glhImporter + importCommitsOfProject: project + since: since + until: until. + foundCommits ]. allCommits do: [ :commit | glhImporter importCreatorOfCommit: commit ]. glhImporter chainsCommitsFrom: allCommits. @@ -98,7 +101,9 @@ ProjectMetric >> loadMergeRequestsSince: since until: until [ ProjectMetric >> loadProjectCompleteMergeRequestsSince: since until: until [ | mergeRequests | - mergeRequests := self loadProjectMergeRequestsSince: since until: until. + mergeRequests := self + loadProjectMergeRequestsSince: since + until: until. mergeRequests do: [ :mergeRequest | glhImporter importMergeResquestMerger: mergeRequest. diff --git a/src/GitLabHealth-Model-Analysis/String.extension.st b/src/GitLabHealth-Model-Analysis/String.extension.st index d6be4dfd..bc675238 100644 --- a/src/GitLabHealth-Model-Analysis/String.extension.st +++ b/src/GitLabHealth-Model-Analysis/String.extension.st @@ -1,38 +1,39 @@ Extension { #name : #String } { #category : #'*GitLabHealth-Model-Analysis' } -String >> absLevenshteinDistanceWith: aString [ - ^ { self levenshteinDistanceWith: aString . - aString levenshteinDistanceWith: self } max. +String >> absLevenshteinDistanceWith: aString [ + + ^ { + (self levenshteinDistanceWith: aString). + (aString levenshteinDistanceWith: self) } max ] { #category : #'*GitLabHealth-Model-Analysis' } -String >> levenshteinDistanceWith: aString [ - | m n distances cost | - - m := self size. - n := aString size. - - "Initialize a 2D array (m+1 x n+1) to store distances" - distances := Array2D rows: m+1 columns: n+1 element: 0. - - "Fill base cases for first row and first column" - 1 to: m do: [ :i | distances at: i+1 at: 1 put: i ]. - 1 to: n do: [ :j | distances at: 1 at: j+1 put: j ]. - - - "Compute distances using dynamic programming" - 2 to: m do: [ :i | - 2 to: n do: [ :j | - cost := (self at: i) = (aString at: j) ifTrue: [ 0 ] ifFalse: [ 1 ]. - distances at: (i) at: (j) put: { - ((distances at: i-1 at: j) + 1) . - ((distances at: 1 at: j-1) + 1) . - ((distances at: i-1 at: j-1) + cost) . - } min . - ]. - ]. - - "Return the Levenshtein distance between the two strings" - ^ distances at: m at: n +String >> levenshteinDistanceWith: aString [ + + | m n distances cost | + m := self size. + n := aString size. + + "Initialize a 2D array (m+1 x n+1) to store distances" + distances := Array2D rows: m + 1 columns: n + 1 element: 0. + + "Fill base cases for first row and first column" + 1 to: m do: [ :i | distances at: i + 1 at: 1 put: i ]. + 1 to: n do: [ :j | distances at: 1 at: j + 1 put: j ]. + + + "Compute distances using dynamic programming" + 2 to: m do: [ :i | + 2 to: n do: [ :j | + cost := (self at: i) = (aString at: j) + ifTrue: [ 0 ] + ifFalse: [ 1 ]. + distances at: i at: j put: { + ((distances at: i - 1 at: j) + 1). + ((distances at: 1 at: j - 1) + 1). + ((distances at: i - 1 at: j - 1) + cost) } min ] ]. + + "Return the Levenshtein distance between the two strings" + ^ distances at: m at: n ] diff --git a/src/GitLabHealth-Model-Extension-Tests/GLHExtensionTest.class.st b/src/GitLabHealth-Model-Extension-Tests/GLHExtensionTest.class.st index 09169747..50e0bb9c 100644 --- a/src/GitLabHealth-Model-Extension-Tests/GLHExtensionTest.class.st +++ b/src/GitLabHealth-Model-Extension-Tests/GLHExtensionTest.class.st @@ -14,6 +14,177 @@ GLHExtensionTest >> setUp [ model := GLHModel new ] +{ #category : #tests } +GLHExtensionTest >> testAddAllKeyedBy [ + + | col | + col := OrderedCollection new. + col addAll: { 12 } keyedBy: [ :a | a ]. + self assert: col size equals: 1. + self assert: col first equals: 12 +] + +{ #category : #tests } +GLHExtensionTest >> testAddAllKeyedByIntersectionOfUsers [ + + | col result user1 user2 user3 user2b user3b user4 | + user1 := GLHUser new. + user1 id: 1. + user2 := GLHUser new. + user2 id: 2. + user3 := GLHUser new. + user3 id: 3. + col := OrderedCollection new. + col + addAll: { + user1. + user2. + user3 } + keyedBy: [ :a | a id ]. + self assert: col size equals: 3. + user2b := GLHUser new. + user2b id: 2. + user3b := GLHUser new. + user3b id: 3. + user4 := GLHUser new. + user4 id: 4. + + result := col + addAll: { + user2b. + user3b. + user4 } + keyedBy: [ :a | a id ]. + self assert: col size equals: 4. + self assert: result size equals: 3. + self assert: (result includes: user2). + self assert: (result includes: user3). + self assert: (result includes: user4) +] + +{ #category : #tests } +GLHExtensionTest >> testAddAllKeyedByTwiceEqualityIsTheSameButNotObject [ + + | col user result userB | + user := GLHUser new. + user id: 12. + col := OrderedCollection new. + col addAll: { user } keyedBy: [ :a | a id ]. + self assert: col size equals: 1. + self assert: col first identicalTo: user. + userB := GLHUser new. + userB id: 12. + result := col addAll: { userB } keyedBy: [ :a | a id ]. + self assert: col size equals: 1. + self assert: result first identicalTo: user +] + +{ #category : #tests } +GLHExtensionTest >> testAddAllKeyedByTwiceTheSame [ + + | col user result | + user := GLHUser new. + user id: 12. + col := OrderedCollection new. + col addAll: { user } keyedBy: [ :a | a ]. + self assert: col size equals: 1. + self assert: col first identicalTo: user. + result := col addAll: { user } keyedBy: [ :a | a ]. + self assert: col size equals: 1. + self assert: result first identicalTo: user +] + +{ #category : #tests } +GLHExtensionTest >> testAddAllOfTypeKeyed [ + + | user values | + user := GLHUser new. + user id: 12. + values := model + addAll: { user } + ofType: GLHUser + keyedBy: [ :entry | entry id ]. + self assert: values size equals: 1. + self assert: values anyOne equals: user +] + +{ #category : #tests } +GLHExtensionTest >> testAddAllOfTypeKeyedByDifferentEntity [ + + | user values alreadyExistingUser | + alreadyExistingUser := model newUser. + alreadyExistingUser id: 15. + user := GLHUser new. + user id: 12. + values := model + addAll: { user } + ofType: GLHUser + keyedBy: [ :val | val id ]. + self assert: values size equals: 1. + self assert: model size equals: 2. + self assert: (values includes: user). + self assert: (model includes: user). + self assert: (model includes: alreadyExistingUser) +] + +{ #category : #tests } +GLHExtensionTest >> testAddAllOfTypeKeyedDifferentEntity [ + + | user values alreadyExistingUser | + alreadyExistingUser := model newUser. + alreadyExistingUser id: 15. + user := GLHUser new. + user id: 12. + values := model + addAll: { user } + ofType: GLHUser + keyedBy: [ :a | a id ]. + self assert: values size equals: 1. + self assert: model size equals: 2. + self assert: (values includes: user). + self assert: (model includes: user). + self assert: (model includes: alreadyExistingUser) +] + +{ #category : #tests } +GLHExtensionTest >> testAddAllOfTypeKeyedIntersection [ + + | user1 user2 user3 user2b user3b user4 result | + user1 := GLHUser new. + user1 id: 1. + user2 := GLHUser new. + user2 id: 2. + user3 := GLHUser new. + user3 id: 3. + model + addAll: { + user1. + user2. + user3 } + ofType: GLHUser + keyedBy: [ :entry | entry id ]. + self assert: model size equals: 3. + user2b := GLHUser new. + user2b id: 2. + user3b := GLHUser new. + user3b id: 3. + user4 := GLHUser new. + user4 id: 4. + + result := model + addAll: { + user2b. + user3b. + user4 } + ofType: GLHUser + keyedBy: [ :entry | entry id ]. + self assert: model size equals: 4. + self assert: result size equals: 3. + self assert: (result includes: user2). + self assert: (result includes: user3). + self assert: (result includes: user4) +] + { #category : #tests } GLHExtensionTest >> testAddAllOfTypeUnless [ @@ -189,3 +360,13 @@ GLHExtensionTest >> testAddAllUnlessTwiceTheSame [ self assert: col size equals: 1. self assert: result first identicalTo: user ] + +{ #category : #tests } +GLHExtensionTest >> testAddAllkeyedBy [ + + | col | + col := OrderedCollection new. + col addAll: { 12 } keyedBy: [ :a | a ]. + self assert: col size equals: 1. + self assert: col first equals: 12 +] diff --git a/src/GitLabHealth-Model-Extension/GLHChange.extension.st b/src/GitLabHealth-Model-Extension/GLHChange.extension.st index d72c888f..26bfe17f 100644 --- a/src/GitLabHealth-Model-Extension/GLHChange.extension.st +++ b/src/GitLabHealth-Model-Extension/GLHChange.extension.st @@ -31,7 +31,7 @@ GLHChange class >> newFrom: aDiffLine [ aChange := (trimedLine beginsWith: #+) ifTrue: [ GLHAddition new ] ifFalse: [ - (aDiffLine trim beginsWith: #-) + (trimedLine beginsWith: #-) ifTrue: [ GLHDeletion new ] ifFalse: [ GLHLineOfCode new ] ]. diff --git a/src/GitLabHealth-Model-Extension/GLHCommit.extension.st b/src/GitLabHealth-Model-Extension/GLHCommit.extension.st index 8406ee13..5fdf4387 100644 --- a/src/GitLabHealth-Model-Extension/GLHCommit.extension.st +++ b/src/GitLabHealth-Model-Extension/GLHCommit.extension.st @@ -1,5 +1,14 @@ Extension { #name : #GLHCommit } +{ #category : #'*GitLabHealth-Model-Extension' } +GLHCommit >> allBranches [ + "A commit is associated to a branch only if it is the head of its branch. This method search all the branches by exploring the children commit " + + ^ (self + deep: [ :commit | commit childCommits ] + flatCollect: [ :commit | commit branch ]) , self branch +] + { #category : #'*GitLabHealth-Model-Extension' } GLHCommit >> computeChurnForMaxCommits: aLimit [ diff --git a/src/GitLabHealth-Model-Extension/MooseAbstractGroup.extension.st b/src/GitLabHealth-Model-Extension/MooseAbstractGroup.extension.st index 201be1db..69f5e105 100644 --- a/src/GitLabHealth-Model-Extension/MooseAbstractGroup.extension.st +++ b/src/GitLabHealth-Model-Extension/MooseAbstractGroup.extension.st @@ -7,10 +7,55 @@ MooseAbstractGroup >> add: anElmt unless: aConditionAsBlock [ ^ (self addAll: { anElmt } unless: aConditionAsBlock) first ] +{ #category : #'*GitLabHealth-Model-Extension' } +MooseAbstractGroup >> addAll: anElmtCollection keyedBy: aConditionAsBlock [ + "()>>>" + + anElmtCollection ifEmpty: [ ^ {} ]. + ^ self + addAll: anElmtCollection + ofType: anElmtCollection first class + keyedBy: aConditionAsBlock +] + +{ #category : #'*GitLabHealth-Model-Extension' } +MooseAbstractGroup >> addAll: anElmtCollection ofType: aType keyedBy: aKeyBlock [ + + | existingElements elementByKey result | + + existingElements := self allWithType: aType. + + elementByKey := Dictionary new: + existingElements size + anElmtCollection size. + + result := OrderedCollection new: anElmtCollection size. + + "Indexe les éléments déjà présents. + En cas de doublon existant, conserve le premier." + existingElements do: [ :existing | + elementByKey + at: (aKeyBlock value: existing) + ifAbsentPut: [ existing ] ]. + + anElmtCollection do: [ :candidate | + | canonicalElement | + + canonicalElement := elementByKey + at: (aKeyBlock value: candidate) + ifAbsentPut: [ + self add: candidate. + candidate ]. + + result add: canonicalElement ]. + + ^ result +] + { #category : #'*GitLabHealth-Model-Extension' } MooseAbstractGroup >> addAll: anElmtCollection ofType: aType unless: aConditionAsBlock [ | originalCollection returnCollection | + "you should use addAll: anElmtCollection ofType: aType keyedBy: aKeyBlock if possible" originalCollection := (self allWithType: aType). "Create same kind of collection" returnCollection := OrderedCollection new. diff --git a/src/GitLabHealth-Model-Extension/OrderedCollection.extension.st b/src/GitLabHealth-Model-Extension/OrderedCollection.extension.st index db53bb8b..5a1ee2d5 100644 --- a/src/GitLabHealth-Model-Extension/OrderedCollection.extension.st +++ b/src/GitLabHealth-Model-Extension/OrderedCollection.extension.st @@ -1,5 +1,11 @@ Extension { #name : #OrderedCollection } +{ #category : #'*GitLabHealth-Model-Extension' } +OrderedCollection >> add: anElement keyedBy: aKeyBlock [ + + ^ (self addAll: { anElement } keyedBy: aKeyBlock) first +] + { #category : #'*GitLabHealth-Model-Extension' } OrderedCollection >> add: anElmt unless: aConditionAsBlock [ "()>>>" @@ -7,6 +13,34 @@ OrderedCollection >> add: anElmt unless: aConditionAsBlock [ ^ (self addAll: { anElmt } unless: aConditionAsBlock) first ] +{ #category : #'*GitLabHealth-Model-Extension' } +OrderedCollection >> addAll: anElmtCollection keyedBy: aKeyBlock [ + + | elementByKey result | + elementByKey := Dictionary new: self size + anElmtCollection size. + + result := OrderedCollection new: anElmtCollection size. + + "Indexe les éléments déjà présents. + En cas de doublon existant, conserve le premier." + self do: [ :existing | + elementByKey + at: (aKeyBlock value: existing) + ifAbsentPut: [ existing ] ]. + + anElmtCollection do: [ :candidate | + | canonicalElement | + canonicalElement := elementByKey + at: (aKeyBlock value: candidate) + ifAbsentPut: [ + self add: candidate. + candidate ]. + + result add: canonicalElement ]. + + ^ result +] + { #category : #'*GitLabHealth-Model-Extension' } OrderedCollection >> addAll: anElmtCollection unless: aConditionAsBlock [ diff --git a/src/GitLabHealth-Model-Generator/GLHMetamodelGenerator.class.st b/src/GitLabHealth-Model-Generator/GLHMetamodelGenerator.class.st index 81d0290c..02aba235 100644 --- a/src/GitLabHealth-Model-Generator/GLHMetamodelGenerator.class.st +++ b/src/GitLabHealth-Model-Generator/GLHMetamodelGenerator.class.st @@ -292,7 +292,7 @@ GLHMetamodelGenerator >> defineRelations [ (pipeline property: #jobs) <>-* (job property: #pipeline). (pipeline property: #user) *- (user property: #launchedPipelines). (user property: #jobs) <>-* (job property: #user). - (diff property: #file) <>-* (file property: #diffs). + (diff property: #file) *-* (file property: #diffs). (diff property: #diffRanges) <>-* (diffRange property: #diff). (diffRange property: #changes) -* (change property: #diffRange). @@ -685,7 +685,7 @@ GLHMetamodelGenerator >> tagProperties [ { #category : #tags } GLHMetamodelGenerator >> tagRelations [ - (tag property: #commit) - (commit property: #tag). + (tag property: #commit) *- (commit property: #tags). (tag property: #release) - (release property: #tag). (tag property: #repository) *-<> (repository property: #tags). ] diff --git a/src/GitLabHealth-Model-Importer-Tests/GitlabModelImporterRemoteTest.class.st b/src/GitLabHealth-Model-Importer-Tests/GitlabModelImporterRemoteTest.class.st index 1764e285..69d48f83 100644 --- a/src/GitLabHealth-Model-Importer-Tests/GitlabModelImporterRemoteTest.class.st +++ b/src/GitLabHealth-Model-Importer-Tests/GitlabModelImporterRemoteTest.class.st @@ -31,7 +31,7 @@ GitlabModelImporterRemoteTest >> model [ ^ self class resources first current model ] -{ #category : #test } +{ #category : #tests } GitlabModelImporterRemoteTest >> testFilterCommitChanges [ | importer collection resultCollection | diff --git a/src/GitLabHealth-Model-Importer/GitlabModelImporter.class.st b/src/GitLabHealth-Model-Importer/GitlabModelImporter.class.st index 16953646..0f3766d4 100644 --- a/src/GitLabHealth-Model-Importer/GitlabModelImporter.class.st +++ b/src/GitLabHealth-Model-Importer/GitlabModelImporter.class.st @@ -1,10 +1,6 @@ Class { #name : #GitlabModelImporter, #superclass : #GitModelImporter, - #instVars : [ - 'withInitialCommits', - 'withInitialMergeRequest' - ], #category : #'GitLabHealth-Model-Importer' } @@ -179,28 +175,26 @@ GitlabModelImporter >> completeImportedProject: aGLHProject [ add: aGLHProject unless: self blockOnIdEquality. - self importLatestPipelinesOfProject: importedProject. + self factoryImporter importLatestPipelinesOfProject: importedProject. "aGLHProject creator: (self importUser: aGLHProject creator_id)." - (self importUser: importedProject creator_id) addCreatedProject: - importedProject. + (self factoryImporter importUser: importedProject creator_id) + addCreatedProject: importedProject. + - importedProject repository: GLHRepository new. self glhModel add: importedProject repository. self completeImportsOfRepository: importedProject repository. - + withInitialMergeRequest ifTrue: [ - self + self factoryImporter importMergeRequestsOfProject: importedProject since: DateAndTime today until: DateAndTime now ]. ^ importedProject - - ] { #category : #'import - repositories' } @@ -591,18 +585,6 @@ GitlabModelImporter >> glhApi: anObject [ repoApi := anObject ] -{ #category : #accessing } -GitlabModelImporter >> glhModel [ - - ^ glhModel -] - -{ #category : #accessing } -GitlabModelImporter >> glhModel: anObject [ - - glhModel := anObject -] - { #category : #'import - users' } GitlabModelImporter >> importActiveHumanUsers [ @@ -766,73 +748,6 @@ GitlabModelImporter >> importCommit: aCommitID ofProject: aGLHProject [ ^ parsedResult ] -{ #category : #'import - commits' } -GitlabModelImporter >> importCommitOfProject: anProject withId: anID [ - - | commit result | -self - deprecated: 'Use importCommit:ofProject: instead of current one' - on: '27 August 2025' - in: 'MSR 1.3.0 ' - transformWith: '`@rcv importCommitOfProject: `arg1 withId: `arg2' -> '`@rcv importCommit: `arg2 ofProject: `arg1'. - anID ifNil: [ ^ nil ]. - - ('looking for commit ' , anID printString , ' in project : ' - , anProject id printString) recordInfo. - - commit := (self - detectEntityType: GLHCommit - overAttribut: #id - equalTo: anID) ifNil: [ - result := self repoApi commits - get: anID - inProject: anProject id. - commit := (self parseCommitsResult: '[' , result , ']') - first. - - commit := self glhModel - add: commit - unless: self blockOnIdEquality. - commit repository: anProject repository. - - commit ]. - - self withCommitDiffs ifTrue: [ self importDiffOfCommit: commit ]. - - ^ commit -] - -{ #category : #'import - commits' } -GitlabModelImporter >> importCommits: aGLHProject [ - "limited to the last 20 commits" - - | results parsedResults params | - self - deprecated: - 'Use importLatestCommitsOfProject: instead of current one' - on: '19 September 2025' - in: - 'Pharo-12.0.0+SNAPSHOT.build.1571.sha.cf5fcd22e66957962c97dffc58b0393b7f368147 (64 Bit)'. - - params := { (#with_stats -> 'true') } asDictionary. - results := self repoApi commits - getByPage: 1 - perPage: 20 - inProject: aGLHProject id - withParams: params. - - parsedResults := self parseCommitsResult: results. - self glhModel addAll: parsedResults unless: self blockOnIdEquality. - - parsedResults do: [ :commit | - commit repository: aGLHProject repository ]. - - self withCommitDiffs ifTrue: [ - parsedResults do: [ :commit | self importDiffOfCommit: commit ] ]. - - ^ parsedResults -] - { #category : #'import - commits' } GitlabModelImporter >> importCommitsFollowing: aCommit upToDays: aNumberOfDay [ "import the 'n' commits of a project starting from an initial 'aCommit' commit. @@ -873,9 +788,6 @@ GitlabModelImporter >> importCommitsFromTag: fromTag toTag: toTag [ GitlabModelImporter >> importCommitsOfBranch: aGLHBranch [ | commits | - " result := self glhApi - commitsOfProject: aGLHBranch repository project id - forRefName: aGLHBranch name." commits := self importCommitsOfBranch: aGLHBranch since: withCommitsSince. @@ -1097,7 +1009,7 @@ GitlabModelImporter >> importDiffOfMergeRequest: aMergeRequest [ ^ aMergeRequest diffs ] -{ #category : #'import - repositories' } +{ #category : #'import - files' } GitlabModelImporter >> importDirectoryFiles: aDirectoryFile OfBranch: aBranch [ | result files apiFiles params | @@ -1123,13 +1035,13 @@ GitlabModelImporter >> importDirectoryFiles: aDirectoryFile OfBranch: aBranch [ self importDirectoryFiles: file OfBranch: aBranch ] ] -{ #category : #'import - file' } +{ #category : #'import - files' } GitlabModelImporter >> importFileWithPath: aPath ofProject: aGLHProject inBranch: aBranch [ self flag: 'imported file need to be store inside the model'. ^ self repoApi repositories getRawFile: aPath ofProject: aGLHProject id withParams: {#ref -> aBranch name} asDictionary . ] -{ #category : #'import - repositories' } +{ #category : #'import - files' } GitlabModelImporter >> importFilesOfBranch: aBranch [ | result files apiFiles params | @@ -1278,16 +1190,22 @@ GitlabModelImporter >> importLatestReleaseOfProject: aGLHProject [ ^ foundRelease. ] -{ #category : #'import - commits' } +{ #category : #'import - merge-requests' } GitlabModelImporter >> importMergeRequestCommits: aGLPHEMergeRequest [ | commits result | aGLPHEMergeRequest commits ifNotNil: [ ^ aGLPHEMergeRequest commits ]. - - result := self repoApi mergeRequests commitsOf: aGLPHEMergeRequest iid inProject: aGLPHEMergeRequest project id. - - commits := (result collect: [ :commitsJson | self parseCommitsResult: commitsJson ]) flattened. - commits := commits collect: [ :commit | self importCommit: commit id ofProject: aGLPHEMergeRequest project ]. + + result := self repoApi mergeRequests + commitsOf: aGLPHEMergeRequest iid + inProject: aGLPHEMergeRequest project id. + + commits := (result collect: [ :commitsJson | + self parseCommitsResult: commitsJson ]) flattened. + commits := commits collect: [ :commit | "Could we use factory.. unsure because sometimes commits of squashed MR are only saved on remote" + self + importCommit: commit id + ofProject: aGLPHEMergeRequest project ]. aGLPHEMergeRequest commits: commits. @@ -1640,7 +1558,7 @@ GitlabModelImporter >> importProject: aProjectID [ | result projectResult | ('Import project with id: ' , aProjectID printString) recordInfo. - (glhModel allWithType: GLHProject) + (self glhModel allWithType: GLHProject) detect: [ :project | project id = aProjectID ] ifFound: [ :project | ^ project ]. @@ -2212,9 +2130,3 @@ GitlabModelImporter >> subGroupsOf: aGroupID [ ^ subgroups ] - -{ #category : #accessing } -GitlabModelImporter >> withInitialCommits: aBoolean [ - - withInitialCommits := aBoolean -] diff --git a/src/GitLabHealth-Model-Visualization/GLHGroupVisualization.class.st b/src/GitLabHealth-Model-Visualization/GLHGroupVisualization.class.st index 79cb9bd1..aff0bbcd 100644 --- a/src/GitLabHealth-Model-Visualization/GLHGroupVisualization.class.st +++ b/src/GitLabHealth-Model-Visualization/GLHGroupVisualization.class.st @@ -132,7 +132,7 @@ GLHGroupVisualization >> forGroup: aGLHGroups [ canvas: c; shapes: c nodes; connectFrom: #group. - RSHorizontalVanDerPloegTreeLayout new on: c nodes. + RSHorizontalTreeLayout new on: c nodes. legend := self createLegend. legend container: c. diff --git a/src/GitLabHealth-Model/GLHCommit.class.st b/src/GitLabHealth-Model/GLHCommit.class.st index 13d2c100..73e79ac9 100644 --- a/src/GitLabHealth-Model/GLHCommit.class.st +++ b/src/GitLabHealth-Model/GLHCommit.class.st @@ -21,11 +21,11 @@ a commit attached to a repository |---| | `branch` | `GLHCommit` | `commits` | `GLHBranch` | | | `childCommits` | `GLHCommit` | `parentCommits` | `GLHCommit` | | -| `commitedMergeRequest` | `GLHCommit` | `mergeRequestCommit` | `GLHMergeRequest` | | | `commitedMergeRequest` | `GLHCommit` | `mergedCommit` | `GLHMergeRequest` | | +| `commitedMergeRequest` | `GLHCommit` | `mergeRequestCommit` | `GLHMergeRequest` | | | `parentCommits` | `GLHCommit` | `childCommits` | `GLHCommit` | | | `squashedMergeRequest` | `GLHCommit` | `squashCommit` | `GLHMergeRequest` | | -| `tag` | `GLHCommit` | `commit` | `GLHTag` | | +| `tags` | `GLHCommit` | `commit` | `GLHTag` | | ## Properties @@ -58,31 +58,31 @@ Class { #traits : 'FamixTNamedEntity + GLHTRef', #classTraits : 'FamixTNamedEntity classTrait + GLHTRef classTrait', #instVars : [ - '#parent_ids => FMProperty defaultValue: \'OrderedCollection new\'', - '#id => FMProperty', - '#short_id => FMProperty', - '#title => FMProperty', - '#author_name => FMProperty', + '#additions => FMProperty', '#author_email => FMProperty', + '#author_name => FMProperty', '#authored_date => FMProperty', - '#committer_name => FMProperty', - '#committer_email => FMProperty', + '#branch => FMMany type: #GLHBranch opposite: #commits', + '#childCommits => FMMany type: #GLHCommit opposite: #parentCommits', + '#commitCreator => FMOne type: #GLHUser opposite: #commits', + '#commitedMergeRequest => FMOne type: #GLHMergeRequest opposite: #mergedCommit', '#committed_date => FMProperty', + '#committer_email => FMProperty', + '#committer_name => FMProperty', '#created_at => FMProperty', - '#message => FMProperty', - '#web_url => FMProperty', - '#additions => FMProperty', '#deletions => FMProperty', - '#repository => FMOne type: #GLHRepository opposite: #commits', - '#branch => FMMany type: #GLHBranch opposite: #commits', '#diffs => FMMany type: #GLHDiff opposite: #commit', - '#commitCreator => FMOne type: #GLHUser opposite: #commits', - '#parentCommits => FMMany type: #GLHCommit opposite: #childCommits', - '#childCommits => FMMany type: #GLHCommit opposite: #parentCommits', + '#id => FMProperty', '#jobs => FMMany type: #GLHJob opposite: #commit', - '#tag => FMOne type: #GLHTag opposite: #commit', - '#commitedMergeRequest => FMOne type: #GLHMergeRequest opposite: #mergedCommit', - '#squashedMergeRequest => FMOne type: #GLHMergeRequest opposite: #squashCommit' + '#message => FMProperty', + '#parentCommits => FMMany type: #GLHCommit opposite: #childCommits', + '#parent_ids => FMProperty defaultValue: \'OrderedCollection new\'', + '#repository => FMOne type: #GLHRepository opposite: #commits', + '#short_id => FMProperty', + '#squashedMergeRequest => FMOne type: #GLHMergeRequest opposite: #squashCommit', + '#tags => FMMany type: #GLHTag opposite: #commit', + '#title => FMProperty', + '#web_url => FMProperty' ], #category : #'GitLabHealth-Model-Entities' } @@ -126,6 +126,12 @@ GLHCommit >> addParentCommit: anObject [ ^ self parentCommits add: anObject ] +{ #category : #adding } +GLHCommit >> addTag: anObject [ + + ^ self tags add: anObject +] + { #category : #accessing } GLHCommit >> additions [ @@ -473,19 +479,19 @@ GLHCommit >> squashedMergeRequest: anObject [ ] { #category : #accessing } -GLHCommit >> tag [ - "Relation named: #tag type: #GLHTag opposite: #commit" +GLHCommit >> tags [ + "Relation named: #tags type: #GLHTag opposite: #commit" - ^ tag + ^ tags ] { #category : #accessing } -GLHCommit >> tag: anObject [ +GLHCommit >> tags: anObject [ - tag := anObject + tags value: anObject ] { #category : #accessing } diff --git a/src/GitLabHealth-Model/GLHDiff.class.st b/src/GitLabHealth-Model/GLHDiff.class.st index ad9c9172..c0f89837 100644 --- a/src/GitLabHealth-Model/GLHDiff.class.st +++ b/src/GitLabHealth-Model/GLHDiff.class.st @@ -13,11 +13,11 @@ The diff of a commit | Relation | Origin | Opposite | Type | Comment | |---| | `diffRanges` | `GLHDiff` | `diff` | `GLHDiffRange` | | -| `file` | `GLHDiff` | `diffs` | `GLHFile` | | ### Other | Relation | Origin | Opposite | Type | Comment | |---| +| `file` | `GLHDiff` | `diffs` | `GLHFile` | | | `mergeRequest` | `GLHDiff` | `diffs` | `GLHMergeRequest` | | @@ -168,13 +168,6 @@ GLHDiff >> file: anObject [ file value: anObject ] -{ #category : #navigation } -GLHDiff >> fileGroup [ - - - ^ MooseSpecializedGroup withAll: self file asSet -] - { #category : #accessing } GLHDiff >> mergeRequest [ "Relation named: #mergeRequest type: #GLHMergeRequest opposite: #diffs" diff --git a/src/GitLabHealth-Model/GLHFile.class.st b/src/GitLabHealth-Model/GLHFile.class.st index c193f4d3..921217ed 100644 --- a/src/GitLabHealth-Model/GLHFile.class.st +++ b/src/GitLabHealth-Model/GLHFile.class.st @@ -8,9 +8,13 @@ A file | Relation | Origin | Opposite | Type | Comment | |---| | `branch` | `GLHFile` | `files` | `GLHBranch` | | -| `diffs` | `GLHFile` | `file` | `GLHDiff` | | | `directoryOwner` | `GLHFile` | `files` | `GLHFileDirectory` | | +### Other +| Relation | Origin | Opposite | Type | Comment | +|---| +| `diffs` | `GLHFile` | `file` | `GLHDiff` | | + ## Properties ====================== @@ -24,10 +28,10 @@ Class { #name : #GLHFile, #superclass : #GLHEntity, #instVars : [ - '#name => FMProperty', '#branch => FMOne type: #GLHBranch opposite: #files', + '#diffs => FMMany type: #GLHDiff opposite: #file', '#directoryOwner => FMOne type: #GLHFileDirectory opposite: #files', - '#diffs => FMOne type: #GLHDiff opposite: #file' + '#name => FMProperty' ], #category : #'GitLabHealth-Model-Entities' } @@ -41,6 +45,12 @@ GLHFile class >> annotation [ ^ self ] +{ #category : #adding } +GLHFile >> addDiff: anObject [ + + ^ self diffs add: anObject +] + { #category : #accessing } GLHFile >> branch [ "Relation named: #branch type: #GLHBranch opposite: #files" @@ -69,7 +79,6 @@ GLHFile >> diffs [ "Relation named: #diffs type: #GLHDiff opposite: #file" - ^ diffs ] @@ -77,14 +86,7 @@ GLHFile >> diffs [ GLHFile >> diffs: anObject [ - diffs := anObject -] - -{ #category : #navigation } -GLHFile >> diffsGroup [ - - - ^ MooseSpecializedGroup with: self diffs + diffs value: anObject ] { #category : #accessing } diff --git a/src/GitLabHealth-Model/GLHFileDirectory.class.st b/src/GitLabHealth-Model/GLHFileDirectory.class.st index 9838cb5b..bc8ffa29 100644 --- a/src/GitLabHealth-Model/GLHFileDirectory.class.st +++ b/src/GitLabHealth-Model/GLHFileDirectory.class.st @@ -16,7 +16,10 @@ Class { #name : #GLHFileDirectory, #superclass : #GLHFile, #instVars : [ - '#files => FMMany type: #GLHFile opposite: #directoryOwner' + '#files => FMMany type: #GLHFile opposite: #directoryOwner', + '#coverage', + '#numberOfFilesTop1Dev', + '#truckFactor' ], #category : #'GitLabHealth-Model-Entities' } diff --git a/src/GitLabHealth-Model/GLHIssue.class.st b/src/GitLabHealth-Model/GLHIssue.class.st index 2d6fc7bb..80320c00 100644 --- a/src/GitLabHealth-Model/GLHIssue.class.st +++ b/src/GitLabHealth-Model/GLHIssue.class.st @@ -27,8 +27,8 @@ an Issues help collaboration within a team to plan, track, and deliver work | `description` | `String` | nil | | | `due_date` | `Object` | nil | | | `id` | `Number` | nil | | -| `name` | `String` | nil | Basic name of the entity, not full reference.| | `name` | `String` | nil | | +| `name` | `String` | nil | Basic name of the entity, not full reference.| | `state` | `String` | nil | | | `updated_at` | `Object` | nil | | diff --git a/src/GitLabHealth-Model/GLHJob.class.st b/src/GitLabHealth-Model/GLHJob.class.st index b50f9f3d..bed62ed5 100644 --- a/src/GitLabHealth-Model/GLHJob.class.st +++ b/src/GitLabHealth-Model/GLHJob.class.st @@ -20,8 +20,8 @@ A CI Job | `allow_failure` | `Boolean` | nil | | | `duration` | `Object` | nil | | | `id` | `Number` | nil | | -| `name` | `String` | nil | | | `name` | `String` | nil | Basic name of the entity, not full reference.| +| `name` | `String` | nil | | | `ref` | `String` | nil | | | `web_url` | `String` | nil | | diff --git a/src/GitLabHealth-Model/GLHTag.class.st b/src/GitLabHealth-Model/GLHTag.class.st index 8715d8b0..55558d34 100644 --- a/src/GitLabHealth-Model/GLHTag.class.st +++ b/src/GitLabHealth-Model/GLHTag.class.st @@ -12,7 +12,7 @@ a Tag is a reference to a specific point in the repository's history ### Other | Relation | Origin | Opposite | Type | Comment | |---| -| `commit` | `GLHTag` | `tag` | `GLHCommit` | | +| `commit` | `GLHTag` | `tags` | `GLHCommit` | | | `release` | `GLHTag` | `tag` | `GLHRelease` | | @@ -35,13 +35,13 @@ Class { #traits : 'GLHTRef', #classTraits : 'GLHTRef classTrait', #instVars : [ + '#commit => FMOne type: #GLHCommit opposite: #tags', + '#created_at => FMProperty', '#message => FMProperty', - '#target => FMProperty', '#protected => FMProperty', - '#created_at => FMProperty', - '#commit => FMOne type: #GLHCommit opposite: #tag', '#release => FMOne type: #GLHRelease opposite: #tag', - '#repository => FMOne type: #GLHRepository opposite: #tags' + '#repository => FMOne type: #GLHRepository opposite: #tags', + '#target => FMProperty' ], #category : #'GitLabHealth-Model-Entities' } @@ -57,7 +57,7 @@ GLHTag class >> annotation [ { #category : #accessing } GLHTag >> commit [ - "Relation named: #commit type: #GLHCommit opposite: #tag" + "Relation named: #commit type: #GLHCommit opposite: #tags" ^ commit diff --git a/src/GitLabHealth-Visualization/RSCommit.class.st b/src/GitLabHealth-Visualization/RSCommit.class.st index 5637dacf..8f5b8834 100644 --- a/src/GitLabHealth-Visualization/RSCommit.class.st +++ b/src/GitLabHealth-Visualization/RSCommit.class.st @@ -140,7 +140,7 @@ RSCommit >> buildCommitDiffFileImpact [ shapes := RSGroup new. - aCommit diffsGroup do: [ :diff | + aCommit diffs do: [ :diff | shapes add: (RSCommitDiff new shapeForDiffRange: diff diff --git a/src/GitProject-FamixConnector/GPCGitToFamixConnector.class.st b/src/GitProject-FamixConnector/GPCGitToFamixConnector.class.st index a824dd10..b6d85dc5 100644 --- a/src/GitProject-FamixConnector/GPCGitToFamixConnector.class.st +++ b/src/GitProject-FamixConnector/GPCGitToFamixConnector.class.st @@ -40,13 +40,16 @@ GPCGitToFamixConnector >> connectCommit: commit [ { #category : #accessing } GPCGitToFamixConnector >> connectDiff: diff [ "I connect a glhDiff with the famix model" - - (((famixModel allUsing: FamixTClass) reject: #isAnonymousClass) - , (famixModel allUsing: FamixTEnum) - , (famixModel allWithType: FamixJavaInterface) reject: [ :type | - type sourceAnchor isNil ]) + | col | + col := OrderedCollection new. + col addAll: ((famixModel allUsing: FamixTClass) reject: #isAnonymousClass). + col addAll: (famixModel allUsing: FamixTEnum). + col addAll: (famixModel allWithType: FamixJavaInterface). + col detect: [ :class | - class sourceAnchor fileName endsWith: diff new_path ] + (self fullSourceAnchorPath: class) + endsWith: diff new_path + ] ifFound: [ :class | class appliedDiffs add: diff ] ] @@ -62,6 +65,18 @@ GPCGitToFamixConnector >> famixModel: anObject [ famixModel := anObject ] +{ #category : #accessing } +GPCGitToFamixConnector >> fullSourceAnchorPath: aSourcedEntity [ + "string of the full path name of aSourcedEntity source file, within the mooseModel rootFolder" + + ^aSourcedEntity sourceAnchor + ifNil: [ '' ] + ifNotNil: [ :sourceAnchor | + (aSourcedEntity mooseModel rootFolder / (sourceAnchor fileName)) + fullName + ] +] + { #category : #accessing } GPCGitToFamixConnector >> glhProject [ diff --git a/src/GitProjectHealth-Bridge/GPHBridge.class.st b/src/GitProjectHealth-Bridge/GPHBridge.class.st new file mode 100644 index 00000000..3c30cdbc --- /dev/null +++ b/src/GitProjectHealth-Bridge/GPHBridge.class.st @@ -0,0 +1,28 @@ +Class { + #name : #GPHBridge, + #superclass : #GitBridge, + #category : #'GitProjectHealth-Bridge' +} + +{ #category : #'class initialization' } +GPHBridge class >> initialize [ + + SessionManager default registerSystemClassNamed: self name +] + +{ #category : #'class initialization' } +GPHBridge class >> resources [ + ^ self root / 'res' +] + +{ #category : #'class initialization' } +GPHBridge class >> testResources [ + + ^ self resources / 'testResource' +] + +{ #category : #'class initialization' } +GPHBridge class >> testResourcesZip [ + + ^ self resources / 'testResource.zip' +] diff --git a/src/GitProjectHealth-Bridge/ManifestGitProjectHealthBridge.class.st b/src/GitProjectHealth-Bridge/ManifestGitProjectHealthBridge.class.st new file mode 100644 index 00000000..9e51877c --- /dev/null +++ b/src/GitProjectHealth-Bridge/ManifestGitProjectHealthBridge.class.st @@ -0,0 +1,8 @@ +" +This is a the bridge using [GitBridge](https://github.com/jecisc/gitbridge) that link this repository in Pharo to the git source in local +" +Class { + #name : #ManifestGitProjectHealthBridge, + #superclass : #PackageManifest, + #category : #'GitProjectHealth-Bridge-Manifest' +} diff --git a/src/GitProjectHealth-Bridge/package.st b/src/GitProjectHealth-Bridge/package.st new file mode 100644 index 00000000..438cb39b --- /dev/null +++ b/src/GitProjectHealth-Bridge/package.st @@ -0,0 +1 @@ +Package { #name : #'GitProjectHealth-Bridge' } diff --git a/src/GitProjectHealth-Model-Importer-Tests/GPModelImporterTest.class.st b/src/GitProjectHealth-Model-Importer-Tests/GPModelImporterTest.class.st deleted file mode 100644 index 12909087..00000000 --- a/src/GitProjectHealth-Model-Importer-Tests/GPModelImporterTest.class.st +++ /dev/null @@ -1,25 +0,0 @@ -" -A GPModelImporterTest is a test class for testing the behavior of GPModelImporter -" -Class { - #name : #GPModelImporterTest, - #superclass : #TestCase, - #category : #'GitProjectHealth-Model-Importer-Tests' -} - -{ #category : #tests } -GPModelImporterTest >> testParseDiffString [ - - "Given" - | diff diffRange | - diff := GLHDiff new. - - diff diffString: '--- a/etc/name.properties -+++ b/etc/new-name.properties'. - - "When" - diffRange := GitModelImporter new parseDiffString: diff. - - "Then" - self assert: diffRange equals: diff diffRanges. -] diff --git a/src/GitProjectHealth-Model-Importer-Tests/GitModelImporterTest.class.st b/src/GitProjectHealth-Model-Importer-Tests/GitModelImporterTest.class.st new file mode 100644 index 00000000..98bdd9cd --- /dev/null +++ b/src/GitProjectHealth-Model-Importer-Tests/GitModelImporterTest.class.st @@ -0,0 +1,101 @@ +" +A GitModelImporterTest is a test class for testing the behavior of GitModelImporter +" +Class { + #name : #GitModelImporterTest, + #superclass : #TestCase, + #instVars : [ + 'model', + 'importer' + ], + #category : #'GitProjectHealth-Model-Importer-Tests' +} + +{ #category : #running } +GitModelImporterTest >> setUp [ + + super setUp. + model := GLHModel new. + importer := GitModelImporter new. + importer glhModel: model. + importer withCommitDiffs: true +] + +{ #category : #tests } +GitModelImporterTest >> testFilterCommitChanges [ + + | string | + string := '--- hello + world + +++ how + \ No newline at end of file + you' lines. + self assert: (importer filterCommitChanges: string) equals: ' world + you' lines +] + +{ #category : #tests } +GitModelImporterTest >> testParseDiffString [ + + "Given" + | diff diffRange | + diff := GLHDiff new. + + diff diffString: '--- a/etc/name.properties ++++ b/etc/new-name.properties'. + + "When" + diffRange := importer parseDiffString: diff. + + "Then" + self assert: diffRange equals: diff diffRanges. +] + +{ #category : #tests } +GitModelImporterTest >> testParseDiffStringComplete [ + + | diff diffRange | + diff := GLHDiff new. + diff new_path: 'new/test/path.txt'. + model add: diff. + diff diffString: + '@@ -15,9 +15,9 @@ LLMModifierTest >> testGetViolationsMessage [ + message1 := ''message1''. + message2 := ''message2''. + +- violations := OrderedCollection new. ++ violations := MooseSpecializedGroup new. + violations add: (Violation new message: message1). +- ++ + violations add: (Violation new message: message2). + violations add: (Violation new message: message2).'. + importer parseDiffString: diff. + self assert: diff diffRanges size equals: 1. + diffRange := diff diffRanges anyOne. + self assert: diffRange changes size equals: 9. + self + assert: + (diffRange changes asMooseGroup allWithType: GLHDeletion) size + equals: 2. + self + assert: + (diffRange changes asMooseGroup allWithType: GLHAddition) size + equals: 2 +] + +{ #category : #tests } +GitModelImporterTest >> testParseEmptyDiffString [ + "Given" + + | diff diffRange | + diff := GLHDiff new. + + diff diffString: ''. + + "When" + diffRange := importer parseDiffString: diff. + + "Then" + self assert: diffRange isEmpty +] diff --git a/src/GitProjectHealth-Model-Importer-Tests/package.st b/src/GitProjectHealth-Model-Importer-Tests/package.st index 07161f7e..65c2ab3e 100644 --- a/src/GitProjectHealth-Model-Importer-Tests/package.st +++ b/src/GitProjectHealth-Model-Importer-Tests/package.st @@ -1 +1 @@ -Package { #name : #'GitProjectHealth-Model-Importer-Tests' } +Package { #name : #'GitProjectHealth-Model-Importer-Tests' } diff --git a/src/GitProjectHealth-Model-Importer/GPModelImporter.class.st b/src/GitProjectHealth-Model-Importer/GPModelImporter.class.st deleted file mode 100644 index e3ae04ab..00000000 --- a/src/GitProjectHealth-Model-Importer/GPModelImporter.class.st +++ /dev/null @@ -1,30 +0,0 @@ -" -I am a superclass for Model importer. -I am a try of normalization for the different importer. - -I also define some contract so it is easier to switch from one importer to another -" -Class { - #name : #GPModelImporter, - #superclass : #GitModelImporter, - #classVars : [ - 'currentImporter' - ], - #category : #'GitProjectHealth-Model-Importer' -} - -{ #category : #'accessing - global variables' } -GPModelImporter class >> importers [ - ^ currentImporter -] - -{ #category : #testing } -GPModelImporter class >> isDeprecated [ - - ^ true -] - -{ #category : #initialization } -GPModelImporter class >> reset [ - currentImporter := OrderedDictionary new. -] diff --git a/src/GitProjectHealth-Model-Importer/GitModelImporter.class.st b/src/GitProjectHealth-Model-Importer/GitModelImporter.class.st index e048398c..5793eb81 100644 --- a/src/GitProjectHealth-Model-Importer/GitModelImporter.class.st +++ b/src/GitProjectHealth-Model-Importer/GitModelImporter.class.st @@ -14,10 +14,12 @@ Class { 'userCatalogue', 'repoApi', 'withCommitDiffs', - 'generalReader' - ], - #classVars : [ - 'currentImporter' + 'generalReader', + 'withCommitDiffsChanges', + 'withDiffRanges', + 'withInitialCommits', + 'withInitialMergeRequest', + 'factoryImporter' ], #category : #'GitProjectHealth-Model-Importer' } @@ -98,21 +100,6 @@ GitModelImporter class >> forGitlabWithToken: anApiToken andModel: aGitModel [ ^ self forGitlabHostOn: 'https://gitlab.com/api/v4' withToken: anApiToken andModel: aGitModel ] -{ #category : #'accessing - global variables' } -GitModelImporter class >> importers [ - ^ currentImporter -] - -{ #category : #'class initialization' } -GitModelImporter class >> initialize [ - currentImporter := Dictionary new. -] - -{ #category : #initialization } -GitModelImporter class >> reset [ - currentImporter := OrderedDictionary new. -] - { #category : #initialization } GitModelImporter >> beWithFiles [ @@ -146,14 +133,12 @@ GitModelImporter >> blockForBranchEquality [ { #category : #equality } GitModelImporter >> blockForDiffEquality [ -^ [ :existing :new | - | sameSize sameDiff sameFile | - sameSize := existing diffString size = new diffString size. + ^ [ :existing :new | + | sameDiff sameFile | sameDiff := existing diffString = new diffString. sameFile := existing new_path = new new_path. - sameSize and: [ sameDiff and: [ sameFile ] ]. - ] + sameDiff and: [ sameFile ] ] ] { #category : #equality } @@ -192,7 +177,7 @@ GitModelImporter >> blockOnNameEquality [ ^ self blockEqualityOn: #name ] -{ #category : #utils } +{ #category : #utilities } GitModelImporter >> chainsCommitsFrom: commitsCollection [ | dic | @@ -405,6 +390,18 @@ GitModelImporter >> configureReaderForUser: reader [ ] +{ #category : #accessing } +GitModelImporter >> factoryImporter [ + + ^ factoryImporter +] + +{ #category : #accessing } +GitModelImporter >> factoryImporter: anObject [ + + factoryImporter := anObject +] + { #category : #utils } GitModelImporter >> filterCommitChanges: aCollection [ @@ -428,6 +425,12 @@ GitModelImporter >> glhModel: anObject [ glhModel := anObject ] +{ #category : #'import - pipelines' } +GitModelImporter >> importAllPipelinesOfProject: aGLHProject [ + self subclassResponsibility + +] + { #category : #'import - commits' } GitModelImporter >> importAndLoadLatestsCommitsOfProject: aGLHProject [ @@ -446,7 +449,7 @@ GitModelImporter >> importAuthorOfCommit: aGLHCommit [ self subclassResponsibility ] -{ #category : #'import - repositories' } +{ #category : #'import - branches' } GitModelImporter >> importBranchesOf: aGLHProject [ self @@ -458,9 +461,15 @@ GitModelImporter >> importBranchesOf: aGLHProject [ ^ self subclassResponsibility ] -{ #category : #'import - repositories' } +{ #category : #'import - branches' } GitModelImporter >> importBranchesOfProject: aGLHProject [ + ^ self factoryImporter importBranchesOfRepository: aGLHProject repository +] + +{ #category : #'import - branches' } +GitModelImporter >> importBranchesOfRepository: aGLHRepository [ + ^ self subclassResponsibility ] @@ -519,12 +528,22 @@ GitModelImporter >> importDiffRangesForDiff: aGLHDiff [ diffRanges := self glhModel addAll: (self parseDiffString: aGLHDiff) - unless: self blockForDiffRangeEquality. + keyedBy: [ :el | + { + el diff mooseID. + el originalLineRange. + el newLineRange } ]. ^ aGLHDiff diffRanges addAll: diffRanges unless: self blockForDiffRangeEquality ] +{ #category : #'import - files' } +GitModelImporter >> importFilesOfBranch: aGLHBranch [ + + ^ self subclassResponsibility +] + { #category : #'import - groups' } GitModelImporter >> importGroup: aGroupID [ @@ -578,6 +597,8 @@ GitModelImporter >> importLatestReleaseOfProject: aGLHProject [ { #category : #'import - merge-requests' } GitModelImporter >> importMergeRequestCommits: mergeRequest [ + + self subclassResponsibility ] { #category : #'import - merge-requests' } @@ -671,6 +692,13 @@ GitModelImporter >> importRelease: aReleaseID ofProject: aGLHProject [ self subclassResponsibility ] +{ #category : #'import - repositories' } +GitModelImporter >> importRepository: aGLHRepository [ + "Repository is about git, not the social platform directly" + + ^ self subclassResponsibility +] + { #category : #'import - tags' } GitModelImporter >> importTagsForProject: aGLHProject [ self subclassResponsibility @@ -706,8 +734,10 @@ GitModelImporter >> initReader [ GitModelImporter >> initialize [ super initialize. - self withFiles: false. + self withFiles: false. + self withDiffRanges: false. self withCommitsSince: (Date today - 1 week) asDateAndTime. + self factoryImporter: self. userCatalogue := GLHUserCatalogueV2 new anImporter: self; yourself. @@ -715,58 +745,49 @@ GitModelImporter >> initialize [ self initReader ] -{ #category : #initialization } -GitModelImporter >> makeGlobal [ - ^ self makeGlobal: DateAndTime now printString. -] - -{ #category : #initialization } -GitModelImporter >> makeGlobal: aLabel [ - - currentImporter := GithubModelImporter importers ifNil: [ OrderedDictionary new ]. - currentImporter at: aLabel put: self. - ^ currentImporter -] - { #category : #parsing } GitModelImporter >> parseDiffString: aDiff [ "parse diff string to create entities for each lines" - | lines range index firstChangeRange | + | lines index firstChangeRange range | + (aDiff diffString isNil or: [ aDiff diffString isEmpty ]) ifTrue: [ ^ aDiff diffRanges ]. + lines := aDiff diffString lines. index := 0. - range := nil. - - lines ifEmpty: [ ^ aDiff diffRanges ]. lines := self filterCommitChanges: lines. lines ifEmpty: [ ^ aDiff diffRanges ]. (lines first beginsWith: '@@') ifFalse: [ ^ aDiff diffRanges ]. - lines do: [ :line | | aChange | firstChangeRange := false. - (line trim beginsWith: #'@@') ifTrue: [ + (line trimLeft beginsWith: #'@@') ifTrue: [ range := GLHDiffRange newFrom: line. firstChangeRange := true. - range := aDiff diffRanges add: range unless: [ :existing :new | - existing originalLineRange = new originalLineRange and: [ - existing newLineRange = new newLineRange ] ] ]. + range := aDiff diffRanges add: range keyedBy: [ :val | + { + val originalLineRange. + val newLineRange } ] ]. self withCommitDiffs ifTrue: [ firstChangeRange ifFalse: [ aChange := GLHChange newFrom: line. aChange relativeIndex: index. - aChange := range changes add: aChange unless: [ :existing :new | - existing sourceCode = new sourceCode and: [ - existing index = new index ] ]. + aChange := range changes add: aChange keyedBy: [ :el | + { + el index. + el sourceCode } ]. index := index + 1 ] ] ]. "add changes to the diff range" aDiff diffRanges do: [ :diffRange | - self glhModel addAll: diffRange changes unless: [ :existing :new | - existing diffRange mooseID = new diffRange mooseID and: [ - existing sourceCode = new sourceCode and: [ - existing index = new index ] ] ] ]. + self glhModel + addAll: diffRange changes + ofType: GLHChange + keyedBy: [ :val | + { + val diffRange mooseID. + val sourceCode. + val index } ] ]. ^ aDiff diffRanges ] @@ -833,6 +854,18 @@ GitModelImporter >> withCommitsSince: someDays [ ^ withCommitsSince ] +{ #category : #accessing } +GitModelImporter >> withDiffRanges [ + + ^ withDiffRanges +] + +{ #category : #accessing } +GitModelImporter >> withDiffRanges: anObject [ + + withDiffRanges := anObject +] + { #category : #accessing } GitModelImporter >> withFiles [ @@ -844,3 +877,15 @@ GitModelImporter >> withFiles: aBoolean [ withFiles := aBoolean ] + +{ #category : #accessing } +GitModelImporter >> withInitialCommits [ + + ^ withInitialCommits +] + +{ #category : #accessing } +GitModelImporter >> withInitialCommits: anObject [ + + withInitialCommits := anObject +] diff --git a/src/GitProjectHealth-Model-Importer/TGitModelImporterTest.trait.st b/src/GitProjectHealth-Model-Importer/TGitModelImporterTest.trait.st index 72d797a7..b3f9df23 100644 --- a/src/GitProjectHealth-Model-Importer/TGitModelImporterTest.trait.st +++ b/src/GitProjectHealth-Model-Importer/TGitModelImporterTest.trait.st @@ -237,7 +237,7 @@ TGitModelImporterTest >> defaultProject [ TGitModelImporterTest >> testImportBranchesOf [ | collection element | - collection := importer importBranchesOf: self defaultProject. + collection := importer importBranchesOfProject: self defaultProject. self assert: collection isCollection. diff --git a/src/GitProjectHealth-Model-LocalImporter-Tests/GitLocalModelImporterTest.class.st b/src/GitProjectHealth-Model-LocalImporter-Tests/GitLocalModelImporterTest.class.st new file mode 100644 index 00000000..9ab4d790 --- /dev/null +++ b/src/GitProjectHealth-Model-LocalImporter-Tests/GitLocalModelImporterTest.class.st @@ -0,0 +1,144 @@ +" +This tests uses the test repository under GPHBridge (the files of the repository) +" +Class { + #name : #GitLocalModelImporterTest, + #superclass : #TestCase, + #instVars : [ + 'localImporter', + 'model', + 'repository' + ], + #category : #'GitProjectHealth-Model-LocalImporter-Tests' +} + +{ #category : #accessing } +GitLocalModelImporterTest class >> resources [ + + ^ Array with: GitLocalModelImporterWithTestResource +] + +{ #category : #running } +GitLocalModelImporterTest >> setUp [ + + super setUp. + localImporter := GitLocalModelImporter new. + model := GLHModel new. + localImporter glhModel: model. + localImporter withFiles: true. + localImporter withCommitDiffs: true. + localImporter withDiffRanges: true. + localImporter withCommitsSince: (DateAndTime year: 2026 day: 1). + + repository := GLHRepository new + cacheAt: #localImporterReference + put: (GPHBridge testResources / 'testResource') asFileReference; + yourself. + + model add: repository. +] + +{ #category : #tests } +GitLocalModelImporterTest >> testConnectionBetweenDivergingBranches [ + + | lastCommitBranchB | + localImporter importRepository: repository. + lastCommitBranchB := (model allWithType: GLHCommit) detect: [ :commit | + commit id + = '505cd202d27777c018af5590a44689928ea55430' ]. + self assert: lastCommitBranchB childCommits size equals: 2 +] + +{ #category : #tests } +GitLocalModelImporterTest >> testConnectionBetweenSuccessiveBranches [ + + | lastCommitBranchB | + localImporter importRepository: repository. + lastCommitBranchB := (model allWithType: GLHCommit) detect: [ :commit | + commit id + = '02766af59f0b1bbf85861c67aa2128ed5315c8df' ]. + self + assert: lastCommitBranchB parentCommits anyOne id + equals: '4de84ee217a9e6fe1561233db282e5bc661e5e98'. + self + assert: lastCommitBranchB childCommits anyOne id + equals: 'da8ce23a62daec1c558614e2a66aac7f05b9a589' +] + +{ #category : #tests } +GitLocalModelImporterTest >> testFirstCommitHasADiff [ + + | commit | + localImporter importRepository: repository. + commit := (model allWithType: GLHCommit) detect: [ :commi | + commi parentCommits isEmpty ]. + self assert: commit diffs isNotEmpty +] + +{ #category : #tests } +GitLocalModelImporterTest >> testHasRenamedField [ + + | file renameCommit | + localImporter importRepository: repository. + file := (model allWithType: GLHFileBlob) detect: [ :fileBlob | + fileBlob path endsWith: 'renamed-hello.st' ]. + self assert: file diffs size equals: 3. + renameCommit := (model allWithType: GLHCommit) detect: [ :commit | + commit id + = '505cd202d27777c018af5590a44689928ea55430' ]. + self assert: renameCommit diffs anyOne renamed_file +] + +{ #category : #tests } +GitLocalModelImporterTest >> testImportCommitsOfRepositorySinceUntil [ + + | commits originalSize | + localImporter importBranchesOfRepository: repository. + commits := localImporter + importCommitsOfRepository: repository + since: (DateAndTime year: 2026 month: 9 day: 3) + until: (DateAndTime year: 2026 month: 9 day: 5). + originalSize := commits size. + commits := localImporter + importCommitsOfRepository: repository + since: (DateAndTime year: 2026 month: 8 day: 3) + until: (DateAndTime year: 2026 month: 9 day: 5). + self assert: originalSize < commits size +] + +{ #category : #tests } +GitLocalModelImporterTest >> testImportFilesOfBranch [ + + | file | + localImporter importRepository: repository. + file := (model allWithType: GLHFileBlob) detect: [ :fi | + fi name = 'renamed-hello.st' ]. + self assert: file isNotNil +] + +{ #category : #tests } +GitLocalModelImporterTest >> testImportFilesOfBranchDoNotImportNotCommitedFile [ + + | file | + (GPHBridge testResources / 'testResource' / 'wrong-file.st') + ensureCreateFile. + localImporter importRepository: repository. + file := (model allWithType: GLHFileBlob) + detect: [ :fi | fi name = 'wrong-file.st' ] + ifNone: [ nil ]. + self assert: file isNil +] + +{ #category : #tests } +GitLocalModelImporterTest >> testSameHeadCommitForTwoBranches [ + + | branchA anotherBranchA | + localImporter importRepository: repository. + branchA := (model allWithType: GLHBranch) detect: [ :branch | + branch name = 'brancha' ]. + anotherBranchA := (model allWithType: GLHBranch) detect: [ :branch | + branch name = 'another-branch-a' ]. + self + assert: branchA commits anyOne + identicalTo: anotherBranchA commits anyOne +] diff --git a/src/GitProjectHealth-Model-LocalImporter-Tests/GitLocalModelImporterWithTestResource.class.st b/src/GitProjectHealth-Model-LocalImporter-Tests/GitLocalModelImporterWithTestResource.class.st new file mode 100644 index 00000000..0abc3b0d --- /dev/null +++ b/src/GitProjectHealth-Model-LocalImporter-Tests/GitLocalModelImporterWithTestResource.class.st @@ -0,0 +1,20 @@ +Class { + #name : #GitLocalModelImporterWithTestResource, + #superclass : #TestResource, + #category : #'GitProjectHealth-Model-LocalImporter-Tests' +} + +{ #category : #running } +GitLocalModelImporterWithTestResource >> setUp [ + + super setUp. + ZipArchive extractFrom: (GPHBridge testResourcesZip) to: GPHBridge testResources + +] + +{ #category : #running } +GitLocalModelImporterWithTestResource >> tearDown [ + + GPHBridge testResources ensureDeleteAll. + super tearDown +] diff --git a/src/GitProjectHealth-Model-LocalImporter-Tests/package.st b/src/GitProjectHealth-Model-LocalImporter-Tests/package.st new file mode 100644 index 00000000..10572b1d --- /dev/null +++ b/src/GitProjectHealth-Model-LocalImporter-Tests/package.st @@ -0,0 +1 @@ +Package { #name : #'GitProjectHealth-Model-LocalImporter-Tests' } diff --git a/src/GitProjectHealth-Model-LocalImporter/GitLocalModelImporter.class.st b/src/GitProjectHealth-Model-LocalImporter/GitLocalModelImporter.class.st new file mode 100644 index 00000000..af692ada --- /dev/null +++ b/src/GitProjectHealth-Model-LocalImporter/GitLocalModelImporter.class.st @@ -0,0 +1,447 @@ +" +Based on the local path of a git repository, this project fill as possible a model +" +Class { + #name : #GitLocalModelImporter, + #superclass : #GitModelImporter, + #instVars : [ + 'localGitRepositoryFileReference', + 'gitRepository', + 'indexedFiles' + ], + #category : #'GitProjectHealth-Model-LocalImporter' +} + +{ #category : #'import - files' } +GitLocalModelImporter >> buildGPHFileFor: aDiskDirectoryEntry [ + + | file root | + root := (self glhModel allWithSubTypesOf: GLHFile) + detect: [ :f | + f directoryOwner isNil and: [ f name = aDiskDirectoryEntry filename ] ] + ifFound: [ :f | f ] + ifNone: [ + file := (aDiskDirectoryEntry isBlob + ifFalse: [ GLHFileDirectory ] + ifTrue: [ GLHFileBlob ]) new. + self glhModel add: file. + file name: aDiskDirectoryEntry filename ]. + (root isKindOf: GLHFileDirectory) ifTrue: [ + root files: (aDiskDirectoryEntry entries collect: [ :child | + self buildGPHFileFor: child in: root ]) ]. + ^ root +] + +{ #category : #'import - files' } +GitLocalModelImporter >> buildGPHFileFor: aFileEntry in: parent [ + + | modelFile | + modelFile := parent files + detect: [ :childFile | + childFile name = aFileEntry filename ] + ifFound: [ :glhFile | glhFile ] + ifNone: [ + | file | + file := (aFileEntry isBlob + ifFalse: [ GLHFileDirectory ] + ifTrue: [ GLHFileBlob ]) new. + self glhModel add: file. + file name: aFileEntry filename ]. + + (modelFile isKindOf: GLHFileDirectory) ifTrue: [ + modelFile files + addAll: (aFileEntry entries collect: [ :child | + self buildGPHFileFor: child in: modelFile ]) + keyedBy: [ :futurChild | { futurChild name } ] ]. + ^ modelFile +] + +{ #category : #'import - commits' } +GitLocalModelImporter >> connectDiffs: diffs inside: aGPHRepository [ + "could be refactor for a classic lazy accessor. But maybe this project can also be use for several repository. So to think about later" + + indexedFiles ifNil: [ + indexedFiles := (aGPHRepository allToScope: GLHFile) + collect: [ :el | el path -> el ] + as: Dictionary ]. + "reverse order to have a smart rename history" + (diffs asOrderedCollection sort: [ :diffA :diffB | + diffA commit committed_date > diffB commit committed_date ]) do: [ + :diff | + indexedFiles + at: diff new_path + ifPresent: [ :gphFile | + diff addFile: gphFile. + "support for file renaming" + indexedFiles at: diff old_path put: gphFile ] + ifAbsent: [ "not supported yet" ] ] +] + +{ #category : #'private - diffs' } +GitLocalModelImporter >> createDiffFrom: patch and: delta [ + + | glhDiff | + glhDiff := self glhModel newDiff. + glhDiff new_path: delta newFile path. + glhDiff old_path: delta oldFile path. + delta status = LGitDeltaTypeEnum git_delta_renamed ifTrue: [ + glhDiff renamed_file: true ]. + delta status = LGitDeltaTypeEnum git_delta_added ifTrue: [ + glhDiff new_file: true ]. + delta status = LGitDeltaTypeEnum git_delta_deleted ifTrue: [ + glhDiff deleted_file: true ]. + [ glhDiff diffString: (patch patchString allButFirstLines: 2) ] + on: Error + do: [ + self flag: + #'some illegal continuation byte for utf-8 encoding can appears here' ]. + ^ glhDiff +] + +{ #category : #operation } +GitLocalModelImporter >> fetchMRRemotes: repo [ + + | refspecMR remote | + "But will miss old ones https://stackoverflow.com/a/75139038/5257140" + remote := repo allRemotes detect: [ :remote | + remote remoteName = 'origin' ]. + refspecMR := LGitRefSpec fromString: + '+refs/merge-requests/*/head:refs/remotes/origin/mr/*'. + remote fetchSpec: refspecMR. + remote + fetchWithCredentials: + (IceCredentialsProvider defaultForRemoteUrl: remote url) + refSpec: refspecMR +] + +{ #category : #accessing } +GitLocalModelImporter >> gitCommitOf: aGLHCommit [ + + ^ aGLHCommit cacheAt: #importerOrigin ifAbsentPut: [ + LGitCommit + of: (self gitRepositoryOf: aGLHCommit repository) + fromHexString: aGLHCommit id ] +] + +{ #category : #'import - commits' } +GitLocalModelImporter >> gitRepository [ + + ^ (LGitRepository on: localGitRepositoryFileReference) + init; + yourself +] + +{ #category : #accessing } +GitLocalModelImporter >> gitRepositoryOf: aGLHRepository [ + + ^ aGLHRepository cacheAt: #localImporterGitReference ifAbsentPut: [ + | val | + val := aGLHRepository + cacheAt: #localImporterReference + ifAbsentPut: [ self tryClone: aGLHRepository project ]. + val ifNil: [ + Error signal: 'Local GLHRepository without importer reference' ]. + + (LGitRepository on: + (aGLHRepository cacheAt: #localImporterReference ifAbsent: [Error signal: 'Local GLHRepository without importer reference'])) + init; + yourself ] +] + +{ #category : #'import - branches' } +GitLocalModelImporter >> importBranchesOfRepository: aGLHRepository [ + + | branches headCommit addedBranches | + headCommit := (self gitRepositoryOf: aGLHRepository) head object. + branches := (self gitRepositoryOf: aGLHRepository) allBranches + collect: [ :branch | + GLHBranch new + name: branch gitName; + sha: branch annotatedCommit id hexString; + yourself ]. + addedBranches := aGLHRepository branches + addAll: branches + keyedBy: [ :el | el name ]. + self glhModel addAll: branches keyedBy: [ :el | + { + el name. + el repository } ]. + + self withFiles ifTrue: [ + addedBranches do: [ :branch | self importFilesOfBranch: branch ] ]. + (self gitRepositoryOf: aGLHRepository) resetHardTo: headCommit. + + ^ addedBranches +] + +{ #category : #'import - commits' } +GitLocalModelImporter >> importCommit: aLGitCommit [ + + | commit gitDiff stats parentTree | + (self glhModel allWithType: GLHCommit) asOrderedCollection + detect: [ :comm | comm id = aLGitCommit name ] + ifFound: [ :comm | ^ comm ]. + commit := self glhModel newCommit + id: aLGitCommit name; + title: + (aLGitCommit message lines ifEmpty: [ { '' } ]) first; + message: aLGitCommit message; + created_at: aLGitCommit time asDateAndTime; + committed_date: aLGitCommit time asDateAndTime; + committer_email: aLGitCommit author email; + committer_name: aLGitCommit author name; + authored_date: aLGitCommit time asDateAndTime; + author_email: aLGitCommit author email; + author_name: aLGitCommit author name; + parent_ids: + (aLGitCommit parents collect: [ :parent | parent name ]); + yourself. + gitDiff := LGitDiff of: aLGitCommit repository. + parentTree := commit parent_ids + ifNotEmpty: [ :parentIds | + (LGitCommit + of: aLGitCommit repository + fromHexString: parentIds anyOne) tree ] + ifEmpty: [ FFIExternalObject null ]. + gitDiff diffTree: parentTree toTree: aLGitCommit tree. + stats := LGitDiffStats of: aLGitCommit repository. + gitDiff diff_get_stats: stats diff: gitDiff. + commit additions: (stats diff_stats_insertions: stats). + commit deletions: (stats diff_stats_deletions: stats). + self resolveCreatorOfCommit: commit. + ^ commit +] + +{ #category : #'import - commits' } +GitLocalModelImporter >> importCommit: aCommitID ofProject: aGLHProject [ + + | gitCommit commit | + gitCommit := LGitCommit + of: (self gitRepositoryOf: aGLHProject repository) + fromHexString: aCommitID. + + commit := self importCommit: gitCommit. + ^ aGLHProject repository commits add: commit keyedBy: [ :el | el id ] +] + +{ #category : #'import - commits' } +GitLocalModelImporter >> importCommitsOfProject: aProject since: sinceDate until: untilDate [ + + ^ self + importCommitsOfRepository: aProject repository + since: sinceDate + until: untilDate +] + +{ #category : #'import - repositories' } +GitLocalModelImporter >> importCommitsOfRepository: aGLHRepository [ + + ^ self + importCommitsOfRepository: aGLHRepository + since: withCommitsSince + until: nil +] + +{ #category : #'import - commits' } +GitLocalModelImporter >> importCommitsOfRepository: aGPHRepository since: sinceDate until: untilDate [ + + | revwalk commits alreadyExploredCommit toReturn | + commits := Set new. + "to stop when already explored" + alreadyExploredCommit := aGPHRepository commits + collect: [ :commit | commit id ] + as: Set. + (self gitRepositoryOf: aGPHRepository) allBranches do: [ :branch | + | gphBranch shouldContinue currentBranchCommit | + gphBranch := (glhModel allWithType: GLHBranch) + detect: [ :gBranch | branch gitName = gBranch name ] + ifNone: [ nil ]. + + revwalk := LGitRevwalk of: (self gitRepositoryOf: aGPHRepository). + revwalk pushReference: branch. + currentBranchCommit := nil. + shouldContinue := true. + [ + | glhCommit gitCommit | + gitCommit := revwalk next. + glhCommit := self importCommit: gitCommit. + + currentBranchCommit := glhCommit. + commits add: glhCommit. + alreadyExploredCommit add: glhCommit id. + glhCommit cacheAt: #importerOrigin put: gitCommit. + + gphBranch commits ifEmpty: [ gphBranch addCommit: glhCommit ] ] + doWhileTrue: [ + revwalk hasNext and: [ + currentBranchCommit committed_date > sinceDate ] ]. + + self chainsCommitsFrom: commits ]. + + self withCommitDiffs ifTrue: [ + | diffs | + diffs := commits flatCollect: [ :commit | + self importDiffOfCommit: commit ]. + self withFiles ifTrue: [ + self connectDiffs: diffs inside: aGPHRepository ] ]. + aGPHRepository commits addAll: commits keyedBy: [ :el | el id ]. + toReturn := self glhModel allWithType: GLHCommit. + sinceDate ifNotNil: [ + toReturn := toReturn select: [ :c | c committed_date > sinceDate ] ]. + untilDate ifNotNil: [ + toReturn := toReturn select: [ :c | c committed_date < untilDate ] ]. + ^ toReturn +] + +{ #category : #'import - diffs' } +GitLocalModelImporter >> importDiffOfCommit: commit [ + + | sourceCommit gitDiff | + commit diffs ifEmpty: [ + sourceCommit := self gitCommitOf: commit. + gitDiff := LGitDiff of: sourceCommit repository. + + commit parent_ids + ifNotEmpty: [ + | parentTree | + parentTree := commit parent_ids + ifNotEmpty: [ :parentIds | + (LGitCommit + of: sourceCommit repository + fromHexString: parentIds anyOne) tree ] + ifEmpty: [ FFIExternalObject null ]. + gitDiff := gitDiff diffTree: parentTree toTree: sourceCommit tree ] + ifEmpty: [ + gitDiff := gitDiff + diffTree: FFIExternalObject null + toTree: sourceCommit tree ]. + + gitDiff diffFindSimilar. + commit diffs: ((1 to: gitDiff numberOfDeltas) collect: [ :idx | + | delta patch | + delta := gitDiff deltas at: idx. + patch := gitDiff patchAt: idx. + self createDiffFrom: patch and: delta ]) ]. + + "changes are added into the model during the import" + self withDiffRanges ifTrue: [ + commit diffs do: [ :diff | + | ranges | + ranges := self importDiffRangesForDiff: diff ] ]. + + ^ commit diffs +] + +{ #category : #'import - files' } +GitLocalModelImporter >> importFilesOfBranch: aGLHBranch [ + + | gitBranch | + gitRepository := self gitRepositoryOf: aGLHBranch repository. + gitBranch := gitRepository allBranches detect: [ :branch | + branch gitName = aGLHBranch name ]. + + + gitRepository resetHardTo: gitBranch object. + aGLHBranch files: (gitBranch object tree entries collect: [ :entry | + self buildGPHFileFor: entry ]) +] + +{ #category : #'import - commits' } +GitLocalModelImporter >> importLatestCommitsOfProject [ + + | revwalk i commits | + i := 0. + revwalk := LGitRevwalk of: self gitRepository. + commits := OrderedCollection new. + revwalk pushHead. + [ revwalk hasNext and: [ i < 50 ] ] whileTrue: [ + | gitCommit glhCommit | + i := i + 1. + gitCommit := revwalk next. + glhCommit := self importCommit: gitCommit. + commits add: glhCommit. + glhCommit cacheAt: #importerOrigin put: gitCommit ]. + + self chainsCommitsFrom: commits. + + self withCommitDiffs ifTrue: [ + commits do: [ :commit | self importDiffOfCommit: commit ] ] +] + +{ #category : #'import - commits' } +GitLocalModelImporter >> importLatestCommitsOfProject: aGLHProject [ + "this importer load based on inner state local git repository" + + ^ self importLatestCommitsOfProject +] + +{ #category : #'import - repositories' } +GitLocalModelImporter >> importRepository: aGLHRepository [ + + | commits | + self importBranchesOfRepository: aGLHRepository. + commits := self importCommitsOfRepository: aGLHRepository. + aGLHRepository commits addAll: commits keyedBy: [ :el | el id ] +] + +{ #category : #'import - commits' } +GitLocalModelImporter >> importUserByCommit: aGLHCommit [ + + | user | + user := self userCatalogue collectUsernames + at: aGLHCommit author_name + ifAbsentPut: [ + | glhUser | + glhUser := GLHUser new + id: aGLHCommit author_name; + username: aGLHCommit author_name; + name: aGLHCommit author_name; + email: aGLHCommit author_email; + yourself. + + glhUser ]. + self glhModel add: user unless: self blockOnIdEquality. + self userCatalogue addUser: user withName: user username. + ^ user +] + +{ #category : #initialization } +GitLocalModelImporter >> initialize [ + + super initialize. + withCommitDiffs := true +] + +{ #category : #accessing } +GitLocalModelImporter >> localGitRepository: aFileReference [ + + localGitRepositoryFileReference := aFileReference +] + +{ #category : #'import - commits' } +GitLocalModelImporter >> resolveCreatorOfCommit: aGLHCommit [ + aGLHCommit commitCreator ifNil: [ + aGLHCommit commitCreator: + (self importUserByCommit: aGLHCommit) ]. + + ^ aGLHCommit commitCreator +] + +{ #category : #operation } +GitLocalModelImporter >> tryClone: aGLHProject [ + + | url clonePath gitUrl repo cloneOptions | + aGLHProject ifNil: [ ^ nil ]. + url := aGLHProject web_url ifNil: [ ^ nil ]. + clonePath := FileSystem workingDirectory / 'msr' / aGLHProject name. + clonePath exists ifTrue: [ ^ clonePath ]. + clonePath ensureCreateDirectory. + gitUrl := url , '.git'. + repo := LGitRepository on: clonePath. + cloneOptions := repo cloneOptionsStructureClass + withCredentialsProvider: + (IceCredentialsProvider defaultForRemoteUrl: url). + repo clone: gitUrl options: cloneOptions. + self fetchMRRemotes: repo. + ^ clonePath +] diff --git a/src/GitProjectHealth-Model-LocalImporter/LGitPatch.extension.st b/src/GitProjectHealth-Model-LocalImporter/LGitPatch.extension.st new file mode 100644 index 00000000..f7750111 --- /dev/null +++ b/src/GitProjectHealth-Model-LocalImporter/LGitPatch.extension.st @@ -0,0 +1,21 @@ +Extension { #name : #LGitPatch } + +{ #category : #'*GitProjectHealth-Model-LocalImporter' } +LGitPatch >> patchString [ + | buf patchString | + buf := LGitBuf new. + self patch_to_buf: buf. + patchString := buf prim_ptr utf8StringFromCString. + buf free. + ^ patchString +] + +{ #category : #'*GitProjectHealth-Model-LocalImporter' } +LGitPatch >> patch_to_buf: out [ + + ^ self + ffiCallSafely: + #( LGitReturnCodeEnum git_patch_to_buf #( LGitBuf * out , LGitPatch + self ) ) + options: #( optMayGC optCoerceNilToNull ) +] diff --git a/src/GitProjectHealth-Model-LocalImporter/LGitRefSpec.extension.st b/src/GitProjectHealth-Model-LocalImporter/LGitRefSpec.extension.st new file mode 100644 index 00000000..ae56ccc0 --- /dev/null +++ b/src/GitProjectHealth-Model-LocalImporter/LGitRefSpec.extension.st @@ -0,0 +1,18 @@ +Extension { #name : #LGitRefSpec } + +{ #category : #'*GitProjectHealth-Model-LocalImporter' } +LGitRefSpec >> initializeFromString: aString [ + + | stream | + + "self assert: ('(\+?\w(\w|\-|\.|/)*)?\:\w(\w|\-|\.|/)*' asRegex matches: aString)." + "this is wrong, for example ''+refs/merge-requests/*/head:refs/remotes/origin/mr/*' should work" + + stream := aString readStream. + [ + (stream peek = $+) ifTrue: [ + self beForced. + stream next ]. + self source: (stream upTo: $:). + self destination: stream upToEnd ] ensure: [ stream close ] +] diff --git a/src/GitProjectHealth-Model-LocalImporter/LGitReference.extension.st b/src/GitProjectHealth-Model-LocalImporter/LGitReference.extension.st new file mode 100644 index 00000000..32c0d279 --- /dev/null +++ b/src/GitProjectHealth-Model-LocalImporter/LGitReference.extension.st @@ -0,0 +1,10 @@ +Extension { #name : #LGitReference } + +{ #category : #'*GitProjectHealth-Model-LocalImporter' } +LGitReference >> gitName [ + + ^ self isLocal + ifTrue: [ self name withoutPrefix: 'refs/heads/' ] + ifFalse: [ "removing the refs/remote and the name of the remote" + (self name withoutPrefix: 'refs/remotes/') copyAfter: $/ ] +] diff --git a/src/GitProjectHealth-Model-LocalImporter/String.extension.st b/src/GitProjectHealth-Model-LocalImporter/String.extension.st new file mode 100644 index 00000000..eca88c1d --- /dev/null +++ b/src/GitProjectHealth-Model-LocalImporter/String.extension.st @@ -0,0 +1,15 @@ +Extension { #name : #String } + +{ #category : #'*GitProjectHealth-Model-LocalImporter' } +String >> allButFirstLines: aNumberOfLine [ + + | stream skipped | + stream := ReadStream on: self. + skipped := 0. + + [ stream atEnd or: [ skipped >= aNumberOfLine ] ] whileFalse: [ + stream upTo: Character lf. + skipped := skipped + 1 ]. + + ^ stream upToEnd +] diff --git a/src/GitProjectHealth-Model-LocalImporter/package.st b/src/GitProjectHealth-Model-LocalImporter/package.st new file mode 100644 index 00000000..7cb970b8 --- /dev/null +++ b/src/GitProjectHealth-Model-LocalImporter/package.st @@ -0,0 +1 @@ +Package { #name : #'GitProjectHealth-Model-LocalImporter' } diff --git a/src/GitProjectHealth-Model-MixedImporter/GitModelMixedImporter.class.st b/src/GitProjectHealth-Model-MixedImporter/GitModelMixedImporter.class.st new file mode 100644 index 00000000..d9b90b52 --- /dev/null +++ b/src/GitProjectHealth-Model-MixedImporter/GitModelMixedImporter.class.st @@ -0,0 +1,149 @@ +Class { + #name : #GitModelMixedImporter, + #superclass : #GitModelImporter, + #instVars : [ + 'apiImporter', + 'localImporter' + ], + #category : #'GitProjectHealth-Model-MixedImporter' +} + +{ #category : #accessing } +GitModelMixedImporter >> apiImporter [ + + ^ apiImporter +] + +{ #category : #accessing } +GitModelMixedImporter >> apiImporter: anObject [ + + apiImporter := anObject. + apiImporter factoryImporter: self +] + +{ #category : #accessing } +GitModelMixedImporter >> glhModel: anObject [ + + glhModel := anObject. + self apiImporter ifNotNil: [ :importer | importer glhModel: anObject ]. + self localImporter ifNotNil: [ :importer | importer glhModel: anObject ] +] + +{ #category : #'import - branches' } +GitModelMixedImporter >> importBranchesOfProject: aGLHProject [ + + ^ self localImporter importBranchesOfProject: aGLHProject +] + +{ #category : #'import - branches' } +GitModelMixedImporter >> importBranchesOfRepository: aGLHRepository [ + + ^ self localImporter importBranchesOfRepository: aGLHRepository +] + +{ #category : #'import - commits' } +GitModelMixedImporter >> importCommit: aCommitID ofProject: aGLHProject [ + + ^ [ + self localImporter importCommit: aCommitID ofProject: aGLHProject ] + on: LGit_GIT_ENOTFOUND + do: [ + self apiImporter importCommit: aCommitID ofProject: aGLHProject ] +] + +{ #category : #'import - commits' } +GitModelMixedImporter >> importCommitsOfProject: aGLHProject since: since until: until [ + + ^ self localImporter importCommitsOfProject: aGLHProject since: since until: until +] + +{ #category : #'import - diffs' } +GitModelMixedImporter >> importDiffOfCommit: aCommit [ + + ^ [ self localImporter importDiffOfCommit: aCommit ] + on: LGit_GIT_ENOTFOUND + do: [ self apiImporter importDiffOfCommit: aCommit ] +] + +{ #category : #'import - pipelines' } +GitModelMixedImporter >> importLatestPipelinesOfProject: aGLHProject [ + + ^ self apiImporter importLatestPipelinesOfProject: aGLHProject +] + +{ #category : #'import - merge-requests' } +GitModelMixedImporter >> importMergeRequestCommits: mergeRequest [ + + ^ self apiImporter importMergeRequestCommits: mergeRequest +] + +{ #category : #'import - merge-requests' } +GitModelMixedImporter >> importMergeRequestMergeCommits: aGLHMergeRequest [ + + ^ self apiImporter importMergeRequestMergeCommits: aGLHMergeRequest +] + +{ #category : #'import - merge-requests' } +GitModelMixedImporter >> importMergeRequestsOfProject: aGLHProject since: fromDate until: toDate [ + + ^ self apiImporter importMergeRequestsOfProject: aGLHProject since: fromDate until: toDate +] + +{ #category : #'import - notes' } +GitModelMixedImporter >> importNotesOfMergeRequest: mergeRequest [ + + ^ self apiImporter importNotesOfMergeRequest: mergeRequest +] + +{ #category : #'import - projects' } +GitModelMixedImporter >> importProject: id [ + + ^ self apiImporter importProject: id +] + +{ #category : #'import - tags' } +GitModelMixedImporter >> importTagsForProject: aGLHProject [ + + ^ self apiImporter importTagsForProject: aGLHProject +] + +{ #category : #'import - users' } +GitModelMixedImporter >> importUser: aUserID [ + + ^ self apiImporter importUser: aUserID +] + +{ #category : #'import - users' } +GitModelMixedImporter >> importUserByUsername: username [ + + ^ self apiImporter importUserByUsername: username +] + +{ #category : #accessing } +GitModelMixedImporter >> localImporter [ + + ^ localImporter +] + +{ #category : #accessing } +GitModelMixedImporter >> localImporter: anObject [ + + localImporter := anObject. + localImporter factoryImporter: self +] + +{ #category : #accessing } +GitModelMixedImporter >> withCommitDiffs: aBoolean [ + + super withCommitDiffs: aBoolean. + self localImporter withCommitDiffs: aBoolean. + self apiImporter withCommitDiffs: aBoolean. +] + +{ #category : #accessing } +GitModelMixedImporter >> withDiffRanges: aBoolean [ + + super withDiffRanges: aBoolean. + self localImporter ifNotNil: [ :importer | importer withDiffRanges: aBoolean ]. + self apiImporter ifNotNil: [ :importer | importer withDiffRanges: aBoolean ]. +] diff --git a/src/GitProjectHealth-Model-MixedImporter/package.st b/src/GitProjectHealth-Model-MixedImporter/package.st new file mode 100644 index 00000000..fab22fb7 --- /dev/null +++ b/src/GitProjectHealth-Model-MixedImporter/package.st @@ -0,0 +1 @@ +Package { #name : #'GitProjectHealth-Model-MixedImporter' }