Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/175 jira mining #243

Open
wants to merge 108 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
108 commits
Select commit Hold shift + click to select a range
5c0721b
Updated version of node-sass dependency (only working version with no…
martan001 Nov 11, 2023
0104fa5
Added initial parts of Jira ITS mining
martan001 Nov 11, 2023
8d28276
Updated version of node-sass dependency (only working version with no…
martan001 Nov 11, 2023
d904efe
Added initial parts of Jira ITS mining
martan001 Nov 11, 2023
3ab3133
Merge remote-tracking branch 'origin/feature/175-jira-mining' into fe…
martan001 Nov 15, 2023
09d57a7
Changed version of sass module back due to incompatibilities
martan001 Nov 19, 2023
4a51810
Rewritten to Typescript and fixed the request method
martan001 Nov 19, 2023
f290310
Rewritten in Typescript and added first parts of indexing
martan001 Nov 19, 2023
fe83588
Added paginated getComments method and fixed paginatedRequest to diff…
martan001 Nov 22, 2023
801d9c2
Added functionality to get issues of a project and to process the com…
martan001 Nov 22, 2023
dc76d18
Added functionality to get versions of a project and to get developme…
martan001 Nov 24, 2023
30258dc
Added functionality to index projectversions and persist them and fix…
martan001 Nov 24, 2023
8496197
Changed parts of paginator to support pagination using startAt index …
martan001 Nov 24, 2023
a022e9a
Added indexing of merge request information from issue
martan001 Nov 24, 2023
4156bb5
Changed implementation of indexing merge requests. Moved it to the pa…
martan001 Nov 24, 2023
06dc145
Added JiraITSIndexer to the used indexers
martan001 Nov 24, 2023
f5744a9
Added functionality to get information about merge requests linked in…
martan001 Nov 24, 2023
e091d86
Fixed some invalid names of fields stored in DB
martan001 Dec 5, 2023
8a08332
Changed implementation to use project key specified in config file
martan001 Dec 7, 2023
cf1f2b1
Added hack to make
martan001 Dec 7, 2023
65b549c
Added additional fields to persist of an issue.
martan001 Jan 3, 2024
e78f934
Improved logging.
martan001 Jan 3, 2024
6922f46
Fixed an issue that caused information of merge requests not being pe…
martan001 Jan 3, 2024
c81c427
Changed the structure of the saved milestone object in the DB and cha…
martan001 Jan 5, 2024
5f1cac3
Updated some fields that are persisted for merge requests and issues …
martan001 Jan 5, 2024
9f01831
Changed to save the correct description for merge requests and update…
martan001 Jan 6, 2024
fabd70c
Removed code parts for persisting merge requests due to problems with…
martan001 Jan 17, 2024
23e927a
Changed condition because of different approach for pagination in Jir…
martan001 Jan 19, 2024
3e4fbbf
Changed getMergeRequest to make requests in parallel and wrapped meth…
martan001 Jan 19, 2024
3e1c073
Fixed issue where issues and merge requests were not saved correctly.…
martan001 Jan 19, 2024
c02a0e9
Removed unused JiraUrlProvider.js
martan001 Jan 19, 2024
4a45287
Fixed a bug that caused returning wrong merge request with findOneByI…
martan001 Jan 19, 2024
a045555
#175 Fixed a bug that caused to exit the index method too early witho…
martan001 Jan 24, 2024
899cf62
#175 Improved performance of processing comments by checking if addit…
martan001 Jan 26, 2024
a18c726
#175 Changed functionality how issues and merge requests are being pr…
martan001 Jan 27, 2024
de8dedd
#175 Updated implementation of getMergeRequest method.
martan001 Jan 27, 2024
120e4c0
#175 Improved logging and made methods only used in JiraITSIndexer.ts…
martan001 Jan 27, 2024
6a06ed5
#175 Added information what fields are not available in Jira and chan…
martan001 Jan 27, 2024
6922c46
#175 Fixed processing of description and mentioned users in comments …
martan001 Jan 27, 2024
ba02ada
#175 Fixed a bug that caused an error when the description of an issu…
martan001 Jan 28, 2024
848a92f
#175 Set default timeout value for requests.
martan001 Jan 28, 2024
cba83c9
#175 Added a check for optional values since if value was not found i…
martan001 Jan 28, 2024
92d89ef
#175 Included to persist now the full comment objects of an issue add…
martan001 Feb 3, 2024
0023273
#175 Added check for response status code.
martan001 Feb 3, 2024
b7e11a4
#175 Added functionality to get worklog information containing time s…
martan001 Feb 3, 2024
b981548
#175 Fixed an error that caused the frontend to not load because of d…
martan001 Feb 3, 2024
3e2681e
#175 Added support to retrieve changelog of issue if needed to get ed…
martan001 Feb 3, 2024
a85d236
#175 Changed when retrieving changelog information to only get inform…
martan001 Feb 4, 2024
2c88909
#175 Added jira to setupConfig.js.
martan001 Feb 4, 2024
898cace
#175 Fixed a bug that caused versions to be updated every time.
martan001 Feb 4, 2024
9fab525
#175 Fixed a bug that caused an error when no pull request informatio…
martan001 Feb 4, 2024
656ba32
#175 Added missing checks when field of version is undefined that led…
martan001 Feb 5, 2024
28ce38a
#175 Changes in implementation to modify Jira worklog objects to be a…
martan001 Feb 7, 2024
85cf8e0
#175 Added method that change user objects to be almost equal to user…
martan001 Feb 7, 2024
a9b7072
#175 Further changes to key names of user object.
martan001 Feb 7, 2024
057e3eb
#175 Changed implementation of retrieving pullrequest information sin…
martan001 Feb 7, 2024
00a5254
#175 Added missing url-join declaration
martan001 Feb 12, 2024
3a75bb0
#175 Simplified check.
martan001 Feb 12, 2024
091161f
#175 Added info how long index method needs.
martan001 Feb 12, 2024
ba419bb
#175 Changes to persisted issue fields.
martan001 Feb 12, 2024
e9d6a75
#175 Added types.
martan001 Feb 12, 2024
c84cddf
#175 Additional types added and checks made.
martan001 Feb 12, 2024
eb3f568
#175 Additional fields added to persisted issue and further improveme…
martan001 Feb 14, 2024
b0065d1
#175 Fixed a bug that caused the issue impact query to not work due t…
martan001 Feb 14, 2024
970de04
#175 Commented code and added own gitlab project for testing
martan001 Feb 17, 2024
39254ea
#175 Type improvements and refactoring of method to get detailed deve…
martan001 Feb 17, 2024
7743624
#175 Further improvements to types and refactored method to get detai…
martan001 Feb 18, 2024
13e0c47
#175 Removed unused method after changing API version to 2 since this…
martan001 Feb 18, 2024
e28b998
#175 Refactoring: Renaming parameters, variables, simplifying conditi…
martan001 Feb 18, 2024
44b40d2
#175 Further type improvements.
martan001 Feb 18, 2024
b7b76d1
#175 Fixed a bug that occurred after refactoring which caused the pro…
martan001 Feb 18, 2024
e32409e
#175 Removed useless if statement since it is checked in the method t…
martan001 Feb 18, 2024
60bae36
#175 Removed methods which are no longer needed since API v2 returns …
martan001 Feb 18, 2024
87b1344
#175 Added missing value type for JiraVersion type.
martan001 Feb 18, 2024
4095172
#175 Fixed a bug that caused the program to fail when development dat…
martan001 Feb 18, 2024
7904a09
#175 Fixed a bug that caused timetracking information to be persisted…
martan001 Feb 21, 2024
f39ad11
#175 Fixed a bug caused by non-unique iid values for persisted projec…
martan001 Feb 21, 2024
48c651e
#175 Refactoring of variable names.
martan001 Feb 21, 2024
93a3324
#175 Refactoring of variable names, changing comments.
martan001 Feb 21, 2024
396f5c0
#175 Added the original worklogs array to the persisted fields and fi…
martan001 Feb 21, 2024
d163701
#175 Removed some comments, renamed variables and changed some variab…
martan001 Feb 21, 2024
6ecb241
#175 Renaming of type names.
martan001 Feb 21, 2024
d4d0306
#175 Fixed a bug that caused to only persist commits mentioned in an …
martan001 Feb 21, 2024
198c485
#175 Renaming of types.
martan001 Feb 21, 2024
0706ce0
#175 Removed from "Mentions" type unused fields and renamed files.
martan001 Feb 21, 2024
bd918d1
#175 Changed the id of the milestone object and the mergerequest obje…
martan001 Feb 21, 2024
c4bd384
#175 Changed author.login to save the displayname and author.name to …
martan001 Feb 23, 2024
c5fbbb8
#175 Changed some merge request fields and fixed a bug when searching…
martan001 Feb 25, 2024
82b52c6
#175 Made check for Jira ITS more precise and removed debug logs
martan001 Apr 13, 2024
801f4a5
#175 Extended config and added necessary types to implement functiona…
martan001 Apr 13, 2024
1be3c63
#175 Added functionality to fetch team members if they are assigned t…
martan001 Apr 13, 2024
e8566be
#175 Config type extended for new functionality to persist members of…
martan001 Apr 13, 2024
559e43f
#175 Added jql config param and changed searchString in order to supp…
martan001 Apr 21, 2024
5ce774c
#175 Added Jira class mock for tests.
martan001 Apr 24, 2024
f2e461c
#175 Added Unit tests for Jira indexing.
martan001 Apr 24, 2024
5752a67
#175 Changed to not encode the JQL parameter since it is not needed.
martan001 Apr 26, 2024
24ca116
#175 Changed implementation to cache already retrieved user objects w…
martan001 Apr 27, 2024
3c919d7
#175 Fixed mock return value due to changes in actual getTeamMembers …
martan001 Apr 27, 2024
203c019
#175 Change persisted value for released/unreleased version and chang…
martan001 May 6, 2024
1b1247a
#175 Changed code to also update an issue when new commit is linked s…
martan001 May 8, 2024
72cd616
#175 Added new field to issue model that is necessary for JiraITSInde…
martan001 May 8, 2024
e3d259d
#175 Revert back to original gitlab.js implementation that was change…
martan001 Jun 7, 2024
2208e11
#175 Fixed a bug that due to invalid access to undefined variable and…
martan001 Jun 7, 2024
8453beb
#175 Fixed a bug that caused eslint to fail.
martan001 Jun 8, 2024
70efc8f
#175 Removed debugging code.
martan001 Jun 8, 2024
7f5feee
#175 Changed to set authorization header only when config params are …
martan001 Jun 8, 2024
17798d1
#175 Improved config information.
martan001 Jun 8, 2024
990fe7f
#175 Eslint max line length fixed.
martan001 Jun 8, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion binocular.js
Original file line number Diff line number Diff line change
Expand Up @@ -331,9 +331,9 @@ async function indexing(indexers, context, reporter, gateway, indexingThread) {
if ('setGateway' in indexer) {
indexer.setGateway(gateway);
}

threadLog(indexingThread, `${indexer.constructor.name} fetching data...`);
await indexer.index();

threadLog(indexingThread, `${indexer.constructor.name} ${indexer.isStopping() ? 'stopped' : 'finished'}...`);
return indexer;
})
Expand Down
52 changes: 51 additions & 1 deletion cli/setupConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,47 @@ export function promptUserAndSaveConfig() {
name: 'githubToken',
message: 'Enter GitHub API Token [only necessary for GitHub Repositories]:',
},
{
type: 'input',
name: 'jiraUrl',
message: 'Enter JIRA URL [only necessary for JIRA Indexer]:',
},
{
type: 'input',
name: 'jiraProject',
message: 'Enter JIRA Project Name (Format: project key or full project name) [only necessary for JIRA Indexer]:',
},
{
type: 'input',
name: 'jiraJql',
message:
'Enter JIRA JQL search string (optional field: if this field is not populated then ' +
'all issue of the previous project parameter will be fetched) [only necessary for JIRA Indexer]:',
},
{
type: 'input',
name: 'jiraMail',
message: 'Enter JIRA E-Mail [only necessary for JIRA Indexer]:',
},
{
type: 'input',
name: 'jiraToken',
message: 'Enter JIRA API Token [only necessary for JIRA Indexer]:',
},
{
type: 'input',
name: 'jiraOrganizationId',
message:
'Enter organization ID (only required if your organization is using the "Team" field ' +
'for assigning multiple assignees to an issue) [only necessary for JIRA Indexer]:',
},
{
type: 'input',
name: 'jiraTeamsFieldId',
message:
'Enter customfield key of "Team" field (only if your organization is using the "Team" ' +
'field for assigning multiple assignees to an issue) [only necessary for JIRA Indexer]:',
},
{
type: 'input',
name: 'arangoHost',
Expand Down Expand Up @@ -67,7 +108,7 @@ export function promptUserAndSaveConfig() {
type: 'list',
name: 'its',
message: 'Select ITS (Issue Tracking System):',
choices: ['github', 'gitlab', 'none'],
choices: ['github', 'gitlab', 'jira', 'none'],
default: 'none',
},
{
Expand All @@ -93,6 +134,15 @@ export function promptUserAndSaveConfig() {
token: answers.githubToken,
},
},
jira: {
url: answers.jiraUrl,
username: answers.jiraMail,
project: answers.jiraProject,
jql: answers.jiraJql,
token: answers.jiraToken,
organizationId: answers.jiraOrganizationId,
teamsId: answers.jiraTeamsFieldId,
},
arango: {
host: answers.arangoHost,
port: parseInt(answers.arangoPort),
Expand Down
7 changes: 7 additions & 0 deletions lib/core/provider/gitlab.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,27 @@ class GitLab {

getProject(projectId) {
log('getProject(%o)', projectId);

return this.request(`/projects/${projectId}`).then((resp) => {
return resp.body;
});
}

getIssues(projectId) {
log('getIssues(%o)', projectId);

return this.paginatedRequest(`/projects/${projectId}/issues`);
}

getNotes(projectId, issueId) {
log('getNotes(%o, %o)', projectId, issueId);

return this.paginatedRequest(`/projects/${projectId}/issues/${issueId}/notes`);
}

getPipelines(projectId) {
log('getPipelines(%o)', projectId);

return this.graphQL
.request(
gql`
Expand Down Expand Up @@ -114,6 +118,7 @@ class GitLab {

getMileStones(projectId) {
log('getMilestones(%o)', projectId);

return this.paginatedRequest(`/projects/${projectId}/milestones`);
}

Expand All @@ -123,11 +128,13 @@ class GitLab {

getMergeRequests(projectId) {
log('getMergeRequests(%o)', projectId);

return this.paginatedRequest(`/projects/${projectId}/merge_requests?scope=all`);
}

getMergeRequestNotes(projectId, issueId) {
log('getMergeRequestNotes(%o, %o)', projectId, issueId);

return this.paginatedRequest(`/projects/${projectId}/merge_requests/${issueId}/notes`);
}

Expand Down
256 changes: 256 additions & 0 deletions lib/core/provider/jira.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
'use strict';

import debug from 'debug';
import Paginator from '../../paginator';
import urlJoin from 'url-join';
import {
JiraCommitsDetails,
JiraCommitsFullDetails,
JiraCommitsSummary,
JiraDevelopmentSummary,
JiraFullAuthor,
JiraPullRequestDetails,
JiraPullRequestsFullDetails,
JiraPullRequestsSummary,
} from '../../types/jiraTypes';

const log = debug('jira');

class Jira {
private readonly API_VERSION = '2';
private baseUrl;
private applicationbaseUrl;
private count!: number;
private privateToken;
private requestTimeout;
private stopping;
private usermail: string | undefined;

constructor(options: { baseUrl: string; email?: string | undefined; privateToken: string; requestTimeout: number }) {
this.baseUrl = urlJoin(options.baseUrl, `/rest/api/${this.API_VERSION}/`);
this.applicationbaseUrl = options.baseUrl;
this.privateToken = options.privateToken;
this.usermail = options.email;
this.requestTimeout = options.requestTimeout;
this.stopping = false;
}

getIssuesWithJQL(jql: string) {
log('getIssuesWithJQL(%o)', jql);
const jqlSearchString = `fields=*all&jql=${jql}&expand=changelog`;

return this.paginatedRequest('search?' + jqlSearchString + '&');
}

getProjectVersions(projectKey: string) {
log('getProjectVersions(%o)', projectKey);
return this.paginatedRequest(`project/${projectKey}/version?`);
}

private getDetailsPromises(issueId: string, summaryObject: JiraCommitsSummary | JiraPullRequestsSummary) {
const promises: Promise<any>[] = [];

for (const [key] of Object.entries(summaryObject.byInstanceType)) {
promises.push(
this.request(
'dev-status/latest/issue/detail?issueId=' + issueId + `&dataType=${summaryObject.overall.dataType}&applicationType=` + key
)
);
}

return promises;
}

getCommitDetails(issueId: string, summaryObject: JiraCommitsSummary, shouldFetch: boolean) {
if (!summaryObject || !shouldFetch) {
return Promise.resolve([]);
}
const promises: Promise<JiraCommitsFullDetails[]>[] = this.getDetailsPromises(issueId, summaryObject);

let informationToReturn: JiraCommitsDetails[] = [];

return Promise.all(promises).then((responses) => {
for (const response of responses) {
response.forEach((developmentDetailedObject) => {
informationToReturn = informationToReturn.concat(developmentDetailedObject.repositories).flat();
});
}
return informationToReturn;
});
}

getPullrequestDetails(issueId: string, summaryObject: JiraPullRequestsSummary) {
if (!summaryObject) {
return Promise.resolve([]);
}
const promises: Promise<JiraPullRequestsFullDetails[]>[] = this.getDetailsPromises(issueId, summaryObject);

let informationToReturn: JiraPullRequestDetails[] = [];

return Promise.all(promises).then((responses) => {
for (const response of responses) {
response.forEach((developmentDetailedObject) => {
developmentDetailedObject.pullRequests.forEach((pullRequestDetails) => {
pullRequestDetails.instance = developmentDetailedObject._instance;
});
informationToReturn = informationToReturn.concat(developmentDetailedObject.pullRequests).flat();
});
}
return informationToReturn;
});
}

isStopping() {
return this.stopping;
}

stop() {
this.stopping = true;
}

getDevelopmentSummary(issueId: string) {
log('getMergeRequests(%o)', issueId);

return this.request('dev-status/latest/issue/summary?issueId=' + issueId).then((developmentInformation: JiraDevelopmentSummary) => ({
commits: developmentInformation?.repository,
pullrequests: developmentInformation?.pullrequest,
}));
}

getWorklog(issueKey: string) {
log('getWorklog(%o)', issueKey);
return this.paginatedRequest(`issue/${issueKey}/worklog?`);
}

getChangelog(issueKey: string) {
log('getChangelog(%o)', issueKey);
return this.paginatedRequest(`issue/${issueKey}/changelog?`);
}

getComments(issueKey: string) {
log('getComments(%o)', issueKey);
return this.paginatedRequest(`issue/${issueKey}/comment?`);
}

paginatedRequest(path: string) {
log('paginatedRequest(%o)', path);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
return new Paginator(
(start_at: number, per_request: number) => {
// needs to be changed since in Jira pagination uses startAt index and not page
if (this.stopping) {
return Promise.resolve([]);
}
return this.request(path + `startAt=${start_at}&maxResults=${per_request}`);
},
(resp: any) => {
if (path.includes('/comment')) {
return resp.body.comments;
} else if (path.includes('/version') || path.includes('/changelog')) {
return resp.body.values;
} else if (path.includes('/worklog')) {
return resp.body.worklogs;
}
// for issues
return resp.body.issues || [];
},
(resp: { headers: any; body: { total: string; [key: string]: any } }) => {
return (this.count = parseInt(resp.body.total, 10));
},
{ its: 'jira' }
);
}

private request(path: string) {
log('request(%o)', path);
const credentials = this.usermail + ':' + this.privateToken;
const header = this.createHeader(credentials, 'GET');

const isNonOfficial = path.includes('dev-status');
const requestUrl = isNonOfficial ? this.baseUrl.split(`api/${this.API_VERSION}`)[0] + path : urlJoin(this.baseUrl, path);
return fetch(requestUrl, header).then((response) => {
const successful = response.ok;
if (!successful) {
log('different response code: ' + response.status + '\n' + requestUrl);
return { headers: response.headers, body: [] };
}
return response.json().then((data) => {
if (path.includes('user?accountId=')) {
return data;
} else if (!isNonOfficial) {
return { headers: response.headers, body: data };
} else if (path.includes('detail')) {
return data.detail;
} else {
return data.summary;
}
});
});
}

private createHeader(credentials: string, methodType: string) {
const myHeader = new Headers();
myHeader.set('Accept', 'application/json');
if (this.usermail && this.privateToken) {
myHeader.set('Authorization', `Basic ${Buffer.from(credentials).toString('base64')}`);
}
return {
method: methodType,
headers: myHeader,
timeout: this.requestTimeout || 3000,
};
}

private requestTeamMembers(path: string) {
log('team(%o)', path);
const credentials = this.usermail + ':' + this.privateToken;
const header = this.createHeader(credentials, 'POST');

return fetch(path, header).then((response) => {
const successful = response.ok;

return response.json().then((data) => {
if (!successful) {
log('different response code: ' + response.status + '\n' + data);
return [];
}
return data.results;
});
});
}

getTeamMembers(
organizationId: string | undefined,
teamsId: string | undefined,
originalAssignee: JiraFullAuthor | null,
seenUsers: Map<string, JiraFullAuthor>
) {
log('getTeamMembers()');
const retAssignees = originalAssignee ? [originalAssignee] : [];
if (!organizationId || !teamsId) {
return Promise.resolve(retAssignees);
}

const teamMembersUrl = `${this.applicationbaseUrl}gateway/api/public/teams/v1/org/${organizationId}/teams/${teamsId}/members`;

return this.requestTeamMembers(teamMembersUrl).then((response: [{ accountId: string }]) => {
const membersPromises: any[] = response.map((memberId) => {
if (originalAssignee && originalAssignee.accountId !== memberId.accountId) {
const cachedUser = seenUsers.get(memberId.accountId);
if (!cachedUser) {
return this.request(`/user?accountId=${memberId.accountId}`);
} else {
retAssignees.push(cachedUser);
}
}
});

return Promise.all(membersPromises).then((assignees: any[]) => {
return assignees.concat(retAssignees);
});
});
}
}

export default Jira;
Loading
Loading