@dxdeveloperexperience/hygie
Version:
Hygie is an easy-to-use Open-Source REST API allowing you to interact with GIT events. This NestJS API expose a set of customizable rules to automate your project's life cycle.
581 lines (531 loc) • 16.8 kB
text/typescript
import { Injectable, HttpService } from '@nestjs/common';
import { GitServiceInterface } from '../git/git.service.interface';
import {
GitTypeEnum,
convertCommitStatus,
convertIssueState,
convertIssuePRSearchState,
} from '../webhook/utils.enum';
import { GitCommitStatusInfos } from '../git/gitCommitStatusInfos';
import {
GitIssueInfos,
GitIssuePRSearch,
IssueSearchResult,
} from '../git/gitIssueInfos';
import {
GitCommentPRInfos,
GitPRInfos,
GitMergePRInfos,
PRMethodsEnum,
PRSearchResult,
} from '../git/gitPRInfos';
import { logger } from '../logger/logger.service';
import { PreconditionException } from '../exceptions/precondition.exception';
import { GitFileInfos } from '../git/gitFileInfos';
import { Utils } from '../utils/utils';
import { DataAccessService } from '../data_access/dataAccess.service';
import { GitEnv } from '../git/gitEnv.interface';
import { GitRelease } from '../git/gitRelease';
import { GitCommit } from '../git/gitCommit';
import { GitRef } from '../git/gitRef';
import { GitTag } from '../git/gitTag';
import { GitBranchCommit } from '../git/gitBranchSha';
import { GitFileData } from '../git/gitFileData';
/**
* Implement `GitServiceInterface` to interact this a Github repository
*/
export class GithubService implements GitServiceInterface {
token: string;
urlApi: string;
repositoryFullName: string;
// Add repositoryName
configGitHub: object;
constructor(private readonly httpService: HttpService) {}
setToken(token: string) {
this.token = token;
}
setUrlApi(urlApi: string) {
this.urlApi = urlApi;
}
setRepositoryFullName(repositoryFullName: string) {
this.repositoryFullName = repositoryFullName;
}
setConfigGitHub(conf?: any) {
this.configGitHub =
typeof conf !== 'undefined'
? conf
: {
headers: {
Authorization: 'token ' + this.token,
},
};
}
async setEnvironmentVariables(
dataAccessService: DataAccessService,
repositoryFullName: string,
): Promise<void> {
const gitEnv: GitEnv = await Utils.getGitEnv(
dataAccessService,
'remote-envs/' + repositoryFullName + '/config.env',
)
.then(res => res)
.catch(e => {
throw new PreconditionException();
});
this.setToken(Utils.decryptValue(gitEnv.gitToken));
this.setUrlApi(gitEnv.gitApi);
this.setRepositoryFullName(repositoryFullName);
this.setConfigGitHub();
}
updateCommitStatus(gitCommitStatusInfos: GitCommitStatusInfos): void {
const dataGitHub = {
state: convertCommitStatus(
GitTypeEnum.Github,
gitCommitStatusInfos.commitStatus,
),
target_url: gitCommitStatusInfos.targetUrl,
description: gitCommitStatusInfos.descriptionMessage,
context: process.env.APPLICATION_NAME + '/commit',
};
this.httpService
.post(
`${this.urlApi}/repos/${this.repositoryFullName}/statuses/${
gitCommitStatusInfos.commitSha
}`,
dataGitHub,
this.configGitHub,
)
.subscribe(null, err =>
logger.error(err, { location: 'updateCommitStatus' }),
);
}
addIssueComment(gitIssueInfos: GitIssueInfos): void {
const dataGitHub = {
body: gitIssueInfos.comment,
};
this.httpService
.post(
`${this.urlApi}/repos/${this.repositoryFullName}/issues/${
gitIssueInfos.number
}/comments`,
dataGitHub,
this.configGitHub,
)
.subscribe(null, err =>
logger.error(err, { location: 'addIssueComment' }),
);
}
// Github PR is based on Issue
addPRComment(gitCommentPRInfos: GitCommentPRInfos): void {
const gitIssueInfos: GitIssueInfos = new GitIssueInfos();
gitIssueInfos.number = gitCommentPRInfos.number;
gitIssueInfos.comment = gitCommentPRInfos.comment;
this.addIssueComment(gitIssueInfos);
}
createPullRequest(gitCreatePRInfos: GitPRInfos): void {
const customConfig = JSON.parse(JSON.stringify(this.configGitHub));
const dataGitHub: any = {
title: gitCreatePRInfos.title,
body: gitCreatePRInfos.description,
head: gitCreatePRInfos.source,
base: gitCreatePRInfos.target,
};
if (typeof gitCreatePRInfos.draft !== 'undefined') {
dataGitHub.draft = gitCreatePRInfos.draft;
customConfig.headers.Accept =
'application/vnd.github.shadow-cat-preview+json';
}
this.httpService
.post(
`${this.urlApi}/repos/${this.repositoryFullName}/pulls`,
dataGitHub,
customConfig,
)
.subscribe(null, err =>
logger.error(err, { location: 'createPullRequest' }),
);
}
deleteBranch(branchName: string): void {
this.httpService
.delete(
`${this.urlApi}/repos/${
this.repositoryFullName
}/git/refs/heads/${encodeURIComponent(branchName)}`,
this.configGitHub,
)
.subscribe(null, err => logger.error(err, { location: 'deleteBranch' }));
}
updateIssue(gitIssueInfos: GitIssueInfos): void {
const dataGitHub: any = {};
if (typeof gitIssueInfos.state !== 'undefined') {
dataGitHub.state = convertIssueState(
GitTypeEnum.Github,
gitIssueInfos.state,
);
}
if (typeof gitIssueInfos.labels !== 'undefined') {
dataGitHub.labels = gitIssueInfos.labels;
}
this.httpService
.patch(
`${this.urlApi}/repos/${this.repositoryFullName}/issues/${
gitIssueInfos.number
}`,
dataGitHub,
this.configGitHub,
)
.subscribe(null, err => logger.error(err, { location: 'updateIssue' }));
}
async createIssue(gitIssueInfos: GitIssueInfos): Promise<number> {
const dataGitHub: any = {};
if (typeof gitIssueInfos.title !== 'undefined') {
dataGitHub.title = gitIssueInfos.title;
} else {
// Title is required
return;
}
if (typeof gitIssueInfos.labels !== 'undefined') {
dataGitHub.labels = gitIssueInfos.labels;
}
if (typeof gitIssueInfos.assignees !== 'undefined') {
dataGitHub.assignees = gitIssueInfos.assignees;
}
if (typeof gitIssueInfos.description !== 'undefined') {
dataGitHub.body = gitIssueInfos.description;
}
return await this.httpService
.post(
`${this.urlApi}/repos/${this.repositoryFullName}/issues`,
dataGitHub,
this.configGitHub,
)
.toPromise()
.then(response => response.data.number)
.catch(err => logger.error(err, { location: 'createIssue' }));
}
async deleteFile(gitFileInfos: GitFileInfos): Promise<void> {
const localConfig: any = JSON.parse(JSON.stringify(this.configGitHub));
// First of all, get file blob sha
this.httpService
.get(
`${this.urlApi}/repos/${this.repositoryFullName}/contents/${
gitFileInfos.filePath
}`,
this.configGitHub,
)
.subscribe(
response => {
localConfig.params = {
sha: response.data.sha,
message: gitFileInfos.commitMessage,
branch: gitFileInfos.fileBranch,
};
this.httpService
.delete(
`${this.urlApi}/repos/${this.repositoryFullName}/contents/${
gitFileInfos.filePath
}`,
localConfig,
)
.subscribe(
response2 => {
return;
},
err2 => {
logger.error(err2, { location: 'deleteFile/blob' });
throw err2;
},
);
},
err => {
logger.error(err, { location: 'deleteFile' });
throw err;
},
);
}
mergePullRequest(gitMergePRInfos: GitMergePRInfos): void {
const dataGitHub: any = {};
if (gitMergePRInfos.commitTitle !== undefined) {
dataGitHub.commit_title = gitMergePRInfos.commitTitle;
}
if (gitMergePRInfos.commitMessage !== undefined) {
dataGitHub.commit_message = gitMergePRInfos.commitMessage;
}
if (gitMergePRInfos.sha !== undefined) {
dataGitHub.sha = gitMergePRInfos.sha;
}
dataGitHub.merge_method =
gitMergePRInfos.method !== undefined
? gitMergePRInfos.method.toLowerCase()
: PRMethodsEnum.Merge.toLocaleLowerCase();
this.httpService
.put(
`${this.urlApi}/repos/${this.repositoryFullName}/pulls/${
gitMergePRInfos.number
}/merge`,
dataGitHub,
this.configGitHub,
)
.subscribe(null, err =>
logger.error(err, { location: 'mergePullRequest' }),
);
}
updatePullRequest(gitPRInfos: GitPRInfos): void {
const dataGitHub: any = {};
if (typeof gitPRInfos.state !== 'undefined') {
dataGitHub.state = convertIssueState(
GitTypeEnum.Github,
gitPRInfos.state,
);
}
if (typeof gitPRInfos.title !== 'undefined') {
dataGitHub.title = gitPRInfos.title;
}
if (typeof gitPRInfos.target !== 'undefined') {
dataGitHub.base = gitPRInfos.target;
}
if (typeof gitPRInfos.description !== 'undefined') {
dataGitHub.body = gitPRInfos.description;
}
this.httpService
.patch(
`${this.urlApi}/repos/${this.repositoryFullName}/pulls/${
gitPRInfos.number
}`,
dataGitHub,
this.configGitHub,
)
.subscribe(null, err =>
logger.error(err, { location: 'updatePullRequest' }),
);
}
createWebhook(webhookURL: string): void {
this.httpService
.post(
`${this.urlApi}/repos/${this.repositoryFullName}/hooks`,
{
name: 'web',
active: true,
events: ['*'],
config: {
url: `${webhookURL}`,
content_type: 'json',
},
},
this.configGitHub,
)
.subscribe(null, err => logger.error(err, { location: 'createWebhook' }));
}
async getIssues(
gitIssueSearch: GitIssuePRSearch,
): Promise<IssueSearchResult[]> {
const customGithubConfig = JSON.parse(JSON.stringify(this.configGitHub));
customGithubConfig.params = {};
if (typeof gitIssueSearch.sort !== 'undefined') {
customGithubConfig.params.direction = gitIssueSearch.sort.toLowerCase();
}
if (typeof gitIssueSearch.state !== 'undefined') {
customGithubConfig.params.state = convertIssuePRSearchState(
GitTypeEnum.Github,
gitIssueSearch.state,
);
}
return await this.httpService
.get(
`${this.urlApi}/repos/${this.repositoryFullName}/issues`,
customGithubConfig,
)
.toPromise()
.then(res => {
return res.data.map(d => {
if (typeof d.pull_request === 'undefined') {
// Pull Requests are considered as Issues
const issueSearchResult = new IssueSearchResult();
issueSearchResult.updatedAt = d.updated_at;
issueSearchResult.number = d.number;
return issueSearchResult;
}
});
})
.catch(err => logger.error(err, { location: 'getIssues' }));
}
async getPullRequests(
gitIssueSearch: GitIssuePRSearch,
): Promise<PRSearchResult[]> {
const customGithubConfig = JSON.parse(JSON.stringify(this.configGitHub));
customGithubConfig.params = {};
if (typeof gitIssueSearch.sort !== 'undefined') {
customGithubConfig.params.direction = gitIssueSearch.sort.toLowerCase();
}
if (typeof gitIssueSearch.state !== 'undefined') {
customGithubConfig.params.state = convertIssuePRSearchState(
GitTypeEnum.Github,
gitIssueSearch.state,
);
}
return await this.httpService
.get(
`${this.urlApi}/repos/${this.repositoryFullName}/pulls`,
customGithubConfig,
)
.toPromise()
.then(res => {
return res.data.map(d => {
const prSearchResult = new PRSearchResult();
prSearchResult.updatedAt = d.updated_at;
prSearchResult.number = d.number;
return prSearchResult;
});
})
.catch(err => logger.error(err, { location: 'getPullRequests' }));
}
createRelease(gitRelease: GitRelease): void {
const dataGithub: any = {};
dataGithub.tag_name = gitRelease.tag;
if (typeof gitRelease.ref !== 'undefined') {
dataGithub.target_commitish = gitRelease.ref;
}
if (typeof gitRelease.name !== 'undefined') {
dataGithub.name = gitRelease.name;
}
if (typeof gitRelease.description !== 'undefined') {
dataGithub.body = gitRelease.description;
}
this.httpService
.post(
`${this.urlApi}/repos/${this.repositoryFullName}/releases`,
dataGithub,
this.configGitHub,
)
.subscribe(null, err => logger.error(err, { location: 'createRelease' }));
}
async getTree(
directoryPath: string,
branch: string = 'master',
): Promise<string> {
const { base, name } = Utils.splitDirectoryPath(directoryPath);
const customGithubConfig = JSON.parse(JSON.stringify(this.configGitHub));
customGithubConfig.params = {
ref: branch,
};
return await this.httpService
.get(
`${this.urlApi}/repos/${this.repositoryFullName}/contents/${base}`,
customGithubConfig,
)
.toPromise()
.then(response => {
return response.data.find(e => e.name === name).sha;
})
.catch(err => logger.error(err, { location: 'getTree' }));
}
async getLastCommit(branch: string = 'master'): Promise<string> {
return this.httpService
.get(
`${this.urlApi}/repos/${
this.repositoryFullName
}/git/refs/heads/${branch}`,
this.configGitHub,
)
.toPromise()
.then(response => response.data.object.sha)
.catch(err => logger.error(err, { location: 'updateRef' }));
}
async createCommit(gitCommit: GitCommit): Promise<string> {
return await this.httpService
.post(
`${this.urlApi}/repos/${this.repositoryFullName}/git/commits`,
gitCommit,
this.configGitHub,
)
.toPromise()
.then(response => {
return response.data.sha;
})
.catch(err => logger.error(err, { location: 'createCommit' }));
}
updateRef(gitRef: GitRef): void {
this.httpService
.patch(
`${this.urlApi}/repos/${this.repositoryFullName}/git/${gitRef.refName}`,
{
sha: gitRef.sha,
force: gitRef.force,
},
this.configGitHub,
)
.subscribe(null, err => logger.error(err, { location: 'updateRef' }));
}
createRef(gitRef: GitRef): void {
this.httpService
.post(
`${this.urlApi}/repos/${this.repositoryFullName}/git/refs`,
{
sha: gitRef.sha,
ref: gitRef.refName,
},
this.configGitHub,
)
.subscribe(null, err => logger.error(err, { location: 'createRef' }));
}
async createTag(gitTag: GitTag): Promise<string> {
return await this.httpService
.post(
`${this.urlApi}/repos/${this.repositoryFullName}/git/tags`,
{
tag: gitTag.tag,
message: gitTag.message,
object: gitTag.sha,
type: gitTag.type,
},
this.configGitHub,
)
.toPromise()
.then(response => response.data.sha)
.catch(err => logger.error(err, { location: 'createTag' }));
}
async getLastBranchesCommitSha(): Promise<GitBranchCommit[]> {
return await this.httpService
.get(
`${this.urlApi}/repos/${this.repositoryFullName}/branches`,
this.configGitHub,
)
.toPromise()
.then(response =>
response.data.map(b => {
return { commitSha: b.commit.sha, branch: b.name };
}),
)
.catch(err => {
logger.error(err, { location: 'getLastBranchesCommitSha' });
return [];
});
}
async getFileContent(gitFileInfos: GitFileInfos): Promise<GitFileData> {
const localConfig: any = JSON.parse(JSON.stringify(this.configGitHub));
localConfig.params = {
ref: gitFileInfos.fileBranch,
};
return await this.httpService
.get(
`${this.urlApi}/repos/${this.repositoryFullName}/contents/${
gitFileInfos.filePath
}`,
localConfig,
)
.toPromise()
.then(response => {
return {
data: Buffer.from(response.data.content, 'base64').toString(),
};
})
.catch(err => {
throw new Error(
`${err}: ${gitFileInfos.filePath} do not exist on branch ${
gitFileInfos.fileBranch
}`,
);
});
}
}