refactor: Add createPullRequest method to HostedCodeRepository

The HostedCodeRepository class in HostedCodeRepository.ts has been updated to include a new method, createPullRequest. This method allows for the creation of pull requests in the code repository. This change enhances the functionality and flexibility of the HostedCodeRepository class.
This commit is contained in:
Simon Larsen 2024-06-13 12:26:49 +01:00
parent dea385ad44
commit 96d4131614
No known key found for this signature in database
GPG Key ID: 96C5DCA24769DBCA
7 changed files with 483 additions and 13 deletions

View File

@ -4,6 +4,135 @@ import CodeRepositoryFile from './CodeRepositoryFile';
import Dictionary from 'Common/Types/Dictionary';
export default class CodeRepositoryUtil {
public static async createOrCheckoutBranch(data: {
repoPath: string;
branchName: string;
}): Promise<void> {
await Execute.executeCommand(
`cd ${data.repoPath} && git checkout ${data.branchName} || git checkout -b ${data.branchName}`
);
}
// discard all changes in the working directory
public static async discardChanges(data: {
repoPath: string;
}): Promise<void> {
await Execute.executeCommand(
`cd ${data.repoPath} && git checkout .`
);
}
public static async writeToFile(data: {
filePath: string;
repoPath: string;
content: string;
}): Promise<void> {
const totalPath: string = LocalFile.sanitizeFilePath(`${data.repoPath}/${data.filePath}`);
await Execute.executeCommand(
`echo "${data.content}" > ${totalPath}`
);
}
public static async createDirectory(data: {
repoPath: string;
directoryPath: string;
}): Promise<void> {
const totalPath: string = LocalFile.sanitizeFilePath(`${data.repoPath}/${data.directoryPath}`);
await Execute.executeCommand(
`mkdir ${totalPath}`
);
}
public static async deleteFile(data: {
repoPath: string;
filePath: string;
}): Promise<void> {
const totalPath: string = LocalFile.sanitizeFilePath(`${data.repoPath}/${data.filePath}`);
await Execute.executeCommand(
`rm ${totalPath}`
);
}
public static async deleteDirectory(data: {
repoPath: string;
directoryPath: string;
}): Promise<void> {
const totalPath: string = LocalFile.sanitizeFilePath(`${data.repoPath}/${data.directoryPath}`);
await Execute.executeCommand(
`rm -rf ${totalPath}`
);
}
public static async createBranch(data: {
repoPath: string;
branchName: string;
}): Promise<void> {
await Execute.executeCommand(
`cd ${data.repoPath} && git checkout -b ${data.branchName}`
);
}
public static async checkoutBranch(data: {
repoPath: string;
branchName: string;
}): Promise<void> {
await Execute.executeCommand(
`cd ${data.repoPath} && git checkout ${data.branchName}`
);
}
public static async addFilesToGit(data: {
repoPath: string;
filePaths: Array<string>;
}): Promise<void> {
const filePaths = data.filePaths.map((filePath) => {
if(filePath.startsWith('/')){
// remove the leading slash and return
return filePath.substring(1);
}else{
return filePath;
}
});
await Execute.executeCommand(
`cd ${data.repoPath} && git add ${filePaths.join(' ')}`
);
}
public static async commitChanges(data: {
repoPath: string;
message: string;
}): Promise<void> {
await Execute.executeCommand(
`cd ${data.repoPath} && git commit -m "${data.message}"`
);
}
public static async pushChanges(data: {
repoPath: string;
branchName: string;
remoteName?: string | undefined;
}): Promise<void> {
const remoteName: string = data.remoteName || 'origin';
await Execute.executeCommand(
`cd ${data.repoPath} && git push ${remoteName} ${data.branchName}`
);
}
public static async getGitCommitHashForFile(data: {
repoPath: string;
filePath: string;
@ -27,6 +156,7 @@ export default class CodeRepositoryUtil {
directoryPath: string;
repoPath: string;
acceptedFileExtensions?: Array<string>;
ignoreFilesOrDirectories: Array<string>;
}): Promise<{
files: Dictionary<CodeRepositoryFile>;
subDirectories: Array<string>;
@ -57,12 +187,15 @@ export default class CodeRepositoryUtil {
continue;
}
const filePath: string = LocalFile.sanitizeFilePath(
`${directoryPath}/${fileName}`
);
if(data.ignoreFilesOrDirectories.includes(fileName)){
continue;
}
const isDirectory: boolean = (
await Execute.executeCommand(
`file "${LocalFile.sanitizeFilePath(
@ -122,6 +255,7 @@ export default class CodeRepositoryUtil {
repoPath: string;
directoryPath: string;
acceptedFileExtensions: Array<string>;
ignoreFilesOrDirectories: Array<string>;
}): Promise<Dictionary<CodeRepositoryFile>> {
let files: Dictionary<CodeRepositoryFile> = {};
@ -130,6 +264,7 @@ export default class CodeRepositoryUtil {
directoryPath: data.directoryPath,
repoPath: data.repoPath,
acceptedFileExtensions: data.acceptedFileExtensions,
ignoreFilesOrDirectories: data.ignoreFilesOrDirectories,
});
files = {
@ -144,6 +279,7 @@ export default class CodeRepositoryUtil {
repoPath: data.repoPath,
directoryPath: subDirectory,
acceptedFileExtensions: data.acceptedFileExtensions,
ignoreFilesOrDirectories: data.ignoreFilesOrDirectories,
})),
};
}

View File

@ -9,6 +9,7 @@ import { JSONArray, JSONObject } from 'Common/Types/JSON';
import API from 'Common/Utils/API';
export default class GitHubUtil extends HostedCodeRepository {
private getPullRequestFromJSONObject(data: {
pullRequest: JSONObject;
organizationName: string;
@ -149,4 +150,46 @@ export default class GitHubUtil extends HostedCodeRepository {
return allPullRequests;
}
public override async createPullRequest(data: {
baseBranchName: string;
headBranchName: string;
organizationName: string;
repositoryName: string;
title: string;
body: string;
}): Promise<PullRequest> {
const gitHubToken: string = this.authToken;
const url: URL = URL.fromString(
`https://api.github.com/repos/${data.organizationName}/${data.repositoryName}/pulls`
);
const result: HTTPErrorResponse | HTTPResponse<JSONObject> =
await API.post(
url,
{
base: data.baseBranchName,
head: data.headBranchName,
title: data.title,
body: data.body,
},
{
Authorization: `Bearer ${gitHubToken}`,
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
}
);
if (result instanceof HTTPErrorResponse) {
throw result;
}
return this.getPullRequestFromJSONObject({
pullRequest: result.data,
organizationName: data.organizationName,
repositoryName: data.repositoryName,
});
}
}

View File

@ -78,4 +78,15 @@ export default class HostedCodeRepository {
}): Promise<Array<PullRequest>> {
throw new NotImplementedException();
}
public async createPullRequest(_data: {
baseBranchName: string;
headBranchName: string;
organizationName: string;
repositoryName: string;
title: string;
body: string;
}): Promise<PullRequest> {
throw new NotImplementedException();
}
}

