From 89c52cd2ab2e246fe6160b477690eec36f025eb0 Mon Sep 17 00:00:00 2001 From: Kilian B <60846047+knowbased@users.noreply.github.com> Date: Thu, 3 Oct 2024 16:08:34 +0200 Subject: [PATCH] feat: bitbucket importer (#74) * feat: add bitbucket package + base bitbucket api * chore: add bitbucket package in baseline * feat: get user method in bitBucket api * feat: importUser for bitbucket importer * feat: change bitbucket api for 1.0 + add projects method * feat(BitBucketApi): repositories of a project * feat(BitBucketApi): commits of repo in project since until date * feat(BitBucketImporter): importContributedProjectsOfUser method * refactor: improve repo/project transformation into GLHProject * test: add tests for bitbucket api and importer * feat: base completeImportProject * feat: import commits of project in bit bucket importer * feat(bitBucketImporter): import creator of commit method * refactor: move testImportUser method * refactor: rename importCommitsOfProject:since:until * fix: change imporContributedProject test to work on any date * feat(BitBucketApi): remove merge commit in commitsOfRepo:Inproject * feat(bitBucketImporter): add getContributionFromDiff * test: improve tests getContributionFromDiffs: * feat(bitBucketModelImporter): add withInitialCommits * feat(BitBucketApi): add usersByUserName * feat(BitBucketModelImporter): importUserByUsername * fix: problem with commitsInRepoOfProject * fix: small fix in bitbucket importer * fix: infinit loop with importContributedProjects * feat(BitBucketModelImporter): fix importCreatorOfCommit + add tests * test(BitBucketModelImporter): fix test importContributedProject * feat: add parentsId in bit bucket importer commit parsing * feat(BitBucketApi): pullrequestsOfRepo:InProject:Since:Until * feat(bitbucketModelImporter): importMergeRequests:since:until * fix(bitBucketImporter): improve precision in dates * feat(BitBucketModelImporter): add reviewers in parsing of pull requests * feat(BitBucketApi): activities of pull requests * feat(BitBucketModelImporter): import merge request merger * feat(BitBucketApi): commits of pull requests * feat(BitBucketModelImporter): importMergeRequestCommits * fix(BitBucketApi): error in commit created_at date * test: improve mock structure * refactor(BitBucketModelImporter): change classifications of methods * chore: add bitbucket test package in baseline * ci: add bitbucket packages in smalltalk.ston testing --- .smalltalk.ston | 4 +- .../BaselineOfGitLabHealth.class.st | 21 +- .../BitBucketApiMock.class.st | 1189 +++++++++++++++++ .../BitBucketModelImporterTest.class.st | 479 +++++++ .../package.st | 1 + .../BitBucketApi.class.st | 272 ++++ .../BitBucketModelImporter.class.st | 450 +++++++ src/BitBucketHealth-Model-Importer/package.st | 1 + .../GLPHImporterMock.class.st | 2 +- .../GitAnalyzerTest.class.st | 2 +- .../GitAnalyzer.class.st | 2 +- .../GitMetricExporter.class.st | 14 +- .../MergeRequestDurationMetric.class.st | 1 + .../UserMergeRequestMetric.class.st | 3 +- .../UserMetric.class.st | 5 +- .../GLHModelImporter.class.st | 78 +- 16 files changed, 2461 insertions(+), 63 deletions(-) create mode 100644 src/BitBucketHealth-Model-Importer-Tests/BitBucketApiMock.class.st create mode 100644 src/BitBucketHealth-Model-Importer-Tests/BitBucketModelImporterTest.class.st create mode 100644 src/BitBucketHealth-Model-Importer-Tests/package.st create mode 100644 src/BitBucketHealth-Model-Importer/BitBucketApi.class.st create mode 100644 src/BitBucketHealth-Model-Importer/BitBucketModelImporter.class.st create mode 100644 src/BitBucketHealth-Model-Importer/package.st diff --git a/.smalltalk.ston b/.smalltalk.ston index 7749e40..6423cb2 100644 --- a/.smalltalk.ston +++ b/.smalltalk.ston @@ -11,9 +11,9 @@ SmalltalkCISpec { } ], #testing: { - #packages : [ 'GitLab.*', 'GLPH.*', 'GitHub.*', 'GitProject.*' ], + #packages : [ 'GitLab.*', 'GLPH.*', 'GitHub.*', 'GitProject.*', 'BitBucket.*' ], #coverage : { - #packages : [ 'GitLab.*', 'GLPH.*', 'GitHub.*', 'GitProject.*' ], + #packages : [ 'GitLab.*', 'GLPH.*', 'GitHub.*', 'GitProject.*', 'BitBucket.*' ], #format : #lcov } } diff --git a/src/BaselineOfGitLabHealth/BaselineOfGitLabHealth.class.st b/src/BaselineOfGitLabHealth/BaselineOfGitLabHealth.class.st index ecbc2d4..f3cf6da 100644 --- a/src/BaselineOfGitLabHealth/BaselineOfGitLabHealth.class.st +++ b/src/BaselineOfGitLabHealth/BaselineOfGitLabHealth.class.st @@ -92,7 +92,8 @@ BaselineOfGitLabHealth >> defineGroups: spec [ 'GitLabHealth-Model-Analysis-Tests' 'GitLabHealth-Visualization' 'GitLabProjectHealth-ExtendModel-Generator' 'GitLabProjectHealth-Model-Importer' - 'GitLabProjectHealth-Model-Importer-Tests' ). + 'GitLabProjectHealth-Model-Importer-Tests' + 'BitBucketHealth-Model-Importer' 'BitBucketHealth-Model-Importer-Tests' ). spec group: 'default' with: #( 'Core' ) ] @@ -123,7 +124,6 @@ BaselineOfGitLabHealth >> definePackages: spec [ spec package: 'GitProjectHealth-Model-Importer'. - "gitlab" spec package: 'GitLabHealth-Model'; @@ -135,10 +135,12 @@ BaselineOfGitLabHealth >> definePackages: spec [ package: 'GitLabHealth-Model-Inspector' with: [ spec requires: #( 'GitLabHealth-Model-Visualization' ) ]; package: 'GitLabHealth-Model-Visualization'; - package: 'GitLabHealth-Model-Importer' - with: [ spec requires: #( 'NeoJSON' 'MoreLogger' 'GitProjectHealth-Model-Importer' ) ]; - package: 'GitHubHealth-Model-Importer' - with: [ spec requires: #( 'NeoJSON' 'MoreLogger' 'GitProjectHealth-Model-Importer' ) ]; + package: 'GitLabHealth-Model-Importer' with: [ + spec requires: + #( 'NeoJSON' 'MoreLogger' 'GitProjectHealth-Model-Importer' ) ]; + package: 'GitHubHealth-Model-Importer' with: [ + spec requires: + #( 'NeoJSON' 'MoreLogger' 'GitProjectHealth-Model-Importer' ) ]; package: 'GitLabHealth-Model-Importer-Tests' with: [ spec requires: #( 'GitLabHealth-Model-Importer' 'GitHubHealth-Model-Importer' ) ]. @@ -146,6 +148,10 @@ BaselineOfGitLabHealth >> definePackages: spec [ package: 'GitHubHealth-Model-Importer-Tests' with: [ spec requires: #( 'GitHubHealth-Model-Importer' ) ]. + "bitBucket" + spec package: 'BitBucketHealth-Model-Importer'; + package: 'BitBucketHealth-Model-Importer-Tests' with: [ spec requires: #( 'BitBucketHealth-Model-Importer' ) ]. + "model extension" spec package: 'GLPHExtended-Model' with: [ @@ -156,7 +162,8 @@ BaselineOfGitLabHealth >> definePackages: spec [ package: 'GLPHExtended-Model-Extension' with: [ spec requires: #( 'GLPHExtended-Model' 'GitLabHealth-Model' 'GitLabHealth-Model-Extension' ) ]; - package: 'GitLabHealth-Model-Analysis' with: [ spec requires: #( 'Voyage' 'AWS' ) ]; + package: 'GitLabHealth-Model-Analysis' + with: [ spec requires: #( 'Voyage' 'AWS' ) ]; package: 'GitLabHealth-Model-Analysis-Tests' with: [ spec requires: #( 'GitLabHealth-Model-Analysis' ) ]; package: 'GitLabHealth-Visualization'; diff --git a/src/BitBucketHealth-Model-Importer-Tests/BitBucketApiMock.class.st b/src/BitBucketHealth-Model-Importer-Tests/BitBucketApiMock.class.st new file mode 100644 index 0000000..687146b --- /dev/null +++ b/src/BitBucketHealth-Model-Importer-Tests/BitBucketApiMock.class.st @@ -0,0 +1,1189 @@ +Class { + #name : #BitBucketApiMock, + #superclass : #Object, + #instVars : [ + 'userMock', + 'commits', + 'diffs', + 'mergeRequests' + ], + #category : #'BitBucketHealth-Model-Importer-Tests' +} + +{ #category : #'api - pull-requests' } +BitBucketApiMock >> activitiesOfPullRequest: pullRequestId inRepo: repoSlug ofProject: projectKey [ + + ^self pullRequestActivities +] + +{ #category : #accessing } +BitBucketApiMock >> commits [ + + ^ commits +] + +{ #category : #accessing } +BitBucketApiMock >> commits1 [ + + | commits1 | + commits1 := '[ + { + "id": "abcdef0123abcdef4567abcdef8987abcdef6543", + "message": "message test", + "displayId": "abcdef0123a", + "author": { + "name": "charlie", + "emailAddress": "charlie@example.com" + }, + "authorTimestamp": 1727168151000, + "committer": { + "name": "charlie", + "emailAddress": "charlie@example.com" + }, + "committerTimestamp": 1727168151000, + "message": "WIP on feature 1", + "parents": [ + { + "id": "abcdef0123abcdef4567abcdef8987abcdef6543", + "displayId": "abcdef0" + } + ] + } + ]'. + + ^ commits1 := (NeoJSONReader on: commits1 readStream) next +] + +{ #category : #accessing } +BitBucketApiMock >> commits: anObject [ + + commits := anObject +] + +{ #category : #'api - pull-requests' } +BitBucketApiMock >> commitsOfPullRequest: mergeRequestId ofRepo: repoSlug inProject: projectKey [ + + ^commits +] + +{ #category : #'api - projects' } +BitBucketApiMock >> commitsOfRepo: repositorySlug inProject: projectKey since: since until: until [ + + ^ self commits select: [ :commit | + | commitDate | + commitDate := DateAndTime fromUnixTime: + (commit at: #committerTimestamp) / 1000. + commitDate >= since asDate and: commitDate <= until asDate ] +] + +{ #category : #accessing } +BitBucketApiMock >> declinedMergeRequest [ + + | pullRequest | + pullRequest := '{ + "id": 539, + "version": 10, + "title": "title", + "state": "DECLINED", + "open": false, + "closed": true, + "createdDate": 1721396425473, + "updatedDate": 1721457513310, + "closedDate": 1721457513310, + "fromRef": { + "id": "refs/heads/wip/1", + "displayId": "wip/1", + "latestCommit": "2", + "repository": { + "slug": "repoSlug", + "id": 242, + "name": "repo-name", + "scmId": "git", + "state": "AVAILABLE", + "statusMessage": "Available", + "forkable": true, + "project": { + "key": "project-key", + "id": 242, + "name": "project-name", + "description": "project description", + "public": true, + "type": "NORMAL", + "links": { + "self": [ + { + "href": "link" + } + ] + } + }, + "public": false, + "links": { + "clone": [ + { + "href": "clone", + "name": "ssh" + }, + { + "href": "clone", + "name": "http" + } + ], + "self": [ + { + "href": "link" + } + ] + } + } + }, + "toRef": { + "id": "refs/heads/develop/trunk", + "displayId": "develop/trunk", + "latestCommit": "3", + "repository": { + "slug": "repo-slug", + "id": 242, + "name": "repo-name", + "scmId": "git", + "state": "AVAILABLE", + "statusMessage": "Available", + "forkable": true, + "project": { + "key": "project-key", + "id": 242, + "name": "project-name", + "description": "description", + "public": true, + "type": "NORMAL", + "links": { + "self": [ + { + "href": "link" + } + ] + } + }, + "public": false, + "links": { + "clone": [ + { + "href": "link", + "name": "ssh" + }, + { + "href": "link-http", + "name": "http" + } + ], + "self": [ + { + "href": "link-self" + } + ] + } + } + }, + "locked": false, + "author": { + "user": { + "name": "user-name", + "id": 1, + "displayName": "user-display-name", + "active": false, + "slug": "user-slug", + "type": "NORMAL", + "links": { + "self": [ + { + "href": "link-self" + } + ] + } + }, + "role": "AUTHOR", + "approved": false, + "status": "UNAPPROVED" + }, + "reviewers": [ + { + "user": { + "name": "reviewer-name", + "emailAddress": "reviewer@email.com", + "id": 1713, + "displayName": "reviewer-display-name", + "active": true, + "slug": "reviewer-slug", + "type": "NORMAL", + "links": { + "self": [ + { + "href": "link-self" + } + ] + } + }, + "lastReviewedCommit": "2", + "role": "REVIEWER", + "approved": true, + "status": "APPROVED" + }, + { + "user": { + "name": "reveiwer2-name", + "emailAddress": "reviewer2@email.com", + "id": 49, + "displayName": "reviewer2 display name", + "active": true, + "slug": "reviewer2-slug", + "type": "NORMAL", + "links": { + "self": [ + { + "href": "link-self" + } + ] + } + }, + "role": "REVIEWER", + "approved": false, + "status": "UNAPPROVED" + } + ], + "participants": [], + "properties": { + "mergeResult": { + "outcome": "CONFLICTED", + "current": true + }, + "resolvedTaskCount": 0, + "commentCount": 4, + "openTaskCount": 0 + }, + "links": { + "self": [ + { + "href": "link" + } + ] + } + }'. + + ^ pullRequest := (NeoJSONReader on: pullRequest readStream) next +] + +{ #category : #accessing } +BitBucketApiMock >> diffs [ + + ^ diffs +] + +{ #category : #accessing } +BitBucketApiMock >> diffs1 [ + + | diffs1 | + diffs1 := '{ + "fromHash": null, + "toHash": "123", + "contextLines": 10, + "whitespace": "SHOW", + "diffs": [ + { + "source": { + "components": [ + "build.gradle" + ], + "parent": "", + "name": "build.gradle", + "extension": "gradle", + "toString": "build.gradle" + }, + "destination": { + "components": [ + "build.gradle" + ], + "parent": "", + "name": "build.gradle", + "extension": "gradle", + "toString": "build.gradle" + }, + "hunks": [ + { + "sourceLine": 1, + "sourceSpan": 14, + "destinationLine": 1, + "destinationSpan": 14, + "segments": [ + { + "type": "CONTEXT", + "lines": [ + { + "source": 3, + "destination": 3, + "line": " line", + "truncated": false + } + ], + "truncated": false + }, + { + "type": "REMOVED", + "lines": [ + { + "source": 4, + "destination": 4, + "line": "line4", + "truncated": false + } + ], + "truncated": false + }, + { + "type": "ADDED", + "lines": [ + { + "source": 5, + "destination": 4, + "line": "line5", + "truncated": false + } + ], + "truncated": false + }, + { + "type": "CONTEXT", + "lines": [ + { + "source": 14, + "destination": 14, + "line": "", + "truncated": false + } + ], + "truncated": false + } + ], + "truncated": false + } + ], + "truncated": false + } + ], + "truncated": false +}'. + + ^ diffs1 := (NeoJSONReader on: diffs1 readStream) next +] + +{ #category : #accessing } +BitBucketApiMock >> diffs: anObject [ + + diffs := anObject +] + +{ #category : #'api - commits' } +BitBucketApiMock >> diffsOfCommit: commitID inRepo: repositorySlug inProject: projectKey [ + ^diffs +] + +{ #category : #accessing } +BitBucketApiMock >> diffsWithoutAdded [ + + | diffsWithoutAdded | + diffsWithoutAdded := '{ + "fromHash": null, + "toHash": "3", + "contextLines": 10, + "whitespace": "SHOW", + "diffs": [ + { + "source": { + "components": [ + "build.gradle" + ], + "parent": "", + "name": "build.gradle", + "extension": "gradle", + "toString": "build.gradle" + }, + "destination": { + "components": [ + "build.gradle" + ], + "parent": "", + "name": "build.gradle", + "extension": "gradle", + "toString": "build.gradle" + }, + "hunks": [ + { + "sourceLine": 1, + "sourceSpan": 14, + "destinationLine": 1, + "destinationSpan": 14, + "segments": [ + { + "type": "CONTEXT", + "lines": [ + { + "source": 1, + "destination": 1, + "line": "line1", + "truncated": false + }, + { + "source": 2, + "destination": 2, + "line": "line2", + "truncated": false + }, + { + "source": 3, + "destination": 3, + "line": "line3", + "truncated": false + } + ], + "truncated": false + }, + { + "type": "REMOVED", + "lines": [ + { + "source": 4, + "destination": 4, + "line": "line4", + "truncated": false + } + ], + "truncated": false + }, + { + "type": "CONTEXT", + "lines": [ + { + "source": 5, + "destination": 5, + "line": "", + "truncated": false + } + ], + "truncated": false + } + ], + "truncated": false + } + ], + "truncated": false + } + ], + "truncated": false +}'. + + ^ diffsWithoutAdded := (NeoJSONReader on: + diffsWithoutAdded readStream) next +] + +{ #category : #accessing } +BitBucketApiMock >> diffsWithoutHunks [ + + | diffsWithoutHunks | + diffsWithoutHunks := '{ + "fromHash": null, + "toHash": "2", + "contextLines": 10, + "whitespace": "SHOW", + "diffs": [ + { + "source": { + "components": [ + "build.gradle" + ], + "parent": "", + "name": "build.gradle", + "extension": "gradle", + "toString": "build.gradle" + }, + "destination": { + "components": [ + "build.gradle" + ], + "parent": "", + "name": "build.gradle", + "extension": "gradle", + "toString": "build.gradle" + }, + + "truncated": false + } + ], + "truncated": false +}'. + + ^ diffsWithoutHunks := (NeoJSONReader on: + diffsWithoutHunks readStream) next +] + +{ #category : #accessing } +BitBucketApiMock >> diffsWithoutRemoved [ + + | diffsWithoutRemoved | + diffsWithoutRemoved := '{ + "fromHash": null, + "toHash": "1", + "contextLines": 10, + "whitespace": "SHOW", + "diffs": [ + { + "source": { + "components": [ + "build.gradle" + ], + "parent": "", + "name": "build.gradle", + "extension": "gradle", + "toString": "build.gradle" + }, + "destination": { + "components": [ + "build.gradle" + ], + "parent": "", + "name": "build.gradle", + "extension": "gradle", + "toString": "build.gradle" + }, + "hunks": [ + { + "sourceLine": 1, + "sourceSpan": 14, + "destinationLine": 1, + "destinationSpan": 14, + "segments": [ + { + "type": "CONTEXT", + "lines": [ + { + "source": 3, + "destination": 3, + "line": " line", + "truncated": false + } + ], + "truncated": false + }, + { + "type": "ADDED", + "lines": [ + { + "source": 5, + "destination": 4, + "line": " test2", + "truncated": false + } + ], + "truncated": false + }, + { + "type": "CONTEXT", + "lines": [ + { + "source": 6, + "destination": 6, + "line": "", + "truncated": false + } + ], + "truncated": false + } + ], + "truncated": false + } + ], + "truncated": false + } + ], + "truncated": false +}'. + + ^ diffsWithoutRemoved := (NeoJSONReader on: + diffsWithoutRemoved readStream) next +] + +{ #category : #initialization } +BitBucketApiMock >> initialize [ + + commits := self commits1. + diffs := self diffs1. + userMock := self user1. + mergeRequests := { self openedMergeRequest } +] + +{ #category : #accessing } +BitBucketApiMock >> mergeRequests [ + + ^ mergeRequests +] + +{ #category : #accessing } +BitBucketApiMock >> mergeRequests: anObject [ + + mergeRequests := anObject +] + +{ #category : #accessing } +BitBucketApiMock >> mergedMergeRequest [ + + | pullRequest | + pullRequest := '{ + "id": 539, + "version": 10, + "title": "title", + "state": "MERGED", + "open": false, + "closed": true, + "createdDate": 1721396425473, + "updatedDate": 1721457513310, + "closedDate": 1721457513310, + "fromRef": { + "id": "refs/heads/wip/1", + "displayId": "wip/1", + "latestCommit": "2", + "repository": { + "slug": "repoSlug", + "id": 242, + "name": "repo-name", + "scmId": "git", + "state": "AVAILABLE", + "statusMessage": "Available", + "forkable": true, + "project": { + "key": "project-key", + "id": 242, + "name": "project-name", + "description": "project description", + "public": true, + "type": "NORMAL", + "links": { + "self": [ + { + "href": "link" + } + ] + } + }, + "public": false, + "links": { + "clone": [ + { + "href": "clone", + "name": "ssh" + }, + { + "href": "clone", + "name": "http" + } + ], + "self": [ + { + "href": "link" + } + ] + } + } + }, + "toRef": { + "id": "refs/heads/develop/trunk", + "displayId": "develop/trunk", + "latestCommit": "3", + "repository": { + "slug": "repo-slug", + "id": 242, + "name": "repo-name", + "scmId": "git", + "state": "AVAILABLE", + "statusMessage": "Available", + "forkable": true, + "project": { + "key": "project-key", + "id": 242, + "name": "project-name", + "description": "description", + "public": true, + "type": "NORMAL", + "links": { + "self": [ + { + "href": "link" + } + ] + } + }, + "public": false, + "links": { + "clone": [ + { + "href": "link", + "name": "ssh" + }, + { + "href": "link-http", + "name": "http" + } + ], + "self": [ + { + "href": "link-self" + } + ] + } + } + }, + "locked": false, + "author": { + "user": { + "name": "user-name", + "id": 1, + "displayName": "user-display-name", + "active": false, + "slug": "user-slug", + "type": "NORMAL", + "links": { + "self": [ + { + "href": "link-self" + } + ] + } + }, + "role": "AUTHOR", + "approved": false, + "status": "UNAPPROVED" + }, + "reviewers": [ + { + "user": { + "name": "reviewer-name", + "emailAddress": "reviewer@email.com", + "id": 1713, + "displayName": "reviewer-display-name", + "active": true, + "slug": "reviewer-slug", + "type": "NORMAL", + "links": { + "self": [ + { + "href": "link-self" + } + ] + } + }, + "lastReviewedCommit": "2", + "role": "REVIEWER", + "approved": true, + "status": "APPROVED" + }, + { + "user": { + "name": "reveiwer2-name", + "emailAddress": "reviewer2@email.com", + "id": 49, + "displayName": "reviewer2 display name", + "active": true, + "slug": "reviewer2-slug", + "type": "NORMAL", + "links": { + "self": [ + { + "href": "link-self" + } + ] + } + }, + "role": "REVIEWER", + "approved": false, + "status": "UNAPPROVED" + } + ], + "participants": [], + "properties": { + "mergeResult": { + "outcome": "CONFLICTED", + "current": true + }, + "resolvedTaskCount": 0, + "commentCount": 4, + "openTaskCount": 0 + }, + "links": { + "self": [ + { + "href": "link" + } + ] + } + }'. + + ^ pullRequest := (NeoJSONReader on: pullRequest readStream) next +] + +{ #category : #accessing } +BitBucketApiMock >> openedMergeRequest [ + + | pullRequest | + pullRequest := '{ + "id": 539, + "version": 10, + "title": "title", + "state": "OPEN", + "open": true, + "closed": false, + "createdDate": 1709726344893, + "updatedDate": 1709728944248, + "fromRef": { + "id": "refs/heads/wip/1", + "displayId": "wip/1", + "latestCommit": "2", + "repository": { + "slug": "repoSlug", + "id": 242, + "name": "repo-name", + "scmId": "git", + "state": "AVAILABLE", + "statusMessage": "Available", + "forkable": true, + "project": { + "key": "project-key", + "id": 242, + "name": "project-name", + "description": "project description", + "public": true, + "type": "NORMAL", + "links": { + "self": [ + { + "href": "link" + } + ] + } + }, + "public": false, + "links": { + "clone": [ + { + "href": "clone", + "name": "ssh" + }, + { + "href": "clone", + "name": "http" + } + ], + "self": [ + { + "href": "link" + } + ] + } + } + }, + "toRef": { + "id": "refs/heads/develop/trunk", + "displayId": "develop/trunk", + "latestCommit": "3", + "repository": { + "slug": "repo-slug", + "id": 242, + "name": "repo-name", + "scmId": "git", + "state": "AVAILABLE", + "statusMessage": "Available", + "forkable": true, + "project": { + "key": "project-key", + "id": 242, + "name": "project-name", + "description": "description", + "public": true, + "type": "NORMAL", + "links": { + "self": [ + { + "href": "link" + } + ] + } + }, + "public": false, + "links": { + "clone": [ + { + "href": "link", + "name": "ssh" + }, + { + "href": "link-http", + "name": "http" + } + ], + "self": [ + { + "href": "link-self" + } + ] + } + } + }, + "locked": false, + "author": { + "user": { + "name": "user-name", + "id": 1, + "displayName": "user-display-name", + "active": false, + "slug": "user-slug", + "type": "NORMAL", + "links": { + "self": [ + { + "href": "link-self" + } + ] + } + }, + "role": "AUTHOR", + "approved": false, + "status": "UNAPPROVED" + }, + "reviewers": [ + { + "user": { + "name": "reviewer-name", + "emailAddress": "reviewer@email.com", + "id": 1713, + "displayName": "reviewer-display-name", + "active": true, + "slug": "reviewer-slug", + "type": "NORMAL", + "links": { + "self": [ + { + "href": "link-self" + } + ] + } + }, + "lastReviewedCommit": "2", + "role": "REVIEWER", + "approved": true, + "status": "APPROVED" + }, + { + "user": { + "name": "reveiwer2-name", + "emailAddress": "reviewer2@email.com", + "id": 49, + "displayName": "reviewer2 display name", + "active": true, + "slug": "reviewer2-slug", + "type": "NORMAL", + "links": { + "self": [ + { + "href": "link-self" + } + ] + } + }, + "role": "REVIEWER", + "approved": false, + "status": "UNAPPROVED" + } + ], + "participants": [], + "properties": { + "mergeResult": { + "outcome": "CONFLICTED", + "current": true + }, + "resolvedTaskCount": 0, + "commentCount": 4, + "openTaskCount": 0 + }, + "links": { + "self": [ + { + "href": "link" + } + ] + } + }'. + + ^pullRequest := (NeoJSONReader on: pullRequest readStream) next. +] + +{ #category : #accessing } +BitBucketApiMock >> projects [ + + | projects | + projects := '[ + { + "key": "PRJ", + "id": 1, + "name": "My Cool Project", + "description": "The description for my cool project.", + "public": true, + "type": "NORMAL", + "links": { + "self": [{"href": "http://link/to/project"}] + } + } + ], +'. + + projects := (NeoJSONReader on: projects readStream) next. + + ^ projects +] + +{ #category : #accessing } +BitBucketApiMock >> pullRequestActivities [ + + | pullRequestActivities | + pullRequestActivities := ' + [{ + "id": 1, + "createdDate": 1720510446734, + "user": { + "name": "user-name", + "emailAddress": "user-name@email.com", + "id": 24, + "displayName": "user name", + "active": true, + "slug": "un", + "type": "NORMAL", + "links": { + "self": [ + { + "href": "link" + } + ] + } + }, + "action": "MERGED" +}]'. + + ^ pullRequestActivities := (NeoJSONReader on: + pullRequestActivities readStream) next +] + +{ #category : #'api - pull-requests' } +BitBucketApiMock >> pullRequestsOfRepo: repoSlug inProject: projectKey since: since until: until [ + + ^ mergeRequests +] + +{ #category : #'api - projects' } +BitBucketApiMock >> repositoriesOfProject: projectKey [ + + | repos | + repos := '[ + { + "slug": "my-repo", + "id": 1, + "name": "My repo", + "scmId": "git", + "state": "AVAILABLE", + "statusMessage": "Available", + "forkable": true, + "project": { + "key": "PRJ", + "id": 1, + "name": "My Cool Project", + "description": "The description for my cool project.", + "public": true, + "type": "NORMAL", + "links": { + "self": [{"href": "http://link/to/project"}] + } + }, + "public": true, + "links": { + "clone": [ + {"href": "ssh://git@/PRJ/my-repo.git", "name": "ssh"}, + {"href": "https:///scm/PRJ/my-repo.git", "name": "http"} + ], + "self": [{"href": "http://link/to/repository"}] + } + } + ],'. + +repos := (NeoJSONReader on: repos readStream) next. + +^repos +] + +{ #category : #accessing } +BitBucketApiMock >> user1 [ + + | user1 | + user1 := ' + { + "name": "test", + "emailAddress": "test@test.com", + "id": 1, + "displayName": "test test", + "active": true, + "slug": "test", + "type": "NORMAL", + "directoryName": "directory", + "deletable": false, + "lastAuthenticationTimestamp": 1727444943000, + "mutableDetails": false, + "mutableGroups": true, + "links": { + "self": [ + { + "href": "test.com" + } + ] + } + } + '. + + + ^ user1 := (NeoJSONReader on: user1 readStream) next +] + +{ #category : #accessing } +BitBucketApiMock >> user: accountId [ + + | user | + user := '{ + "type": "user", + "nickname": "evzijst", + "display_name": "Erik van Zijst", + "created_on": "12-04-2024", + "uuid": "{d301aafa-d676-4ee0-88be-962be7417567}" + }'. + + ^ user +] + +{ #category : #accessing } +BitBucketApiMock >> userMock [ + + ^ userMock +] + +{ #category : #accessing } +BitBucketApiMock >> userMock: anObject [ + + userMock := anObject +] + +{ #category : #'api - user' } +BitBucketApiMock >> usersByUsername: username [ + + userMock ifNil: [ ^Array new ] ifNotNil: [ ^{ userMock }] +] diff --git a/src/BitBucketHealth-Model-Importer-Tests/BitBucketModelImporterTest.class.st b/src/BitBucketHealth-Model-Importer-Tests/BitBucketModelImporterTest.class.st new file mode 100644 index 0000000..14aef61 --- /dev/null +++ b/src/BitBucketHealth-Model-Importer-Tests/BitBucketModelImporterTest.class.st @@ -0,0 +1,479 @@ +" +A BitBucketModelImporterTest is a test class for testing the behavior of BitBucketModelImporter +" +Class { + #name : #BitBucketModelImporterTest, + #superclass : #TestCase, + #category : #'BitBucketHealth-Model-Importer-Tests' +} + +{ #category : #tests } +BitBucketModelImporterTest >> testCompleteImportProject [ + "Given" + + | glphModel project bitBucketApi bitBucketImporter | + bitBucketApi := BitBucketApiMock new. + glphModel := GLPHEModel new name: 'test'. + + project := GLHProject new. + + glphModel add: project. + + bitBucketImporter := BitBucketModelImporter new + bitBucketApi: bitBucketApi; + glhModel: glphModel. + + "When" + project := bitBucketImporter completeImportProject: project. + + "Then" + self deny: project repository equals: nil. + self + assert: (bitBucketImporter glhModel allWithType: GLHRepository) size + equals: 1 +] + +{ #category : #tests } +BitBucketModelImporterTest >> testGetContributionFromDiffs [ + "Given" + + | bitBucketApi glphModel bitBucketImporter diffs contribution | + bitBucketApi := BitBucketApiMock new. + + glphModel := GLPHEModel new name: 'test'. + + bitBucketImporter := BitBucketModelImporter new + bitBucketApi: bitBucketApi; + glhModel: glphModel. + + diffs := bitBucketApi diffs. + + "When" + contribution := bitBucketImporter getContributionFromDiffs: (diffs at: #diffs). + + "Then" + self assert: (contribution at: #additions) equals: 1. + self assert: (contribution at: #deletions) equals: 1 +] + +{ #category : #tests } +BitBucketModelImporterTest >> testGetContributionFromDiffsWithoutAdded [ + "Given" + + | bitBucketApi glphModel bitBucketImporter diffs contribution | + bitBucketApi := BitBucketApiMock new. + + glphModel := GLPHEModel new name: 'test'. + + bitBucketImporter := BitBucketModelImporter new + bitBucketApi: bitBucketApi; + glhModel: glphModel. + + diffs := bitBucketApi diffsWithoutAdded. + bitBucketApi diffs: diffs. + + "When" + contribution := bitBucketImporter getContributionFromDiffs: + (diffs at: #diffs). + + "Then" + self assert: (contribution at: #additions) equals: 0. + self assert: (contribution at: #deletions) equals: 1 +] + +{ #category : #tests } +BitBucketModelImporterTest >> testGetContributionFromDiffsWithoutHunks [ + "Given" + + | bitBucketApi glphModel bitBucketImporter diffs contribution | + bitBucketApi := BitBucketApiMock new. + + glphModel := GLPHEModel new name: 'test'. + + bitBucketImporter := BitBucketModelImporter new + bitBucketApi: bitBucketApi; + glhModel: glphModel. + + diffs := bitBucketApi diffsWithoutHunks. + bitBucketApi diffs: diffs. + + "When" + contribution := bitBucketImporter getContributionFromDiffs: + (diffs at: #diffs). + + "Then" + self assert: (contribution at: #additions) equals: 0. + self assert: (contribution at: #deletions) equals: 0 +] + +{ #category : #tests } +BitBucketModelImporterTest >> testGetContributionFromDiffsWithoutRemoved [ + "Given" + + | bitBucketApi glphModel bitBucketImporter diffs contribution | + bitBucketApi := BitBucketApiMock new. + + glphModel := GLPHEModel new name: 'test'. + + bitBucketImporter := BitBucketModelImporter new + bitBucketApi: bitBucketApi; + glhModel: glphModel. + + diffs := bitBucketApi diffsWithoutRemoved. + bitBucketApi diffs: diffs. + + "When" + contribution := bitBucketImporter getContributionFromDiffs: + (diffs at: #diffs). + + "Then" + self assert: (contribution at: #additions) equals: 1. + self assert: (contribution at: #deletions) equals: 0 +] + +{ #category : #tests } +BitBucketModelImporterTest >> testImportCommitsOfProjectSinceUntil [ + + | bitBucketApi glphModel bitBucketImporter project commits group repo | + "Given" + bitBucketApi := BitBucketApiMock new. + + glphModel := GLPHEModel new name: 'test'. + + bitBucketImporter := BitBucketModelImporter new + bitBucketApi: bitBucketApi; + glhModel: glphModel. + + group := GLHGroup new id: 1. + repo := GLHRepository new. + project := GLHProject new + group: group; + repository: repo. + + "When" + commits := bitBucketImporter + importCommitsOfProject: project + since: '09-23-2024' + until: '09-25-2024'. + + "Then" + self + assert: (bitBucketImporter glhModel allWithType: GLHCommit) size + equals: 1. + + self assert: project repository commits size equals: 1. +] + +{ #category : #tests } +BitBucketModelImporterTest >> testImportContributedProjectsOfUser [ + + | bitBucketApi glphModel bitBucketImporter user projects commits yesterdayAsTimestamp | + bitBucketApi := BitBucketApiMock new. + + glphModel := GLPHEModel new name: 'test'. + + bitBucketImporter := BitBucketModelImporter new + bitBucketApi: bitBucketApi; + glhModel: glphModel. + + user := GLHUser new + id: 1; + username: 'charlie'. + + commits := bitBucketApi commits1. + yesterdayAsTimestamp := (Date today - 1 days) asDateAndTime + asUnixTime * 1000. + commits first at: #committerTimestamp put: yesterdayAsTimestamp. + bitBucketApi commits: commits. + + "When" + projects := bitBucketImporter importContributedProjectsOfUser: user. + + "Then" + self assert: projects size equals: 1. + self + assert: (bitBucketImporter glhModel allWithType: GLHProject) size + equals: 1. + self + assert: (bitBucketImporter glhModel allWithType: GLHProject) first + equals: projects first. + self + assertCollection: user contributedProjects + hasSameElements: projects +] + +{ #category : #tests } +BitBucketModelImporterTest >> testImportCreatorOfCommit [ + + | bitBucketApi glphModel bitBucketImporter creator commit | + + "Given" + bitBucketApi := BitBucketApiMock new. + + glphModel := GLPHEModel new name: 'test'. + + bitBucketImporter := BitBucketModelImporter new + bitBucketApi: bitBucketApi; + glhModel: glphModel. + + commit := GLHCommit new author_name: 'test'. + + + "When" + creator := bitBucketImporter importCreatorOfCommit: commit. + + "Then" + self + assert: creator id + equals: + (bitBucketImporter parseUserIntoGLHUser: bitBucketApi userMock) id. + self assert: commit commitCreator equals: creator +] + +{ #category : #tests } +BitBucketModelImporterTest >> testImportCreatorOfCommitIfAlreadyExist [ + + | bitBucketApi glphModel bitBucketImporter creator commit | + "Given" + bitBucketApi := BitBucketApiMock new. + + glphModel := GLPHEModel new name: 'test'. + + bitBucketImporter := BitBucketModelImporter new + bitBucketApi: bitBucketApi; + glhModel: glphModel. + + glphModel add: (bitBucketImporter parseUserIntoGLHUser: bitBucketApi userMock). + + commit := GLHCommit new author_name: 'test'. + + + "When" + creator := bitBucketImporter importCreatorOfCommit: commit. + + "Then" + self + assert: creator id + equals: + (bitBucketImporter parseUserIntoGLHUser: bitBucketApi userMock) id. + self assert: commit commitCreator equals: creator +] + +{ #category : #tests } +BitBucketModelImporterTest >> testImportMergeRequestCommits [ + + | bitBucketApi glphModel bitBucketImporter mergeRequest commits | + "Given" + bitBucketApi := BitBucketApiMock new. + + glphModel := GLPHEModel new name: 'test'. + + bitBucketImporter := BitBucketModelImporter new + bitBucketApi: bitBucketApi; + glhModel: glphModel. + + mergeRequest := bitBucketImporter + parsePullRequestIntoGLPHEMergeRequest: + bitBucketApi mergedMergeRequest. + + + "When" + commits := bitBucketImporter importMergeRequestCommits: mergeRequest. + + "Then" + self deny: mergeRequest commits equals: nil. + self assert: mergeRequest commits equals: commits +] + +{ #category : #tests } +BitBucketModelImporterTest >> testImportMergeRequestsSinceUntil [ + + | bitBucketApi glphModel bitBucketImporter group repo project mergeRequests mergeRequest | + "Given" + bitBucketApi := BitBucketApiMock new. + + glphModel := GLPHEModel new name: 'test'. + + bitBucketImporter := BitBucketModelImporter new + bitBucketApi: bitBucketApi; + glhModel: glphModel. + + group := GLHGroup new id: 1. + repo := GLHRepository new. + project := GLHProject new + group: group; + repository: repo. + + "When" + mergeRequests := bitBucketImporter + importMergeRequests: project + since: '09-23-2024' + until: '09-25-2024'. + + "Then" + self + assert: + (bitBucketImporter glhModel allWithType: GLPHEMergeRequest) size + equals: 1. + + self assert: mergeRequests size equals: 1. + + mergeRequest := mergeRequests first. + self assert: mergeRequest state equals: 'opened'. + self assert: mergeRequest closed_at equals: nil. +] + +{ #category : #tests } +BitBucketModelImporterTest >> testImportMergeRequestsSinceUntilClosedMR [ + + | bitBucketApi glphModel bitBucketImporter group repo project mergeRequests mergeRequest | + "Given" + bitBucketApi := BitBucketApiMock new. + + glphModel := GLPHEModel new name: 'test'. + + bitBucketImporter := BitBucketModelImporter new + bitBucketApi: bitBucketApi; + glhModel: glphModel. + + bitBucketApi mergeRequests: { bitBucketApi mergedMergeRequest }. + + group := GLHGroup new id: 1. + repo := GLHRepository new. + project := GLHProject new + group: group; + repository: repo. + + "When" + mergeRequests := bitBucketImporter + importMergeRequests: project + since: '09-23-2024' + until: '09-25-2024'. + + "Then" + self + assert: + (bitBucketImporter glhModel allWithType: GLPHEMergeRequest) size + equals: 1. + + self assert: mergeRequests size equals: 1. + + mergeRequest := mergeRequests first. + self assert: mergeRequest state equals: 'merged'. + self assert: mergeRequest closed_at equals: nil. + self deny: mergeRequest merged_at equals: nil. +] + +{ #category : #tests } +BitBucketModelImporterTest >> testImportMergeRequestsSinceUntilDeclinedMR [ + + | bitBucketApi glphModel bitBucketImporter group repo project mergeRequests mergeRequest | + "Given" + bitBucketApi := BitBucketApiMock new. + + glphModel := GLPHEModel new name: 'test'. + + bitBucketImporter := BitBucketModelImporter new + bitBucketApi: bitBucketApi; + glhModel: glphModel. + + bitBucketApi mergeRequests: { bitBucketApi declinedMergeRequest }. + + group := GLHGroup new id: 1. + repo := GLHRepository new. + project := GLHProject new + group: group; + repository: repo. + + "When" + mergeRequests := bitBucketImporter + importMergeRequests: project + since: '09-23-2024' + until: '09-25-2024'. + + "Then" + self + assert: + (bitBucketImporter glhModel allWithType: GLPHEMergeRequest) size + equals: 1. + + self assert: mergeRequests size equals: 1. + + mergeRequest := mergeRequests first. + self assert: mergeRequest state equals: 'closed'. + self deny: mergeRequest closed_at equals: nil. + self assert: mergeRequest merged_at equals: nil +] + +{ #category : #tests } +BitBucketModelImporterTest >> testImportMergeResquestMerger [ + + | bitBucketApi glphModel bitBucketImporter mergeRequest mergeUser | + "Given" + bitBucketApi := BitBucketApiMock new. + + glphModel := GLPHEModel new name: 'test'. + + bitBucketImporter := BitBucketModelImporter new + bitBucketApi: bitBucketApi; + glhModel: glphModel. + + mergeRequest := bitBucketImporter + parsePullRequestIntoGLPHEMergeRequest: + bitBucketApi mergedMergeRequest. + + + "When" + mergeUser := bitBucketImporter importMergeResquestMerger: + mergeRequest. + + "Then" + self deny: mergeRequest merge_user equals: nil. + self assert: mergeRequest merge_user equals: mergeUser +] + +{ #category : #tests } +BitBucketModelImporterTest >> testImportUserByUsername [ + "Given" + + | bitBucketApi glphModel bitBucketImporter user | + bitBucketApi := BitBucketApiMock new. + + glphModel := GLPHEModel new name: 'test'. + + bitBucketImporter := BitBucketModelImporter new + bitBucketApi: bitBucketApi; + glhModel: glphModel. + + "When" + user := bitBucketImporter importUserByUsername: 'test'. + + "Then" + self assert: (glphModel allWithType: GLHUser) size equals: 1. + self + assertCollection: (glphModel allWithType: GLHUser) + hasSameElements: {user}. +] + +{ #category : #tests } +BitBucketModelImporterTest >> testImportUserByUsernameWithoutUsers [ + "Given" + + | bitBucketApi glphModel bitBucketImporter user | + bitBucketApi := BitBucketApiMock new. + + glphModel := GLPHEModel new name: 'test'. + + bitBucketImporter := BitBucketModelImporter new + bitBucketApi: bitBucketApi; + glhModel: glphModel. + + bitBucketApi userMock: nil. + + "When" + user := bitBucketImporter importUserByUsername: 'test'. + + "Then" + self assert: (glphModel allWithType: GLHUser) size equals: 0. + self assert: user equals: nil +] diff --git a/src/BitBucketHealth-Model-Importer-Tests/package.st b/src/BitBucketHealth-Model-Importer-Tests/package.st new file mode 100644 index 0000000..b1f504f --- /dev/null +++ b/src/BitBucketHealth-Model-Importer-Tests/package.st @@ -0,0 +1 @@ +Package { #name : #'BitBucketHealth-Model-Importer-Tests' } diff --git a/src/BitBucketHealth-Model-Importer/BitBucketApi.class.st b/src/BitBucketHealth-Model-Importer/BitBucketApi.class.st new file mode 100644 index 0000000..b6a9726 --- /dev/null +++ b/src/BitBucketHealth-Model-Importer/BitBucketApi.class.st @@ -0,0 +1,272 @@ +Class { + #name : #BitBucketApi, + #superclass : #Object, + #instVars : [ + 'endpoint', + 'basePath', + 'client', + 'bearerToken', + 'apiToken', + 'username' + ], + #category : #'BitBucketHealth-Model-Importer' +} + +{ #category : #'api - pull-requests' } +BitBucketApi >> activitiesOfPullRequest: pullRequestId inRepo: repositorySlug ofProject: projectKey [ + + ^ self allValuesOfPath: + self basePath , '/projects/' , projectKey , '/repos/' + , repositorySlug , '/pull-requests/' , pullRequestId printString + , '/activities' +] + +{ #category : #'private - building' } +BitBucketApi >> allValuesOfPath: path [ + + | results values | + self prepareZnClient. + self client path: path. + values := OrderedCollection new. + + [ + results := self client get. + results := (NeoJSONReader on: results readStream) next. + values addAll: (results at: #values). + results + at: #nextPageStart + ifPresent: [ + client queryAt: #start put: (results at: #nextPageStart) ]. + results at: #isLastPage ] whileFalse. + + ^ values +] + +{ #category : #accessing } +BitBucketApi >> apiToken [ + + ^ apiToken +] + +{ #category : #accessing } +BitBucketApi >> apiToken: anObject [ + + apiToken := anObject +] + +{ #category : #accessing } +BitBucketApi >> basePath [ + + ^ basePath +] + +{ #category : #accessing } +BitBucketApi >> basePath: anObject [ + + basePath := anObject +] + +{ #category : #accessing } +BitBucketApi >> bearerToken [ + + ^ bearerToken +] + +{ #category : #accessing } +BitBucketApi >> bearerToken: anObject [ + + bearerToken := anObject +] + +{ #category : #accessing } +BitBucketApi >> client [ + + ^ client +] + +{ #category : #accessing } +BitBucketApi >> client: anObject [ + + client := anObject +] + +{ #category : #'api - pull-requests' } +BitBucketApi >> commitsOfPullRequest: pullRequestId ofRepo: repoSlug inProject: projectKey [ + ^ self allValuesOfPath: self basePath, '/projects/', projectKey, '/repos/', repoSlug, '/pull-requests/', pullRequestId printString, '/commits'. + +] + +{ #category : #'api - commits' } +BitBucketApi >> commitsOfRepo: repositorySlug inProject: projectKey since: since until: until [ + "/rest/api/1.0/projects/{projectKey}/repos/{repositorySlug}/commits" + + | results lastDate lastCommitterTimestamp commits | + self prepareZnClient. + self client path: + self basePath , '/projects/' , projectKey , '/repos/' + , repositorySlug , '/commits'. + self client queryAt: 'merges' put: 'exclude'. + + commits := OrderedCollection new. + + [ + results := self client get. + results := (NeoJSONReader on: results readStream) next. + commits addAll: (results at: #values). + results + at: #nextPageStart + ifPresent: [ + client queryAt: #start put: (results at: #nextPageStart) ]. + + (results at: #isLastPage) + ifTrue: [ false ] + ifFalse: [ + lastCommitterTimestamp := commits last at: #authorTimestamp. + lastDate := DateAndTime fromUnixTime: lastCommitterTimestamp / 1000. + since asDate <= lastDate ] ] whileTrue. + + ^ commits select: [ :commit | + | commitDate | + commitDate := DateAndTime fromUnixTime: + (commit at: #authorTimestamp) / 1000. + commitDate >= since asDateAndTime and: + commitDate <= until asDateAndTime ] +] + +{ #category : #'api - commits' } +BitBucketApi >> diffsOfCommit: commitID inRepo: repositorySlug inProject: projectKey [ + + | results | + self prepareZnClient. + + self client path: + self basePath , '/projects/' , projectKey , '/repos/' + , repositorySlug , '/commits/' , commitID , '/diff'. + + results := self client get. + + ^ (NeoJSONReader on: results readStream) next +] + +{ #category : #accessing } +BitBucketApi >> endpoint [ + + ^ endpoint +] + +{ #category : #accessing } +BitBucketApi >> endpoint: anObject [ + + endpoint := anObject +] + +{ #category : #initialization } +BitBucketApi >> initialize [ + + self client: (ZnClient new + accept: ZnMimeType applicationJson; + yourself). + + self basePath: 'rest/api/1.0' +] + +{ #category : #'private - building' } +BitBucketApi >> prepareZnClient [ + + client := ZnClient new + accept: ZnMimeType applicationJson; + yourself. + + client host: self endpoint. + client http. + + self bearerToken ifNotNil: [ :token | + client headerAt: #Authorization put: 'Bearer ' , token ]. + self apiToken ifNotNil: [ :anApiKey | + client headerAt: #Authorization put: 'Basic ' + , (self username , ':' , self apiToken) utf8Encoded base64Encoded ]. +] + +{ #category : #'api - projects' } +BitBucketApi >> projects [ + "/rest/api/1.0/projects" + + ^ self allValuesOfPath: self basePath , '/projects'. +] + +{ #category : #'api - pull-requests' } +BitBucketApi >> pullRequestsOfRepo: repositorySlug inProject: projectKey since: since until: until [ + + | pullRequests results lastCommitterTimestamp lastDate | + self prepareZnClient. + self client path: + self basePath , '/projects/' , projectKey , '/repos/' + , repositorySlug , '/pull-requests'. + + self client queryAt: 'state' put: 'all'. + + pullRequests := OrderedCollection new. + + [ + results := self client get. + results := (NeoJSONReader on: results readStream) next. + pullRequests addAll: (results at: #values). + results + at: #nextPageStart + ifPresent: [ + client queryAt: #start put: (results at: #nextPageStart) ]. + + (results at: #isLastPage) + ifTrue: [ false ] + ifFalse: [ + lastCommitterTimestamp := pullRequests last at: #createdDate. + lastDate := DateAndTime fromUnixTime: lastCommitterTimestamp / 1000. + since asDate <= lastDate ] ] whileTrue. + + ^ pullRequests select: [ :commit | + | commitDate | + commitDate := DateAndTime fromUnixTime: + (commit at: #createdDate) / 1000. + commitDate >= since asDate and: commitDate <= until asDate ] +] + +{ #category : #'api - projects' } +BitBucketApi >> repositoriesOfProject: projectKey [ + ^self allValuesOfPath: self basePath, '/projects/', projectKey, '/repos'. +] + +{ #category : #'api - user' } +BitBucketApi >> user: accountId [ + "https://developer.atlassian.com/cloud/bitbucket/rest/api-group-users/#api-user-get" + + self prepareZnClient. + + self client path: self basePath, '/user/' , accountId printString. + ^ self client get +] + +{ #category : #accessing } +BitBucketApi >> username [ + + ^ username +] + +{ #category : #accessing } +BitBucketApi >> username: anObject [ + + username := anObject +] + +{ #category : #'api - user' } +BitBucketApi >> usersByUsername: usernameString [ + + | users | + self prepareZnClient. + self client path: self basePath , '/admin/users'. + self client queryAt: 'filter' put: usernameString. + + users := self client get. + users := (NeoJSONReader on: users readStream) next. + + ^ (users at: #values) +] diff --git a/src/BitBucketHealth-Model-Importer/BitBucketModelImporter.class.st b/src/BitBucketHealth-Model-Importer/BitBucketModelImporter.class.st new file mode 100644 index 0000000..60ef88e --- /dev/null +++ b/src/BitBucketHealth-Model-Importer/BitBucketModelImporter.class.st @@ -0,0 +1,450 @@ +Class { + #name : #BitBucketModelImporter, + #superclass : #GPModelImporter, + #instVars : [ + 'bitBucketApi', + 'withCommitDiffs', + 'withInitialCommits', + 'userCatalogue' + ], + #category : #'BitBucketHealth-Model-Importer' +} + +{ #category : #accessing } +BitBucketModelImporter >> bitBucketApi [ + + ^ bitBucketApi +] + +{ #category : #accessing } +BitBucketModelImporter >> bitBucketApi: anObject [ + + bitBucketApi := anObject +] + +{ #category : #equality } +BitBucketModelImporter >> blockOnIdEquality [ + + ^ [ :existing :new | existing id = new id ] +] + +{ #category : #'private - api' } +BitBucketModelImporter >> completeImportProject: aGLHProject [ + + aGLHProject repository: GLHRepository new. + self glhModel add: aGLHProject repository. + "TODO: import repository" + ^ aGLHProject +] + +{ #category : #'private - api' } +BitBucketModelImporter >> getContributionFromDiffs: diffs [ + + | contribution | + contribution := { + ('additions' -> 0). + ('deletions' -> 0) } asDictionary. + + diffs do: [ :diff | + | hunks segments | + hunks := diff at: #hunks ifAbsent: Array new. + + hunks do: [ :hunk | + | addedSegment removedSegment | + segments := hunk at: #segments. + + addedSegment := segments + detect: [ :segment | + (segment at: #type) = 'ADDED' ] + ifNone: [ nil ]. + removedSegment := segments + detect: [ :segment | + (segment at: #type) = 'REMOVED' ] + ifNone: [ nil ]. + + addedSegment ifNotNil: [ + contribution + at: #additions + put: + (contribution at: #additions) + (addedSegment at: #lines) size ]. + removedSegment ifNotNil: [ + contribution + at: #deletions + put: + (contribution at: #deletions) + (removedSegment at: #lines) size ] ] ]. + + ^ contribution +] + +{ #category : #'import - commits' } +BitBucketModelImporter >> importCommitsOfProject: aGLHProject since: since until: until [ + + | commits | + commits := bitBucketApi + commitsOfRepo: aGLHProject id + inProject: aGLHProject group id + since: since + until: until. + + commits := commits collect: [ :commit | + | glhCommit commitDiffs contribution | + glhCommit := self parseCommitIntoGLHCommit: commit. + commitDiffs := self bitBucketApi + diffsOfCommit: glhCommit id + inRepo: aGLHProject id + inProject: aGLHProject group id. + + contribution := self getContributionFromDiffs: + (commitDiffs at: #diffs). + glhCommit additions: (contribution at: #additions). + glhCommit deletions: (contribution at: #deletions). + glhCommit ]. + + aGLHProject repository commits: commits. + self glhModel addAll: commits unless: self blockOnIdEquality. + + ^ commits +] + +{ #category : #'import - projects' } +BitBucketModelImporter >> importContributedProjectsOfUser: aGLHUser [ + + | projects repositories repositoriesCommits userRepositories userProjects | + "get all projects" + projects := self bitBucketApi projects. + + "get all repos of projects" + repositories := projects flatCollect: [ :project | + self bitBucketApi repositoriesOfProject: + (project at: #key) ]. + + + "get all commits of repo" + repositoriesCommits := repositories collect: [ :repository | + repository -> (self bitBucketApi + commitsOfRepo: (repository at: #slug) + inProject: + ((repository at: #project) at: #key) + since: DateAndTime now - 10 days + until: DateAndTime now) ]. + + + "look if user is author of min one commit" + userRepositories := repositoriesCommits select: [ :repository | + | repos | + repos := repository value + ifEmpty: [ false ] + ifNotEmpty: [ + repository value + detect: [ :commit | + ((commit at: #author) at: #name) + = aGLHUser username ] + ifFound: [ true ] + ifNone: [ false ] ] ]. + + + "Transform user repositories in GLHProject" + userProjects := userRepositories collect: [ :repoCommits | + | repo project | + repo := repoCommits key. + project := repo at: #project. + + (self glhModel allWithType: GLHProject) + detect: [ :glhProject | + glhProject id = (project at: #key) ] + ifFound: [ :glhProject | glhProject ] + ifNone: [ + | glhProject | + glhProject := self parseRepoIntoGLHProject: repo. + glhModel add: glhProject. + glhProject ] ]. + + aGLHUser contributedProjects: userProjects. + + ^ userProjects +] + +{ #category : #'import - commits' } +BitBucketModelImporter >> importCreatorOfCommit: aGLHCommit [ + + | creator | + (self glhModel allWithType: GLHUser) + detect: [ :user | user username = aGLHCommit author_name ] + ifFound: [ :user | + aGLHCommit commitCreator: user. + ^ user ]. + + creator := self importUserByUsername: aGLHCommit author_name. + aGLHCommit commitCreator: creator. + ^ creator +] + +{ #category : #'import - merge-requests' } +BitBucketModelImporter >> importMergeRequestCommits: mergeRequest [ + + | commits | + commits := self bitBucketApi + commitsOfPullRequest: mergeRequest id + ofRepo: mergeRequest project id + inProject: mergeRequest project group id. + + commits := commits collect: [ :commit | + self parseCommitIntoGLHCommit: commit ]. + + mergeRequest commits: commits. + + ^ commits +] + +{ #category : #'import - merge-requests' } +BitBucketModelImporter >> importMergeRequests: aGLHProject since: fromDate until: toDate [ + + | pullRequests | + pullRequests := bitBucketApi + pullRequestsOfRepo: aGLHProject id + inProject: aGLHProject group id + since: fromDate + until: toDate. + + pullRequests := pullRequests collect: [ :pullRequest | + self parsePullRequestIntoGLPHEMergeRequest: + pullRequest ]. + + self glhModel addAll: pullRequests unless: self blockOnIdEquality. + + ^ pullRequests +] + +{ #category : #'import - merge-requests' } +BitBucketModelImporter >> importMergeResquestAuthor: mergeRequest [ + mergeRequest author ifNotNil: [ ^mergeRequest ] +] + +{ #category : #'import - merge-requests' } +BitBucketModelImporter >> importMergeResquestMerger: mergeRequest [ + + | activities mergeActivity mergeUser | + mergeRequest merge_user ifNotNil: [ ^ mergeRequest merge_user ]. + mergeRequest state = 'merged' ifFalse: [ ^ nil ]. + + activities := self bitBucketApi + activitiesOfPullRequest: mergeRequest id + inRepo: mergeRequest project id + ofProject: mergeRequest project group id. + + mergeActivity := activities detect: [ :activity | + (activity at: #action) = 'MERGED' ]. + + mergeUser := mergeActivity at: #user. + + mergeUser := (glhModel allWithType: GLHUser) + detect: [ :user | user id = (mergeUser at: #id) ] + ifFound: [ :user | user ] + ifNone: [ + | glhUser | + glhUser := self parseUserIntoGLHUser: mergeUser. + glhModel add: glhUser. + glhUser ]. + + mergeRequest merge_user: mergeUser. + ^ mergeUser +] + +{ #category : #'import - users' } +BitBucketModelImporter >> importUserByUsername: username [ + + | users user glhUser | + users := self bitBucketApi usersByUsername: username. + + users ifEmpty: [ ^nil ]. + + user := users first. + + glhUser := self parseUserIntoGLHUser: user. + + self glhModel add: glhUser unless: [ :nu :ou | nu id = ou id ]. + ^ glhUser +] + +{ #category : #parsing } +BitBucketModelImporter >> parseCommitIntoGLHCommit: commitDictionary [ + + | author committer parentIds | + author := commitDictionary at: #author. + committer := commitDictionary at: #committer. + + parentIds := (commitDictionary at: #parents) collect: [ :parent | + parent at: #id ]. + + ^ GLHCommit new + id: (commitDictionary at: #id); + message: (commitDictionary at: #message); + author_email: (author at: #emailAddress); + author_name: (author at: #name); + authored_date: (DateAndTime fromUnixTime: + (commitDictionary at: #authorTimestamp) / 1000); + created_at: (DateAndTime fromUnixTime: + (commitDictionary at: #authorTimestamp) / 1000); + committed_date: (DateAndTime fromUnixTime: + (commitDictionary at: #committerTimestamp) / 1000); + committer_email: (committer at: #emailAddress); + committer_name: (committer at: #name); + parent_ids: parentIds +] + +{ #category : #parsing } +BitBucketModelImporter >> parseProjectIntoGLHGroup: projectRepository [ + + ^GLHGroup new + name: (projectRepository at: #name); + id: (projectRepository at: #key); + description: (projectRepository at: #description). +] + +{ #category : #parsing } +BitBucketModelImporter >> parsePullRequestIntoGLPHEMergeRequest: pullRequestDictionary [ + + | repository project toRef fromRef glpheMergeRequest author state reviewers | + toRef := pullRequestDictionary at: #toRef. + fromRef := pullRequestDictionary at: #fromRef. + + reviewers := pullRequestDictionary at: #reviewers. + reviewers := reviewers collect: [ :reviewer | + |reviewerUser| + reviewerUser := reviewer at: #user. + (self glhModel allWithType: GLHUser) detect: [ :user | user id = (reviewerUser at: #id) ] ifFound: [ :user | user ] ifNone: [ + |glhUser| + glhUser := self parseUserIntoGLHUser: reviewerUser. + glhModel add: glhUser. + glhUser. + ] + + ]. + + repository := toRef at: #repository. + project := (self glhModel allWithType: GLHProject) + detect: [ :glhProject | + glhProject id = (repository at: #id) ] + ifFound: [ :glhProject | glhProject ] + ifNone: [ + project := self parseRepoIntoGLHProject: repository. + self glhModel add: project. + project ]. + + + author := pullRequestDictionary at: #author. + author := (self glhModel allWithType: GLHUser) + detect: [ :user | user id = ((author at: #user) at: #id) ] + ifFound: [ :user | user ] + ifNone: [ + self importUserByUsername: + ((author at: #user) at: #displayName) ]. + + + glpheMergeRequest := GLPHEMergeRequest new + name: (pullRequestDictionary at: #title); + title: (pullRequestDictionary at: #title); + id: (pullRequestDictionary at: #id); + project: project; + project_id: project id; + target_branch: (toRef at: #id); + target_project_id: + ((toRef at: #repository) at: #id); + source_branch: (fromRef at: #id); + target_project_id: + ((fromRef at: #repository) at: #id); + updated_at: (DateAndTime fromUnixTime: + (pullRequestDictionary at: #updatedDate) + / 1000); + created_at: (DateAndTime fromUnixTime: + (pullRequestDictionary at: #createdDate) + / 1000); + author: author. + + "STATE" + state := pullRequestDictionary at: #state. + state = 'OPEN' ifTrue: [ glpheMergeRequest state: 'opened' ]. + state = 'MERGED' ifTrue: [ + glpheMergeRequest state: 'merged'. + glpheMergeRequest merged_at: (DateAndTime fromUnixTime: + (pullRequestDictionary at: #closedDate) / 1000) ]. + + state = 'DECLINED' ifTrue: [ + glpheMergeRequest state: 'closed'. + glpheMergeRequest closed_at: (DateAndTime fromUnixTime: + (pullRequestDictionary at: #closedDate) / 1000) ]. + + ^ glpheMergeRequest +] + +{ #category : #parsing } +BitBucketModelImporter >> parseRepoIntoGLHProject: repositoryDictionary [ + + | project group glhProject | + project := repositoryDictionary at: #project. + + group := (self glhModel allWithType: GLHGroup) + detect: [ :glhGroup | glhGroup id = (project at: #key) ] + ifFound: [ :glhGroup | glhGroup ] + ifNone: [ + | newGroup | + newGroup := self parseProjectIntoGLHGroup: project. + glhModel add: newGroup. + newGroup ]. + + + glhProject := GLHProject new + name: (repositoryDictionary at: #name); + id: (repositoryDictionary at: #slug); + repository: GLHRepository new; + group: group. + + group addProject: glhProject. + + ^ glhProject +] + +{ #category : #parsing } +BitBucketModelImporter >> parseUserIntoGLHUser: userDictionnary [ + + ^ GLHUser new name: (userDictionnary at: #displayName); + public_email: (userDictionnary at: #emailAddress); + id: (userDictionnary at: #id); + username: (userDictionnary at: #name). +] + +{ #category : #accessing } +BitBucketModelImporter >> userCatalogue [ + + ^ userCatalogue +] + +{ #category : #accessing } +BitBucketModelImporter >> userCatalogue: anObject [ + + userCatalogue := anObject +] + +{ #category : #accessing } +BitBucketModelImporter >> withCommitDiffs [ + + ^ withCommitDiffs +] + +{ #category : #accessing } +BitBucketModelImporter >> withCommitDiffs: anObject [ + + withCommitDiffs := anObject +] + +{ #category : #accessing } +BitBucketModelImporter >> withInitialCommits [ + + ^ withInitialCommits +] + +{ #category : #accessing } +BitBucketModelImporter >> withInitialCommits: anObject [ + + withInitialCommits := anObject +] diff --git a/src/BitBucketHealth-Model-Importer/package.st b/src/BitBucketHealth-Model-Importer/package.st new file mode 100644 index 0000000..288298e --- /dev/null +++ b/src/BitBucketHealth-Model-Importer/package.st @@ -0,0 +1 @@ +Package { #name : #'BitBucketHealth-Model-Importer' } diff --git a/src/GitLabHealth-Model-Analysis-Tests/GLPHImporterMock.class.st b/src/GitLabHealth-Model-Analysis-Tests/GLPHImporterMock.class.st index 6aafd99..0a63a93 100644 --- a/src/GitLabHealth-Model-Analysis-Tests/GLPHImporterMock.class.st +++ b/src/GitLabHealth-Model-Analysis-Tests/GLPHImporterMock.class.st @@ -46,7 +46,7 @@ GLPHImporterMock >> glhModel: anObject [ ] { #category : #commit } -GLPHImporterMock >> importCommitsOProject: project since: since until: until [ +GLPHImporterMock >> importCommitsOfProject: project since: since until: until [ glhModel addAll: commits. ^ commits diff --git a/src/GitLabHealth-Model-Analysis-Tests/GitAnalyzerTest.class.st b/src/GitLabHealth-Model-Analysis-Tests/GitAnalyzerTest.class.st index 9f12e0e..142d61b 100644 --- a/src/GitLabHealth-Model-Analysis-Tests/GitAnalyzerTest.class.st +++ b/src/GitLabHealth-Model-Analysis-Tests/GitAnalyzerTest.class.st @@ -42,7 +42,7 @@ GitAnalyzerTest >> setUp [ glhImporter - importCommitsOProject: projects first + importCommitsOfProject: projects first since: since asDate until: nil. diff --git a/src/GitLabHealth-Model-Analysis/GitAnalyzer.class.st b/src/GitLabHealth-Model-Analysis/GitAnalyzer.class.st index fc0ded4..ffbbbff 100644 --- a/src/GitLabHealth-Model-Analysis/GitAnalyzer.class.st +++ b/src/GitLabHealth-Model-Analysis/GitAnalyzer.class.st @@ -100,7 +100,7 @@ GitAnalyzer >> analyseCommitFrequencySince: since until: until [ (#frequency -> nil) } asDictionary. commits := glhImporter - importCommitsOProject: onProject + importCommitsOfProject: onProject since: since until: until. diff --git a/src/GitLabHealth-Model-Analysis/GitMetricExporter.class.st b/src/GitLabHealth-Model-Analysis/GitMetricExporter.class.st index 126f7ef..e0f79fc 100644 --- a/src/GitLabHealth-Model-Analysis/GitMetricExporter.class.st +++ b/src/GitLabHealth-Model-Analysis/GitMetricExporter.class.st @@ -517,15 +517,15 @@ GitMetricExporter >> setupAnalysisForUsersWithNames: userNames [ users := userNames collect: [ :username | glhImporter importUserByUsername: username ]. - glhImporter userCatalogue scrapeAuthorNamesForUsers: users. - + glhImporter userCatalogue ifNotNil: [ + glhImporter userCatalogue scrapeAuthorNamesForUsers: users ]. + users do: [ :user | - |projects | + | projects | projects := glhImporter importContributedProjectsOfUser: user. - projects do: [ :project | - glhImporter completeImportProject: project ]. - - ]. + + projects do: [ :project | + glhImporter completeImportProject: project ] ]. entities addAll: users. diff --git a/src/GitLabHealth-Model-Analysis/MergeRequestDurationMetric.class.st b/src/GitLabHealth-Model-Analysis/MergeRequestDurationMetric.class.st index d038eac..609b404 100644 --- a/src/GitLabHealth-Model-Analysis/MergeRequestDurationMetric.class.st +++ b/src/GitLabHealth-Model-Analysis/MergeRequestDurationMetric.class.st @@ -9,6 +9,7 @@ MergeRequestDurationMetric >> calculate [ | groupedByDate gitAnalyzer mergeRequestsValidation filterGroups | userMergeRequests ifNil: [ self load ]. + groupedByDate := self setupGroupedDate. userMergeRequests ifEmpty: [ ^ nil ]. diff --git a/src/GitLabHealth-Model-Analysis/UserMergeRequestMetric.class.st b/src/GitLabHealth-Model-Analysis/UserMergeRequestMetric.class.st index d635858..83a07b9 100644 --- a/src/GitLabHealth-Model-Analysis/UserMergeRequestMetric.class.st +++ b/src/GitLabHealth-Model-Analysis/UserMergeRequestMetric.class.st @@ -21,8 +21,7 @@ UserMergeRequestMetric >> load [ userMergeRequests := self loadUserMergeRequestsSince: (period at: #since) - until: (period at: #until) - + until: (period at: #until). ] { #category : #accessing } diff --git a/src/GitLabHealth-Model-Analysis/UserMetric.class.st b/src/GitLabHealth-Model-Analysis/UserMetric.class.st index ecd36c0..c420eec 100644 --- a/src/GitLabHealth-Model-Analysis/UserMetric.class.st +++ b/src/GitLabHealth-Model-Analysis/UserMetric.class.st @@ -148,7 +148,7 @@ UserMetric >> loadUserCommitsSince: since until: until [ ifAbsentPut: [ | foundCommits | foundCommits := glhImporter - importCommitsOProject: project + importCommitsOfProject: project since: since until: until. foundCommits ] ]. @@ -158,8 +158,7 @@ UserMetric >> loadUserCommitsSince: since until: until [ glhImporter chainsCommitsFrom: allCommits. glhImporter withCommitDiffs: true. - ^allCommits reject: [ :commit | - commit commitCreator ~= user ] + ^ allCommits reject: [ :commit | commit commitCreator ~= user ] ] { #category : #loading } diff --git a/src/GitLabHealth-Model-Importer/GLHModelImporter.class.st b/src/GitLabHealth-Model-Importer/GLHModelImporter.class.st index 18bfeeb..29adf0c 100644 --- a/src/GitLabHealth-Model-Importer/GLHModelImporter.class.st +++ b/src/GitLabHealth-Model-Importer/GLHModelImporter.class.st @@ -398,45 +398,6 @@ GLHModelImporter >> importCommitsFollowing: aCommit upToDays: aNumberOfDay [ until: (date + aNumberOfDay day) ] -{ #category : #commit } -GLHModelImporter >> importCommitsOProject: aProject since: fromDate until: toDate [ - - | newlyFoundCommit page foundCommit | - page := 0. - foundCommit := OrderedCollection new. - newlyFoundCommit := { true }. - [ newlyFoundCommit isNotEmpty ] whileTrue: [ - | results | - page := page + 1. - ('import commit page ' , page printString) recordInfo. - results := self glhApi - commitsOfProject: aProject id - forRefName: nil - since: - (fromDate ifNotNil: [ fromDate asDateAndTime asString ]) - until: - (toDate ifNotNil: [ toDate asDateAndTime asString ]) - path: nil - author: nil - all: true - with_stats: true - firstParent: nil - order: nil - trailers: nil - perPage: 100 - page: page. - - newlyFoundCommit := self parseCommitsResult: results. - "newlyFoundCommit do: [ :c | c repository: aProject repository ]." - - foundCommit addAll: (aProject repository commits - addAll: newlyFoundCommit - unless: self blockOnIdEquality). ]. - - - ^ self glhModel addAll: foundCommit unless: self blockOnIdEquality -] - { #category : #commit } GLHModelImporter >> importCommitsOf: aGLHProject withStats: aBoolean until: toDate [ @@ -547,6 +508,45 @@ GLHModelImporter >> importCommitsOfBranch: aGLHBranch forRefName: refName until: until: toDate ] +{ #category : #commit } +GLHModelImporter >> importCommitsOfProject: aProject since: fromDate until: toDate [ + + | newlyFoundCommit page foundCommit | + page := 0. + foundCommit := OrderedCollection new. + newlyFoundCommit := { true }. + [ newlyFoundCommit isNotEmpty ] whileTrue: [ + | results | + page := page + 1. + ('import commit page ' , page printString) recordInfo. + results := self glhApi + commitsOfProject: aProject id + forRefName: nil + since: + (fromDate ifNotNil: [ fromDate asDateAndTime asString ]) + until: + (toDate ifNotNil: [ toDate asDateAndTime asString ]) + path: nil + author: nil + all: true + with_stats: true + firstParent: nil + order: nil + trailers: nil + perPage: 100 + page: page. + + newlyFoundCommit := self parseCommitsResult: results. + "newlyFoundCommit do: [ :c | c repository: aProject repository ]." + + foundCommit addAll: (aProject repository commits + addAll: newlyFoundCommit + unless: self blockOnIdEquality). ]. + + + ^ self glhModel addAll: foundCommit unless: self blockOnIdEquality +] + { #category : #'as yet unclassified' } GLHModelImporter >> importContributedProjectsOfUser: aGLHUser [