View File

@ -1,4 +1,4 @@
import { CodeRepositoryResult } from './Utils/CodeRepository';
import CodeRepositoryUtil, { CodeRepositoryResult } from './Utils/CodeRepository';
import InitUtil from './Utils/Init';
import ServiceRepositoryUtil from './Utils/ServiceRepository';
import HTTPErrorResponse from 'Common/Types/API/HTTPErrorResponse';
@ -22,10 +22,45 @@ const init: PromiseVoidFunction = async (): Promise<void> => {
});
logger.info(
`Files found in ${serviceRepository.serviceCatalog?.name}: ${
Object.keys(filesInService).length
`Files found in ${serviceRepository.serviceCatalog?.name}: ${Object.keys(filesInService).length
}`
);
await CodeRepositoryUtil.createBranch({
branchName: 'test-branch',
});
// test code from here.
const file = filesInService[Object.keys(filesInService)[0]!];
await CodeRepositoryUtil.writeToFile({
filePath: file?.filePath!,
content: 'Hello World',
});
// commit the changes
await CodeRepositoryUtil.addFilesToGit({
filePaths: [file?.filePath!],
});
await CodeRepositoryUtil.commitChanges({
message: 'Test commit',
});
await CodeRepositoryUtil.pushChanges({
branchName: 'test-branch',
});
// create a pull request
await CodeRepositoryUtil.createPullRequest({
title: 'Test PR',
body: 'Test PR body',
branchName: 'test-branch',
});
}
};
@ -33,7 +68,19 @@ init()
.then(() => {
process.exit(0);
})
.catch((error: Error | HTTPErrorResponse) => {
.catch(async (error: Error | HTTPErrorResponse) => {
try {
await CodeRepositoryUtil.discardChanges();
// change back to main branch.
await CodeRepositoryUtil.checkoutBranch({
branchName: 'main',
});
} catch (e) {
// do nothing.
}
logger.error('Error in starting OneUptime Copilot: ');
if (error instanceof HTTPErrorResponse) {

View File

@ -1,5 +1,6 @@
import {
GetGitHubToken,
GetLocalRepositoryPath,
GetOneUptimeURL,
GetRepositorySecretKey,
} from '../Config';
@ -15,6 +16,8 @@ import GitHubUtil from 'CommonServer/Utils/CodeRepository/GitHub/GitHub';
import logger from 'CommonServer/Utils/Logger';
import CodeRepositoryModel from 'Model/Models/CodeRepository';
import ServiceRepository from 'Model/Models/ServiceRepository';
import CodeRepositoryServerUtil from 'CommonServer/Utils/CodeRepository/CodeRepository'
import PullRequest from 'Common/Types/CodeRepository/PullRequest';
export interface CodeRepositoryResult {
codeRepository: CodeRepositoryModel;
@ -23,6 +26,174 @@ export interface CodeRepositoryResult {
export default class CodeRepositoryUtil {
public static codeRepositoryResult: CodeRepositoryResult | null = null;
public static gitHubUtil: GitHubUtil | null = null;
public static getGitHubUtil(): GitHubUtil {
if (!this.gitHubUtil) {
const gitHubToken: string | null = GetGitHubToken();
if (!gitHubToken) {
throw new BadDataException('GitHub Token is required');
}
this.gitHubUtil = new GitHubUtil({
authToken: gitHubToken,
});
}
return this.gitHubUtil;
}
public static async createBranch(data: {
branchName: string;
}): Promise<void> {
await CodeRepositoryServerUtil.createBranch({
repoPath: GetLocalRepositoryPath(),
branchName: data.branchName,
})
}
public static async createOrCheckoutBranch(data: {
branchName: string;
}): Promise<void> {
await CodeRepositoryServerUtil.createOrCheckoutBranch({
repoPath: GetLocalRepositoryPath(),
branchName: data.branchName,
})
}
public static async writeToFile(data: {
filePath: string;
content: string;
}): Promise<void> {
await CodeRepositoryServerUtil.writeToFile({
repoPath: GetLocalRepositoryPath(),
filePath: data.filePath,
content: data.content,
})
}
public static async createDirectory(data: {
directoryPath: string;
}): Promise<void> {
await CodeRepositoryServerUtil.createDirectory({
repoPath: GetLocalRepositoryPath(),
directoryPath: data.directoryPath,
})
}
public static async deleteFile(data: {
filePath: string;
}): Promise<void> {
await CodeRepositoryServerUtil.deleteFile({
repoPath: GetLocalRepositoryPath(),
filePath: data.filePath,
})
}
public static async deleteDirectory(data: {
directoryPath: string;
}): Promise<void> {
await CodeRepositoryServerUtil.deleteDirectory({
repoPath: GetLocalRepositoryPath(),
directoryPath: data.directoryPath,
})
}
public static async discardChanges(): Promise<void> {
await CodeRepositoryServerUtil.discardChanges({
repoPath: GetLocalRepositoryPath(),
})
}
public static async checkoutBranch(data: {
branchName: string;
}): Promise<void> {
await CodeRepositoryServerUtil.checkoutBranch({
repoPath: GetLocalRepositoryPath(),
branchName: data.branchName,
})
}
public static async checkoutMainBranch(): Promise<void> {
const codeRepository: CodeRepositoryModel = await this.getCodeRepository();
if (!codeRepository.mainBranchName) {
throw new BadDataException('Main Branch Name is required');
}
await this.checkoutBranch({
branchName: codeRepository.mainBranchName!,
});
}
public static async addFilesToGit(data: {
filePaths: Array<string>;
}): Promise<void> {
await CodeRepositoryServerUtil.addFilesToGit({
repoPath: GetLocalRepositoryPath(),
filePaths: data.filePaths,
})
}
public static async commitChanges(data: {
message: string;
}): Promise<void> {
await CodeRepositoryServerUtil.commitChanges({
repoPath: GetLocalRepositoryPath(),
message: data.message,
})
}
public static async pushChanges(data: {
branchName: string;
}): Promise<void> {
await CodeRepositoryServerUtil.pushChanges({
repoPath: GetLocalRepositoryPath(),
branchName: data.branchName,
})
}
public static async createPullRequest(data: {
branchName: string;
title: string;
body: string;
}): Promise<PullRequest> {
const codeRepository: CodeRepositoryModel = await this.getCodeRepository();
if(!codeRepository.mainBranchName){
throw new BadDataException('Main Branch Name is required');
}
if(!codeRepository.organizationName){
throw new BadDataException('Organization Name is required');
}
if(!codeRepository.repositoryName){
throw new BadDataException('Repository Name is required');
}
if(codeRepository.repositoryHostedAt === CodeRepositoryType.GitHub){
return await this.getGitHubUtil().createPullRequest({
headBranchName: data.branchName,
baseBranchName: codeRepository.mainBranchName,
organizationName: codeRepository.organizationName,
repositoryName: codeRepository.repositoryName,
title: data.title,
body: data.body,
});
}else{
throw new BadDataException('Code Repository type not supported');
}
}
public static async getServicesToImproveCode(data: {
codeRepository: CodeRepositoryModel;
@ -60,9 +231,7 @@ export default class CodeRepositoryUtil {
}
const numberOfPullRequestForThisService: number =
await new GitHubUtil({
authToken: gitHuhbToken,
}).getNumberOfPullRequestsExistForService({
await this.getGitHubUtil().getNumberOfPullRequestsExistForService({
serviceRepository: service,
pullRequestState: PullRequestState.Open,
baseBranchName: data.codeRepository.mainBranchName,

View File

@ -2,15 +2,76 @@ import ServiceLanguage from 'Common/Types/ServiceCatalog/ServiceLanguage';
export default class ServiceFileTypesUtil {
public static getCommonDirectoriesToIgnore(): string[] {
return ['node_modules', '.git', 'build', 'dist', 'coverage'];
private static getCommonDirectoriesToIgnore(): string[] {
return ['node_modules', '.git', 'build', 'dist', 'coverage', 'logs', 'tmp', 'temp', 'temporal', 'tempfiles', 'tempfiles'];
}
public static getCommonFilesToIgnore(): string[] {
private static getCommonFilesToIgnore(): string[] {
return ['.DS_Store', 'Thumbs.db', '.gitignore', '.gitattributes'];
}
public static getCommonFilesExtentions(): string[] {
public static getCommonFilesToIgnoreByServiceLanguage(
serviceLanguage: ServiceLanguage
): string[] {
let filesToIgnore: string[] = [];
switch (serviceLanguage) {
case ServiceLanguage.NodeJS:
filesToIgnore = ['package-lock.json'];
break;
case ServiceLanguage.Python:
filesToIgnore = ['__pycache__'];
break;
case ServiceLanguage.Ruby:
filesToIgnore = ['Gemfile.lock'];
break;
case ServiceLanguage.Go:
filesToIgnore = ['go.sum', 'go.mod'];
break;
case ServiceLanguage.Java:
filesToIgnore = ['pom.xml'];
break;
case ServiceLanguage.PHP:
filesToIgnore = ['composer.lock'];
break;
case ServiceLanguage.CSharp:
filesToIgnore = ['packages', 'bin', 'obj'];
break;
case ServiceLanguage.CPlusPlus:
filesToIgnore = ['build', 'CMakeFiles', 'CMakeCache.txt', 'Makefile'];
break;
case ServiceLanguage.Rust:
filesToIgnore = ['Cargo.lock'];
break;
case ServiceLanguage.Swift:
filesToIgnore = ['Podfile.lock'];
break;
case ServiceLanguage.Kotlin:
filesToIgnore = ['gradle', 'build', 'gradlew', 'gradlew.bat', 'gradle.properties'];
break;
case ServiceLanguage.TypeScript:
filesToIgnore = ['node_modules', 'package-lock.json'];
break;
case ServiceLanguage.JavaScript:
filesToIgnore = ['node_modules', 'package-lock.json'];
break;
case ServiceLanguage.Shell:
filesToIgnore = [];
break;
case ServiceLanguage.React:
filesToIgnore = ['node_modules', 'package-lock.json'];
break;
case ServiceLanguage.Other:
filesToIgnore = [];
break;
default:
filesToIgnore = [];
}
return filesToIgnore.concat(this.getCommonFilesToIgnore()).concat(this.getCommonDirectoriesToIgnore());
}
private static getCommonFilesExtentions(): string[] {
// return markdown, dockerfile, etc.
return ['.md', 'dockerfile', '.yml', '.yaml', '.sh', '.gitignore'];
}

View File

@ -26,6 +26,9 @@ export default class ServiceRepositoryUtil {
ServiceFileTypesUtil.getFileExtentionsByServiceLanguage(
serviceRepository.serviceCatalog!.serviceLanguage!
),
ignoreFilesOrDirectories: ServiceFileTypesUtil.getCommonFilesToIgnoreByServiceLanguage(
serviceRepository.serviceCatalog!.serviceLanguage!
)
});
return allFiles;