From 2ce8ba62955dffb61628e9e8d4ee1861d24e4c32 Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Wed, 12 Jun 2024 21:05:37 +0100 Subject: [PATCH 01/11] refactor: Sanitize file paths in CodeRepositoryUtil and LocalFile This commit refactors the CodeRepositoryUtil and LocalFile classes to include a new method, sanitizeFilePath, which removes double slashes from file paths. This change ensures that file paths are properly formatted and improves the reliability and maintainability of the code. --- CommonServer/API/CodeRepositoryAPI.ts | 7 ++- .../Utils/CodeRepository/CodeRepository.ts | 43 ++++++++++++++++--- CommonServer/Utils/LocalFile.ts | 5 +++ Copilot/Index.ts | 13 +++++- 4 files changed, 59 insertions(+), 9 deletions(-) diff --git a/CommonServer/API/CodeRepositoryAPI.ts b/CommonServer/API/CodeRepositoryAPI.ts index b7a726805f..58e5dedf73 100644 --- a/CommonServer/API/CodeRepositoryAPI.ts +++ b/CommonServer/API/CodeRepositoryAPI.ts @@ -48,6 +48,9 @@ export default class CodeRepositoryAPI extends BaseAPI< select: { name: true, mainBranchName: true, + organizationName: true, + repositoryHostedAt: true, + repositoryName: true, }, props: { isRoot: true, @@ -55,7 +58,9 @@ export default class CodeRepositoryAPI extends BaseAPI< }); if (!codeRepository) { - throw new BadDataException('Code repository not found'); + throw new BadDataException( + 'Code repository not found. Secret key is invalid.' + ); } const servicesRepository: Array = diff --git a/CommonServer/Utils/CodeRepository/CodeRepository.ts b/CommonServer/Utils/CodeRepository/CodeRepository.ts index a89ed2d979..76e2d1d647 100644 --- a/CommonServer/Utils/CodeRepository/CodeRepository.ts +++ b/CommonServer/Utils/CodeRepository/CodeRepository.ts @@ -1,4 +1,5 @@ import Execute from '../Execute'; +import LocalFile from '../LocalFile'; import CodeRepositoryFile from './CodeRepositoryFile'; import Dictionary from 'Common/Types/Dictionary'; @@ -7,10 +8,18 @@ export default class CodeRepositoryUtil { repoPath: string; filePath: string; }): Promise { + if (!data.filePath.startsWith('/')) { + data.filePath = '/' + data.filePath; + } + + if (!data.repoPath.startsWith('/')) { + data.repoPath = '/' + data.repoPath; + } + const { repoPath, filePath } = data; return await Execute.executeCommand( - `cd ${repoPath} && git log -1 --pretty=format:"%H" "${filePath}"` + `cd ${repoPath} && git log -1 --pretty=format:"%H" ".${filePath}"` ); } @@ -21,9 +30,19 @@ export default class CodeRepositoryUtil { files: Dictionary; subDirectories: Array; }> { + if (!data.directoryPath.startsWith('/')) { + data.directoryPath = '/' + data.directoryPath; + } + + if (!data.repoPath.startsWith('/')) { + data.repoPath = '/' + data.repoPath; + } + const { directoryPath, repoPath } = data; - const totalPath: string = `${repoPath}/${directoryPath}`; + let totalPath: string = `${repoPath}/${directoryPath}`; + + totalPath = LocalFile.sanitizeFilePath(totalPath); // clean up the path const files: Dictionary = {}; const output: string = await Execute.executeCommand(`ls ${totalPath}`); @@ -37,23 +56,35 @@ export default class CodeRepositoryUtil { continue; } + const filePath: string = LocalFile.sanitizeFilePath( + `${directoryPath}/${fileName}` + ); + const isDirectory: boolean = ( - await Execute.executeCommand(`file "${totalPath}/${fileName}"`) + await Execute.executeCommand( + `file "${LocalFile.sanitizeFilePath( + `${totalPath}/${fileName}` + )}"` + ) ).includes('directory'); if (isDirectory) { - subDirectories.push(`${totalPath}/${fileName}`); + subDirectories.push( + LocalFile.sanitizeFilePath(`${directoryPath}/${fileName}`) + ); continue; } - const filePath: string = `${totalPath}/${fileName}`; const gitCommitHash: string = await this.getGitCommitHashForFile({ filePath, repoPath, }); + const fileExtension: string = fileName.split('.').pop() || ''; files[filePath] = { - filePath, + filePath: LocalFile.sanitizeFilePath( + `${directoryPath}/${fileName}` + ), gitCommitHash, fileExtension, fileName, diff --git a/CommonServer/Utils/LocalFile.ts b/CommonServer/Utils/LocalFile.ts index 8c65a640fc..d0767e9477 100644 --- a/CommonServer/Utils/LocalFile.ts +++ b/CommonServer/Utils/LocalFile.ts @@ -2,6 +2,11 @@ import { PromiseRejectErrorFunction } from 'Common/Types/FunctionTypes'; import fs from 'fs'; export default class LocalFile { + public static sanitizeFilePath(filePath: string): string { + // remove double slashes + return filePath.replace(/\/\//g, '/'); + } + public static async makeDirectory(path: string): Promise { return new Promise( (resolve: VoidFunction, reject: PromiseRejectErrorFunction) => { diff --git a/Copilot/Index.ts b/Copilot/Index.ts index 8adb274be7..59af71b4c7 100644 --- a/Copilot/Index.ts +++ b/Copilot/Index.ts @@ -1,6 +1,7 @@ import { CodeRepositoryResult } from './Utils/CodeRepository'; import InitUtil from './Utils/Init'; import ServiceRepositoryUtil from './Utils/ServiceRepository'; +import HTTPErrorResponse from 'Common/Types/API/HTTPErrorResponse'; import Dictionary from 'Common/Types/Dictionary'; import { PromiseVoidFunction } from 'Common/Types/FunctionTypes'; import CodeRepositoryFile from 'CommonServer/Utils/CodeRepository/CodeRepositoryFile'; @@ -32,8 +33,16 @@ init() .then(() => { process.exit(0); }) - .catch((error: Error) => { + .catch((error: Error | HTTPErrorResponse) => { logger.error('Error in starting OneUptime Copilot: '); - logger.error(error); + + if (error instanceof HTTPErrorResponse) { + logger.error(error.message); + } else if (error instanceof Error) { + logger.error(error.message); + } else { + logger.error(error); + } + process.exit(1); }); From d153ad9bf7712433cd1b15adc578a0e048a61245 Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Wed, 12 Jun 2024 21:27:00 +0100 Subject: [PATCH 02/11] refactor: Update ServiceRepository to include serviceLanguage field The ServiceRepository class in ServiceRepository.ts has been updated to include a new field, serviceLanguage. This field is of type ServiceLanguage and is required. This change allows for better organization and identification of code repositories based on their programming language. --- .../Types/ServiceCatalog/ServiceLanguage.ts | 20 ++++++ CommonServer/API/CodeRepositoryAPI.ts | 1 + .../Utils/CodeRepository/CodeRepository.ts | 21 ++++++ Copilot/Utils/ServiceFileTypes.ts | 71 +++++++++++++++++++ Copilot/Utils/ServiceRepository.ts | 12 ++++ .../Pages/ServiceCatalog/ServiceCatalog.tsx | 3 +- .../src/Pages/ServiceCatalog/View/Index.tsx | 3 +- Model/Models/ServiceCatalog.ts | 19 +---- 8 files changed, 130 insertions(+), 20 deletions(-) create mode 100644 Common/Types/ServiceCatalog/ServiceLanguage.ts create mode 100644 Copilot/Utils/ServiceFileTypes.ts diff --git a/Common/Types/ServiceCatalog/ServiceLanguage.ts b/Common/Types/ServiceCatalog/ServiceLanguage.ts new file mode 100644 index 0000000000..e8850b082d --- /dev/null +++ b/Common/Types/ServiceCatalog/ServiceLanguage.ts @@ -0,0 +1,20 @@ +export enum ServiceLanguage { + NodeJS = 'NodeJS', + React = 'React', + Python = 'Python', + Ruby = 'Ruby', + Go = 'Go', + Java = 'Java', + PHP = 'PHP', + CSharp = 'C#', + CPlusPlus = 'C++', + Rust = 'Rust', + Swift = 'Swift', + Kotlin = 'Kotlin', + TypeScript = 'TypeScript', + JavaScript = 'JavaScript', + Shell = 'Shell', + Other = 'Other', +} + +export default ServiceLanguage; diff --git a/CommonServer/API/CodeRepositoryAPI.ts b/CommonServer/API/CodeRepositoryAPI.ts index 58e5dedf73..538d7b744f 100644 --- a/CommonServer/API/CodeRepositoryAPI.ts +++ b/CommonServer/API/CodeRepositoryAPI.ts @@ -73,6 +73,7 @@ export default class CodeRepositoryAPI extends BaseAPI< serviceCatalog: { name: true, _id: true, + serviceLanguage: true, }, servicePathInRepository: true, limitNumberOfOpenPullRequestsCount: true, diff --git a/CommonServer/Utils/CodeRepository/CodeRepository.ts b/CommonServer/Utils/CodeRepository/CodeRepository.ts index 76e2d1d647..8f37ad5dcc 100644 --- a/CommonServer/Utils/CodeRepository/CodeRepository.ts +++ b/CommonServer/Utils/CodeRepository/CodeRepository.ts @@ -26,6 +26,7 @@ export default class CodeRepositoryUtil { public static async getFilesInDirectory(data: { directoryPath: string; repoPath: string; + acceptedFileExtensions?: Array; }): Promise<{ files: Dictionary; subDirectories: Array; @@ -56,6 +57,24 @@ export default class CodeRepositoryUtil { continue; } + if ( + data.acceptedFileExtensions && + data.acceptedFileExtensions.length > 0 + ) { + let shouldSkip: boolean = true; + + for (const fileExtension of data.acceptedFileExtensions) { + if (fileName.endsWith(fileExtension)) { + shouldSkip = false; + break; + } + } + + if (shouldSkip) { + continue; + } + } + const filePath: string = LocalFile.sanitizeFilePath( `${directoryPath}/${fileName}` ); @@ -100,6 +119,7 @@ export default class CodeRepositoryUtil { public static async getFilesInDirectoryRecursive(data: { repoPath: string; directoryPath: string; + acceptedFileExtensions: Array; }): Promise> { let files: Dictionary = {}; @@ -120,6 +140,7 @@ export default class CodeRepositoryUtil { ...(await this.getFilesInDirectoryRecursive({ repoPath: data.repoPath, directoryPath: subDirectory, + acceptedFileExtensions: data.acceptedFileExtensions, })), }; } diff --git a/Copilot/Utils/ServiceFileTypes.ts b/Copilot/Utils/ServiceFileTypes.ts new file mode 100644 index 0000000000..b0276e320a --- /dev/null +++ b/Copilot/Utils/ServiceFileTypes.ts @@ -0,0 +1,71 @@ +import ServiceLanguage from 'Common/Types/ServiceCatalog/ServiceLanguage'; + +export default class ServiceFileTypesUtil { + public static getCommonFilesExtentions(): string[] { + // return markdown, dockerfile, etc. + return ['.md', 'dockerfile', '.yml', '.yaml', '.sh', '.gitignore']; + } + + public static getFileExtentionsByServiceLanguage( + serviceLanguage: ServiceLanguage + ): string[] { + let fileExtentions: Array = []; + + switch (serviceLanguage) { + case ServiceLanguage.NodeJS: + fileExtentions = ['.js', '.ts', '.json', '.mjs']; + break; + case ServiceLanguage.Python: + fileExtentions = ['.py']; + break; + case ServiceLanguage.Ruby: + fileExtentions = ['.rb']; + break; + case ServiceLanguage.Go: + fileExtentions = ['.go']; + break; + case ServiceLanguage.Java: + fileExtentions = ['.java']; + break; + case ServiceLanguage.PHP: + fileExtentions = ['.php']; + break; + case ServiceLanguage.CSharp: + fileExtentions = ['.cs']; + break; + case ServiceLanguage.CPlusPlus: + fileExtentions = ['.cpp', '.c']; + break; + case ServiceLanguage.Rust: + fileExtentions = ['.rs']; + break; + case ServiceLanguage.Swift: + fileExtentions = ['.swift']; + break; + case ServiceLanguage.Kotlin: + fileExtentions = ['.kt', '.kts']; + break; + case ServiceLanguage.TypeScript: + fileExtentions = ['.ts', '.tsx']; + break; + case ServiceLanguage.JavaScript: + fileExtentions = ['.js', '.jsx']; + break; + case ServiceLanguage.Shell: + fileExtentions = ['.sh']; + break; + case ServiceLanguage.React: + fileExtentions = ['.js', '.ts', '.jsx', '.tsx']; + break; + case ServiceLanguage.Other: + fileExtentions = []; + break; + default: + fileExtentions = []; + } + + // add common files extentions + + return fileExtentions.concat(this.getCommonFilesExtentions()); + } +} diff --git a/Copilot/Utils/ServiceRepository.ts b/Copilot/Utils/ServiceRepository.ts index a11440d03b..34e3c0abe0 100644 --- a/Copilot/Utils/ServiceRepository.ts +++ b/Copilot/Utils/ServiceRepository.ts @@ -1,5 +1,7 @@ import { GetLocalRepositoryPath } from '../Config'; +import ServiceFileTypesUtil from './ServiceFileTypes'; import Dictionary from 'Common/Types/Dictionary'; +import BadDataException from 'Common/Types/Exception/BadDataException'; import CodeRepositoryCommonServerUtil from 'CommonServer/Utils/CodeRepository/CodeRepository'; import CodeRepositoryFile from 'CommonServer/Utils/CodeRepository/CodeRepositoryFile'; import ServiceRepository from 'Model/Models/ServiceRepository'; @@ -10,10 +12,20 @@ export default class ServiceRepositoryUtil { }): Promise> { const { serviceRepository } = data; + if (!serviceRepository.serviceCatalog?.serviceLanguage) { + throw new BadDataException( + 'Service language is not defined in the service catalog' + ); + } + const allFiles: Dictionary = await CodeRepositoryCommonServerUtil.getFilesInDirectoryRecursive({ repoPath: GetLocalRepositoryPath(), directoryPath: serviceRepository.servicePathInRepository || '.', + acceptedFileExtensions: + ServiceFileTypesUtil.getFileExtentionsByServiceLanguage( + serviceRepository.serviceCatalog!.serviceLanguage! + ), }); return allFiles; diff --git a/Dashboard/src/Pages/ServiceCatalog/ServiceCatalog.tsx b/Dashboard/src/Pages/ServiceCatalog/ServiceCatalog.tsx index 8472f2148b..4afea16209 100644 --- a/Dashboard/src/Pages/ServiceCatalog/ServiceCatalog.tsx +++ b/Dashboard/src/Pages/ServiceCatalog/ServiceCatalog.tsx @@ -5,6 +5,7 @@ import PageMap from '../../Utils/PageMap'; import RouteMap, { RouteUtil } from '../../Utils/RouteMap'; import PageComponentProps from '../PageComponentProps'; import Route from 'Common/Types/API/Route'; +import ServiceLanguage from 'Common/Types/ServiceCatalog/ServiceLanguage'; import FormFieldSchemaType from 'CommonUI/src/Components/Forms/Types/FormFieldSchemaType'; import ModelTable from 'CommonUI/src/Components/ModelTable/ModelTable'; import Page from 'CommonUI/src/Components/Page/Page'; @@ -12,7 +13,7 @@ import FieldType from 'CommonUI/src/Components/Types/FieldType'; import DropdownUtil from 'CommonUI/src/Utils/Dropdown'; import Navigation from 'CommonUI/src/Utils/Navigation'; import Label from 'Model/Models/Label'; -import ServiceCatalog, { ServiceLanguage } from 'Model/Models/ServiceCatalog'; +import ServiceCatalog from 'Model/Models/ServiceCatalog'; import React, { Fragment, FunctionComponent, ReactElement } from 'react'; const ServiceCatalogPage: FunctionComponent = ( diff --git a/Dashboard/src/Pages/ServiceCatalog/View/Index.tsx b/Dashboard/src/Pages/ServiceCatalog/View/Index.tsx index 9fc9a8957d..cb23c16a19 100644 --- a/Dashboard/src/Pages/ServiceCatalog/View/Index.tsx +++ b/Dashboard/src/Pages/ServiceCatalog/View/Index.tsx @@ -1,13 +1,14 @@ import LabelsElement from '../../../Components/Label/Labels'; import PageComponentProps from '../../PageComponentProps'; import ObjectID from 'Common/Types/ObjectID'; +import ServiceLanguage from 'Common/Types/ServiceCatalog/ServiceLanguage'; import FormFieldSchemaType from 'CommonUI/src/Components/Forms/Types/FormFieldSchemaType'; import CardModelDetail from 'CommonUI/src/Components/ModelDetail/CardModelDetail'; import FieldType from 'CommonUI/src/Components/Types/FieldType'; import DropdownUtil from 'CommonUI/src/Utils/Dropdown'; import Navigation from 'CommonUI/src/Utils/Navigation'; import Label from 'Model/Models/Label'; -import ServiceCatalog, { ServiceLanguage } from 'Model/Models/ServiceCatalog'; +import ServiceCatalog from 'Model/Models/ServiceCatalog'; import React, { Fragment, FunctionComponent, ReactElement } from 'react'; const StatusPageView: FunctionComponent = ( diff --git a/Model/Models/ServiceCatalog.ts b/Model/Models/ServiceCatalog.ts index 0a8b6eb612..d80831658e 100644 --- a/Model/Models/ServiceCatalog.ts +++ b/Model/Models/ServiceCatalog.ts @@ -23,6 +23,7 @@ import UniqueColumnBy from 'Common/Types/Database/UniqueColumnBy'; import IconProp from 'Common/Types/Icon/IconProp'; import ObjectID from 'Common/Types/ObjectID'; import Permission from 'Common/Types/Permission'; +import ServiceLanguage from 'Common/Types/ServiceCatalog/ServiceLanguage'; import { Column, Entity, @@ -33,24 +34,6 @@ import { ManyToOne, } from 'typeorm'; -export enum ServiceLanguage { - NodeJS = 'NodeJS', - Python = 'Python', - Ruby = 'Ruby', - Go = 'Go', - Java = 'Java', - PHP = 'PHP', - CSharp = 'C#', - CPlusPlus = 'C++', - Rust = 'Rust', - Swift = 'Swift', - Kotlin = 'Kotlin', - TypeScript = 'TypeScript', - JavaScript = 'JavaScript', - Shell = 'Shell', - Other = 'Other', -} - @AccessControlColumn('labels') @EnableDocumentation() @TenantColumn('projectId') From 21973401fbfef33278dbbfbb0a40a3d123ced4c0 Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Wed, 12 Jun 2024 22:07:01 +0100 Subject: [PATCH 03/11] refactor: Improve file extension handling in CodeRepositoryUtil The CodeRepositoryUtil class in CodeRepository.ts has been refactored to improve the handling of file extensions. The changes include: - Simplifying the logic for skipping files with unsupported extensions - Adding support for accepted file extensions in the getFilesInDirectory method These improvements enhance the functionality and maintainability of the code. --- .../Utils/CodeRepository/CodeRepository.ts | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/CommonServer/Utils/CodeRepository/CodeRepository.ts b/CommonServer/Utils/CodeRepository/CodeRepository.ts index 8f37ad5dcc..09aff756a6 100644 --- a/CommonServer/Utils/CodeRepository/CodeRepository.ts +++ b/CommonServer/Utils/CodeRepository/CodeRepository.ts @@ -57,23 +57,7 @@ export default class CodeRepositoryUtil { continue; } - if ( - data.acceptedFileExtensions && - data.acceptedFileExtensions.length > 0 - ) { - let shouldSkip: boolean = true; - - for (const fileExtension of data.acceptedFileExtensions) { - if (fileName.endsWith(fileExtension)) { - shouldSkip = false; - break; - } - } - - if (shouldSkip) { - continue; - } - } + const filePath: string = LocalFile.sanitizeFilePath( `${directoryPath}/${fileName}` @@ -92,6 +76,24 @@ export default class CodeRepositoryUtil { LocalFile.sanitizeFilePath(`${directoryPath}/${fileName}`) ); continue; + }else{ + if ( + data.acceptedFileExtensions && + data.acceptedFileExtensions.length > 0 + ) { + let shouldSkip: boolean = true; + + for (const fileExtension of data.acceptedFileExtensions) { + if (fileName.endsWith(fileExtension)) { + shouldSkip = false; + break; + } + } + + if (shouldSkip) { + continue; + } + } } const gitCommitHash: string = await this.getGitCommitHashForFile({ @@ -127,6 +129,7 @@ export default class CodeRepositoryUtil { await this.getFilesInDirectory({ directoryPath: data.directoryPath, repoPath: data.repoPath, + acceptedFileExtensions: data.acceptedFileExtensions, }); files = { From dea385ad4404e7e28e394170de4240ed55203328 Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Wed, 12 Jun 2024 22:10:47 +0100 Subject: [PATCH 04/11] refactor: Update ServiceFileTypesUtil to include common directories and files to ignore The ServiceFileTypesUtil class in ServiceFileTypes.ts has been updated to include two new methods: getCommonDirectoriesToIgnore and getCommonFilesToIgnore. These methods return arrays of common directories and files that should be ignored in code repositories. This change improves the functionality and maintainability of the code by providing a centralized way to define and retrieve the directories and files to ignore. --- Copilot/Utils/ServiceFileTypes.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Copilot/Utils/ServiceFileTypes.ts b/Copilot/Utils/ServiceFileTypes.ts index b0276e320a..0c4029a328 100644 --- a/Copilot/Utils/ServiceFileTypes.ts +++ b/Copilot/Utils/ServiceFileTypes.ts @@ -1,6 +1,15 @@ import ServiceLanguage from 'Common/Types/ServiceCatalog/ServiceLanguage'; export default class ServiceFileTypesUtil { + + public static getCommonDirectoriesToIgnore(): string[] { + return ['node_modules', '.git', 'build', 'dist', 'coverage']; + } + + public static getCommonFilesToIgnore(): string[] { + return ['.DS_Store', 'Thumbs.db', '.gitignore', '.gitattributes']; + } + public static getCommonFilesExtentions(): string[] { // return markdown, dockerfile, etc. return ['.md', 'dockerfile', '.yml', '.yaml', '.sh', '.gitignore']; From 96d413161424db1912ac87fe0b412390d9f2d950 Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Thu, 13 Jun 2024 12:26:49 +0100 Subject: [PATCH 05/11] 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. --- .../Utils/CodeRepository/CodeRepository.ts | 140 +++++++++++++- .../Utils/CodeRepository/GitHub/GitHub.ts | 43 +++++ .../HostedCodeRepository.ts | 11 ++ Copilot/Index.ts | 55 +++++- Copilot/Utils/CodeRepository.ts | 175 +++++++++++++++++- Copilot/Utils/ServiceFileTypes.ts | 69 ++++++- Copilot/Utils/ServiceRepository.ts | 3 + 7 files changed, 483 insertions(+), 13 deletions(-) diff --git a/CommonServer/Utils/CodeRepository/CodeRepository.ts b/CommonServer/Utils/CodeRepository/CodeRepository.ts index 09aff756a6..6f313639ca 100644 --- a/CommonServer/Utils/CodeRepository/CodeRepository.ts +++ b/CommonServer/Utils/CodeRepository/CodeRepository.ts @@ -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 { + 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 { + await Execute.executeCommand( + `cd ${data.repoPath} && git checkout .` + ); + } + + public static async writeToFile(data: { + filePath: string; + repoPath: string; + content: string; + }): Promise { + + 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 { + + const totalPath: string = LocalFile.sanitizeFilePath(`${data.repoPath}/${data.directoryPath}`); + + await Execute.executeCommand( + `mkdir ${totalPath}` + ); + } + + public static async deleteFile(data: { + repoPath: string; + filePath: string; + }): Promise { + + const totalPath: string = LocalFile.sanitizeFilePath(`${data.repoPath}/${data.filePath}`); + + await Execute.executeCommand( + `rm ${totalPath}` + ); + } + + public static async deleteDirectory(data: { + repoPath: string; + directoryPath: string; + }): Promise { + + 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 { + await Execute.executeCommand( + `cd ${data.repoPath} && git checkout -b ${data.branchName}` + ); + } + + public static async checkoutBranch(data: { + repoPath: string; + branchName: string; + }): Promise { + await Execute.executeCommand( + `cd ${data.repoPath} && git checkout ${data.branchName}` + ); + } + + public static async addFilesToGit(data: { + repoPath: string; + filePaths: Array; + }): Promise { + + 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 { + await Execute.executeCommand( + `cd ${data.repoPath} && git commit -m "${data.message}"` + ); + } + + public static async pushChanges(data: { + repoPath: string; + branchName: string; + remoteName?: string | undefined; + }): Promise { + + 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; + ignoreFilesOrDirectories: Array; }): Promise<{ files: Dictionary; subDirectories: Array; @@ -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; + ignoreFilesOrDirectories: Array; }): Promise> { let files: Dictionary = {}; @@ -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, })), }; } diff --git a/CommonServer/Utils/CodeRepository/GitHub/GitHub.ts b/CommonServer/Utils/CodeRepository/GitHub/GitHub.ts index 081e3cb8fa..20094c7cf2 100644 --- a/CommonServer/Utils/CodeRepository/GitHub/GitHub.ts +++ b/CommonServer/Utils/CodeRepository/GitHub/GitHub.ts @@ -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 { + const gitHubToken: string = this.authToken; + + const url: URL = URL.fromString( + `https://api.github.com/repos/${data.organizationName}/${data.repositoryName}/pulls` + ); + + const result: HTTPErrorResponse | HTTPResponse = + 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, + }); + } } diff --git a/CommonServer/Utils/CodeRepository/HostedCodeRepository/HostedCodeRepository.ts b/CommonServer/Utils/CodeRepository/HostedCodeRepository/HostedCodeRepository.ts index 47877039a0..e7194dd54d 100644 --- a/CommonServer/Utils/CodeRepository/HostedCodeRepository/HostedCodeRepository.ts +++ b/CommonServer/Utils/CodeRepository/HostedCodeRepository/HostedCodeRepository.ts @@ -78,4 +78,15 @@ export default class HostedCodeRepository { }): Promise> { throw new NotImplementedException(); } + + public async createPullRequest(_data: { + baseBranchName: string; + headBranchName: string; + organizationName: string; + repositoryName: string; + title: string; + body: string; + }): Promise { + throw new NotImplementedException(); + } } diff --git a/Copilot/Index.ts b/Copilot/Index.ts index 59af71b4c7..dd0d665602 100644 --- a/Copilot/Index.ts +++ b/Copilot/Index.ts @@ -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 => { }); 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) { diff --git a/Copilot/Utils/CodeRepository.ts b/Copilot/Utils/CodeRepository.ts index 7e70d5ae3c..b60c30953c 100644 --- a/Copilot/Utils/CodeRepository.ts +++ b/Copilot/Utils/CodeRepository.ts @@ -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 { + await CodeRepositoryServerUtil.createBranch({ + repoPath: GetLocalRepositoryPath(), + branchName: data.branchName, + }) + } + + public static async createOrCheckoutBranch(data: { + branchName: string; + }): Promise { + await CodeRepositoryServerUtil.createOrCheckoutBranch({ + repoPath: GetLocalRepositoryPath(), + branchName: data.branchName, + }) + } + + public static async writeToFile(data: { + filePath: string; + content: string; + }): Promise { + await CodeRepositoryServerUtil.writeToFile({ + repoPath: GetLocalRepositoryPath(), + filePath: data.filePath, + content: data.content, + }) + } + + public static async createDirectory(data: { + directoryPath: string; + }): Promise { + await CodeRepositoryServerUtil.createDirectory({ + repoPath: GetLocalRepositoryPath(), + directoryPath: data.directoryPath, + }) + } + + public static async deleteFile(data: { + filePath: string; + }): Promise { + await CodeRepositoryServerUtil.deleteFile({ + repoPath: GetLocalRepositoryPath(), + filePath: data.filePath, + }) + } + + public static async deleteDirectory(data: { + directoryPath: string; + }): Promise { + await CodeRepositoryServerUtil.deleteDirectory({ + repoPath: GetLocalRepositoryPath(), + directoryPath: data.directoryPath, + }) + } + + public static async discardChanges(): Promise { + await CodeRepositoryServerUtil.discardChanges({ + repoPath: GetLocalRepositoryPath(), + }) + } + + public static async checkoutBranch(data: { + branchName: string; + }): Promise { + await CodeRepositoryServerUtil.checkoutBranch({ + repoPath: GetLocalRepositoryPath(), + branchName: data.branchName, + }) + } + + public static async checkoutMainBranch(): Promise { + 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; + }): Promise { + await CodeRepositoryServerUtil.addFilesToGit({ + repoPath: GetLocalRepositoryPath(), + filePaths: data.filePaths, + }) + } + + public static async commitChanges(data: { + message: string; + }): Promise { + await CodeRepositoryServerUtil.commitChanges({ + repoPath: GetLocalRepositoryPath(), + message: data.message, + }) + } + + public static async pushChanges(data: { + branchName: string; + }): Promise { + + await CodeRepositoryServerUtil.pushChanges({ + repoPath: GetLocalRepositoryPath(), + branchName: data.branchName, + }) + } + + + public static async createPullRequest(data: { + branchName: string; + title: string; + body: string; + }): Promise { + + 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, diff --git a/Copilot/Utils/ServiceFileTypes.ts b/Copilot/Utils/ServiceFileTypes.ts index 0c4029a328..e687d07ab4 100644 --- a/Copilot/Utils/ServiceFileTypes.ts +++ b/Copilot/Utils/ServiceFileTypes.ts @@ -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']; } diff --git a/Copilot/Utils/ServiceRepository.ts b/Copilot/Utils/ServiceRepository.ts index 34e3c0abe0..562b0a5f02 100644 --- a/Copilot/Utils/ServiceRepository.ts +++ b/Copilot/Utils/ServiceRepository.ts @@ -26,6 +26,9 @@ export default class ServiceRepositoryUtil { ServiceFileTypesUtil.getFileExtentionsByServiceLanguage( serviceRepository.serviceCatalog!.serviceLanguage! ), + ignoreFilesOrDirectories: ServiceFileTypesUtil.getCommonFilesToIgnoreByServiceLanguage( + serviceRepository.serviceCatalog!.serviceLanguage! + ) }); return allFiles; From a73b050a4d0bedd9a33e553326746c0545b89c98 Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Thu, 13 Jun 2024 12:47:42 +0100 Subject: [PATCH 06/11] refactor: Update CodeRepositoryUtil to include serviceRepository parameter in createBranch and createOrCheckoutBranch This commit modifies the CodeRepositoryUtil class in CodeRepository.ts to include a new parameter, serviceRepository, in the createBranch and createOrCheckoutBranch methods. This change allows for better integration with the ServiceRepository class and improves the flexibility and maintainability of the code. --- .../Utils/CodeRepository/CodeRepository.ts | 130 ++++++++++++++---- Copilot/Index.ts | 11 +- Copilot/Utils/CodeRepository.ts | 12 +- 3 files changed, 123 insertions(+), 30 deletions(-) diff --git a/CommonServer/Utils/CodeRepository/CodeRepository.ts b/CommonServer/Utils/CodeRepository/CodeRepository.ts index 6f313639ca..b76c45b867 100644 --- a/CommonServer/Utils/CodeRepository/CodeRepository.ts +++ b/CommonServer/Utils/CodeRepository/CodeRepository.ts @@ -1,5 +1,6 @@ import Execute from '../Execute'; import LocalFile from '../LocalFile'; +import logger from '../Logger'; import CodeRepositoryFile from './CodeRepositoryFile'; import Dictionary from 'Common/Types/Dictionary'; @@ -9,9 +10,16 @@ export default class CodeRepositoryUtil { repoPath: string; branchName: string; }): Promise { - await Execute.executeCommand( - `cd ${data.repoPath} && git checkout ${data.branchName} || git checkout -b ${data.branchName}` + + const command: string = `cd ${data.repoPath} && git checkout ${data.branchName} || git checkout -b ${data.branchName}`; + + logger.debug("Executing command: " + command); + + const stdout = await Execute.executeCommand( + command ); + + logger.debug(stdout); } @@ -19,9 +27,16 @@ export default class CodeRepositoryUtil { public static async discardChanges(data: { repoPath: string; }): Promise { - await Execute.executeCommand( - `cd ${data.repoPath} && git checkout .` + + const command: string = `cd ${data.repoPath} && git checkout .`; + + logger.debug("Executing command: " + command); + + const stdout = await Execute.executeCommand( + command ); + + logger.debug(stdout); } public static async writeToFile(data: { @@ -32,9 +47,15 @@ export default class CodeRepositoryUtil { const totalPath: string = LocalFile.sanitizeFilePath(`${data.repoPath}/${data.filePath}`); - await Execute.executeCommand( - `echo "${data.content}" > ${totalPath}` + const command: string = `echo "${data.content}" > ${totalPath}`; + + logger.debug("Executing command: " + command); + + const stdout = await Execute.executeCommand( + command ); + + logger.debug(stdout); } public static async createDirectory(data: { @@ -44,9 +65,15 @@ export default class CodeRepositoryUtil { const totalPath: string = LocalFile.sanitizeFilePath(`${data.repoPath}/${data.directoryPath}`); - await Execute.executeCommand( - `mkdir ${totalPath}` + const command: string = `mkdir ${totalPath}`; + + logger.debug("Executing command: " + command); + + const stdout = await Execute.executeCommand( + command ); + + logger.debug(stdout); } public static async deleteFile(data: { @@ -56,9 +83,16 @@ export default class CodeRepositoryUtil { const totalPath: string = LocalFile.sanitizeFilePath(`${data.repoPath}/${data.filePath}`); - await Execute.executeCommand( - `rm ${totalPath}` + + const command: string = `rm ${totalPath}`; + + logger.debug("Executing command: " + command); + + const stdout = await Execute.executeCommand( + command ); + + logger.debug(stdout); } public static async deleteDirectory(data: { @@ -68,9 +102,15 @@ export default class CodeRepositoryUtil { const totalPath: string = LocalFile.sanitizeFilePath(`${data.repoPath}/${data.directoryPath}`); - await Execute.executeCommand( - `rm -rf ${totalPath}` + const command: string = `rm -rf ${totalPath}`; + + logger.debug("Executing command: " + command); + + const stdout = await Execute.executeCommand( + command ); + + logger.debug(stdout); } @@ -78,18 +118,32 @@ export default class CodeRepositoryUtil { repoPath: string; branchName: string; }): Promise { - await Execute.executeCommand( - `cd ${data.repoPath} && git checkout -b ${data.branchName}` + + const command: string = `cd ${data.repoPath} && git checkout -b ${data.branchName}`; + + logger.debug("Executing command: " + command); + + const stdout = await Execute.executeCommand( + command ); + + logger.debug(stdout); } public static async checkoutBranch(data: { repoPath: string; branchName: string; }): Promise { - await Execute.executeCommand( - `cd ${data.repoPath} && git checkout ${data.branchName}` + + const command: string = `cd ${data.repoPath} && git checkout ${data.branchName}`; + + logger.debug("Executing command: " + command); + + const stdout = await Execute.executeCommand( + command ); + + logger.debug(stdout); } public static async addFilesToGit(data: { @@ -106,18 +160,32 @@ export default class CodeRepositoryUtil { } }); - await Execute.executeCommand( - `cd ${data.repoPath} && git add ${filePaths.join(' ')}` + + const command: string = `cd ${data.repoPath} && git add ${filePaths.join(' ')}`; + + logger.debug("Executing command: " + command); + + const stdout = await Execute.executeCommand( + command ); + + logger.debug(stdout); } public static async commitChanges(data: { repoPath: string; message: string; }): Promise { - await Execute.executeCommand( - `cd ${data.repoPath} && git commit -m "${data.message}"` + + const command: string = `cd ${data.repoPath} && git commit -m "${data.message}"`; + + logger.debug("Executing command: " + command); + + const stdout = await Execute.executeCommand( + command ); + + logger.debug(stdout); } public static async pushChanges(data: { @@ -128,9 +196,15 @@ export default class CodeRepositoryUtil { const remoteName: string = data.remoteName || 'origin'; - await Execute.executeCommand( - `cd ${data.repoPath} && git push ${remoteName} ${data.branchName}` + const command: string = `cd ${data.repoPath} && git push ${remoteName} ${data.branchName}`; + + logger.debug("Executing command: " + command); + + const stdout = await Execute.executeCommand( + command ); + + logger.debug(stdout); } public static async getGitCommitHashForFile(data: { @@ -147,9 +221,17 @@ export default class CodeRepositoryUtil { const { repoPath, filePath } = data; - return await Execute.executeCommand( - `cd ${repoPath} && git log -1 --pretty=format:"%H" ".${filePath}"` + const command: string = `cd ${repoPath} && git log -1 --pretty=format:"%H" ".${filePath}"`; + + logger.debug("Executing command: " + command); + + const hash: string = await Execute.executeCommand( + command ); + + logger.debug(hash); + + return hash; } public static async getFilesInDirectory(data: { diff --git a/Copilot/Index.ts b/Copilot/Index.ts index dd0d665602..1bbe54b695 100644 --- a/Copilot/Index.ts +++ b/Copilot/Index.ts @@ -26,9 +26,12 @@ const init: PromiseVoidFunction = async (): Promise => { }` ); + const branchName = 'test-branch-3'; - await CodeRepositoryUtil.createBranch({ - branchName: 'test-branch', + + await CodeRepositoryUtil.createOrCheckoutBranch({ + serviceRepository: serviceRepository, + branchName: branchName, }); // test code from here. @@ -50,7 +53,7 @@ const init: PromiseVoidFunction = async (): Promise => { }); await CodeRepositoryUtil.pushChanges({ - branchName: 'test-branch', + branchName: branchName, }); // create a pull request @@ -58,7 +61,7 @@ const init: PromiseVoidFunction = async (): Promise => { await CodeRepositoryUtil.createPullRequest({ title: 'Test PR', body: 'Test PR body', - branchName: 'test-branch', + branchName: branchName, }); } diff --git a/Copilot/Utils/CodeRepository.ts b/Copilot/Utils/CodeRepository.ts index b60c30953c..c2aafd10ff 100644 --- a/Copilot/Utils/CodeRepository.ts +++ b/Copilot/Utils/CodeRepository.ts @@ -46,19 +46,27 @@ export default class CodeRepositoryUtil { public static async createBranch(data: { branchName: string; + serviceRepository: ServiceRepository; }): Promise { + + const branchName = 'oneuptime-'+(data.serviceRepository.serviceCatalog?.name?.toLowerCase())+'-'+data.branchName; + await CodeRepositoryServerUtil.createBranch({ repoPath: GetLocalRepositoryPath(), - branchName: data.branchName, + branchName: branchName, }) } public static async createOrCheckoutBranch(data: { + serviceRepository: ServiceRepository; branchName: string; }): Promise { + + const branchName = 'oneuptime-'+(data.serviceRepository.serviceCatalog?.name?.toLowerCase())+'-'+data.branchName; + await CodeRepositoryServerUtil.createOrCheckoutBranch({ repoPath: GetLocalRepositoryPath(), - branchName: data.branchName, + branchName: branchName, }) } From a4062872159f08831812ef08e7444d322a9efe1f Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Thu, 13 Jun 2024 13:10:14 +0100 Subject: [PATCH 07/11] refactor: Update branch name in Copilot/Index.ts and add GITHUB_USERNAME to .env.example This commit updates the branch name in Copilot/Index.ts from 'test-branch-3' to 'test-branch-4'. Additionally, it adds the GITHUB_USERNAME variable to the .env.example file. These changes improve the accuracy and functionality of the code. --- .../Utils/CodeRepository/CodeRepository.ts | 19 ------ .../Utils/CodeRepository/GitHub/GitHub.ts | 40 +++++++++++++ .../HostedCodeRepository.ts | 28 ++++++++- Copilot/.env.example | 1 + Copilot/Config.ts | 5 ++ Copilot/Index.ts | 2 +- Copilot/Utils/CodeRepository.ts | 59 ++++++++++++++----- 7 files changed, 118 insertions(+), 36 deletions(-) diff --git a/CommonServer/Utils/CodeRepository/CodeRepository.ts b/CommonServer/Utils/CodeRepository/CodeRepository.ts index b76c45b867..623cb9b3f5 100644 --- a/CommonServer/Utils/CodeRepository/CodeRepository.ts +++ b/CommonServer/Utils/CodeRepository/CodeRepository.ts @@ -188,25 +188,6 @@ export default class CodeRepositoryUtil { logger.debug(stdout); } - public static async pushChanges(data: { - repoPath: string; - branchName: string; - remoteName?: string | undefined; - }): Promise { - - const remoteName: string = data.remoteName || 'origin'; - - const command: string = `cd ${data.repoPath} && git push ${remoteName} ${data.branchName}`; - - logger.debug("Executing command: " + command); - - const stdout = await Execute.executeCommand( - command - ); - - logger.debug(stdout); - } - public static async getGitCommitHashForFile(data: { repoPath: string; filePath: string; diff --git a/CommonServer/Utils/CodeRepository/GitHub/GitHub.ts b/CommonServer/Utils/CodeRepository/GitHub/GitHub.ts index 20094c7cf2..64039aec13 100644 --- a/CommonServer/Utils/CodeRepository/GitHub/GitHub.ts +++ b/CommonServer/Utils/CodeRepository/GitHub/GitHub.ts @@ -7,6 +7,8 @@ import PullRequestState from 'Common/Types/CodeRepository/PullRequestState'; import OneUptimeDate from 'Common/Types/Date'; import { JSONArray, JSONObject } from 'Common/Types/JSON'; import API from 'Common/Utils/API'; +import Execute from '../../Execute'; +import logger from '../../Logger'; export default class GitHubUtil extends HostedCodeRepository { @@ -152,6 +154,44 @@ export default class GitHubUtil extends HostedCodeRepository { } + public override async addRemote(data: { remoteName: string; organizationName: string; repositoryName: string; }): Promise { + + const url: URL = URL.fromString( + `https://github.com/${data.organizationName}/${data.repositoryName}.git` + ); + + const command: string = `git remote add ${data.remoteName} ${url.toString()}`; + + logger.debug("Executing command: " + command); + + const result: string = await Execute.executeCommand(command); + + logger.debug(result); + } + + + public override async pushChanges(data: { + branchName: string; + organizationName: string; + repoName: string; + }){ + + const branchName: string = data.branchName; + + const username: string = this.username; + const password: string = this.authToken; + + logger.debug("Pushing changes to remote repository with username: " + username); + + const command: string = `git push -u https://${username}:${password}@github.com/${data.organizationName}/${data.repositoryName}.git ${branchName}`; + logger.debug("Executing command: " + command); + + const result: string = await Execute.executeCommand(command); + + logger.debug(result); + } + + public override async createPullRequest(data: { baseBranchName: string; headBranchName: string; diff --git a/CommonServer/Utils/CodeRepository/HostedCodeRepository/HostedCodeRepository.ts b/CommonServer/Utils/CodeRepository/HostedCodeRepository/HostedCodeRepository.ts index e7194dd54d..852ea5b347 100644 --- a/CommonServer/Utils/CodeRepository/HostedCodeRepository/HostedCodeRepository.ts +++ b/CommonServer/Utils/CodeRepository/HostedCodeRepository/HostedCodeRepository.ts @@ -5,15 +5,25 @@ import NotImplementedException from 'Common/Types/Exception/NotImplementedExcept import ServiceRepository from 'Model/Models/ServiceRepository'; export default class HostedCodeRepository { - public constructor(data: { authToken: string }) { + public constructor(data: { + authToken: string, + username: string, + }) { if (!data.authToken) { throw new BadDataException('authToken is required'); } + if (!data.username) { + throw new BadDataException('username is required'); + } + + this.username = data.username; + this.authToken = data.authToken; } public authToken: string = ''; + public username: string = ''; public async getNumberOfPullRequestsExistForService(data: { serviceRepository: ServiceRepository; @@ -89,4 +99,20 @@ export default class HostedCodeRepository { }): Promise { throw new NotImplementedException(); } + + public async pushChanges(_data: { + branchName: string; + organizationName: string; + repoName: string; + }): Promise { + throw new NotImplementedException(); + } + + public async addRemote(_data: { + remoteName: string; + organizationName: string; + repositoryName: string; + }): Promise { + throw new NotImplementedException(); + } } diff --git a/Copilot/.env.example b/Copilot/.env.example index 1459d90e7b..3e74c022e3 100644 --- a/Copilot/.env.example +++ b/Copilot/.env.example @@ -2,3 +2,4 @@ ONEUPTIME_URL=https://oneuptime.com ONEUPTIME_REPOSITORY_SECRET_KEY=your-repository-secret-key ONEUPTIME_LOCAL_REPOSITORY_PATH=/repository GITHUB_TOKEN= +GITHUB_USERNAME= diff --git a/Copilot/Config.ts b/Copilot/Config.ts index b8a3393632..58f77f67a8 100644 --- a/Copilot/Config.ts +++ b/Copilot/Config.ts @@ -24,3 +24,8 @@ export const GetGitHubToken: GetStringOrNullFunction = (): string | null => { const token: string | null = process.env['GITHUB_TOKEN'] || null; return token; }; + +export const GetGitHubUsername: GetStringOrNullFunction = (): string | null => { + const username: string | null = process.env['GITHUB_USERNAME'] || null; + return username; +} diff --git a/Copilot/Index.ts b/Copilot/Index.ts index 1bbe54b695..c2d236f153 100644 --- a/Copilot/Index.ts +++ b/Copilot/Index.ts @@ -26,7 +26,7 @@ const init: PromiseVoidFunction = async (): Promise => { }` ); - const branchName = 'test-branch-3'; + const branchName = 'test-branch-4'; await CodeRepositoryUtil.createOrCheckoutBranch({ diff --git a/Copilot/Utils/CodeRepository.ts b/Copilot/Utils/CodeRepository.ts index c2aafd10ff..eb3306c076 100644 --- a/Copilot/Utils/CodeRepository.ts +++ b/Copilot/Utils/CodeRepository.ts @@ -1,5 +1,6 @@ import { GetGitHubToken, + GetGitHubUsername, GetLocalRepositoryPath, GetOneUptimeURL, GetRepositorySecretKey, @@ -28,16 +29,26 @@ 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(); + const gitHubUsername: string | null = GetGitHubUsername(); + + if (!gitHubUsername) { + throw new BadDataException('GitHub Username is required'); + } + + if (!gitHubToken) { throw new BadDataException('GitHub Token is required'); } + this.gitHubUtil = new GitHubUtil({ authToken: gitHubToken, + username: gitHubUsername!, }); } @@ -49,7 +60,7 @@ export default class CodeRepositoryUtil { serviceRepository: ServiceRepository; }): Promise { - const branchName = 'oneuptime-'+(data.serviceRepository.serviceCatalog?.name?.toLowerCase())+'-'+data.branchName; + const branchName = 'oneuptime-' + (data.serviceRepository.serviceCatalog?.name?.toLowerCase()) + '-' + data.branchName; await CodeRepositoryServerUtil.createBranch({ repoPath: GetLocalRepositoryPath(), @@ -62,7 +73,7 @@ export default class CodeRepositoryUtil { branchName: string; }): Promise { - const branchName = 'oneuptime-'+(data.serviceRepository.serviceCatalog?.name?.toLowerCase())+'-'+data.branchName; + const branchName = 'oneuptime-' + (data.serviceRepository.serviceCatalog?.name?.toLowerCase()) + '-' + data.branchName; await CodeRepositoryServerUtil.createOrCheckoutBranch({ repoPath: GetLocalRepositoryPath(), @@ -108,7 +119,7 @@ export default class CodeRepositoryUtil { }) } - public static async discardChanges(): Promise { + public static async discardChanges(): Promise { await CodeRepositoryServerUtil.discardChanges({ repoPath: GetLocalRepositoryPath(), }) @@ -117,7 +128,7 @@ export default class CodeRepositoryUtil { public static async checkoutBranch(data: { branchName: string; }): Promise { - await CodeRepositoryServerUtil.checkoutBranch({ + await CodeRepositoryServerUtil.checkoutBranch({ repoPath: GetLocalRepositoryPath(), branchName: data.branchName, }) @@ -157,10 +168,28 @@ export default class CodeRepositoryUtil { branchName: string; }): Promise { - await CodeRepositoryServerUtil.pushChanges({ - repoPath: GetLocalRepositoryPath(), - branchName: data.branchName, - }) + 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().pushChanges({ + branchName: data.branchName, + organizationName: codeRepository.organizationName, + repoName: codeRepository.repositoryName, + }); + } } @@ -172,22 +201,22 @@ export default class CodeRepositoryUtil { const codeRepository: CodeRepositoryModel = await this.getCodeRepository(); - if(!codeRepository.mainBranchName){ + if (!codeRepository.mainBranchName) { throw new BadDataException('Main Branch Name is required'); } - if(!codeRepository.organizationName){ + if (!codeRepository.organizationName) { throw new BadDataException('Organization Name is required'); } - if(!codeRepository.repositoryName){ + if (!codeRepository.repositoryName) { throw new BadDataException('Repository Name is required'); } - if(codeRepository.repositoryHostedAt === CodeRepositoryType.GitHub){ + if (codeRepository.repositoryHostedAt === CodeRepositoryType.GitHub) { return await this.getGitHubUtil().createPullRequest({ - headBranchName: data.branchName, + headBranchName: data.branchName, baseBranchName: codeRepository.mainBranchName, organizationName: codeRepository.organizationName, repositoryName: codeRepository.repositoryName, @@ -195,10 +224,10 @@ export default class CodeRepositoryUtil { body: data.body, }); - }else{ + } else { throw new BadDataException('Code Repository type not supported'); } - + } From 17c47f7d8908316f22416c4154589081b055f977 Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Thu, 13 Jun 2024 13:32:16 +0100 Subject: [PATCH 08/11] refactor: Update ServiceFileTypesUtil to include common directories and files to ignore --- .../Utils/CodeRepository/CodeRepository.ts | 154 +++++++--------- .../Utils/CodeRepository/GitHub/GitHub.ts | 34 ++-- .../HostedCodeRepository.ts | 7 +- Copilot/Config.ts | 2 +- Copilot/Index.ts | 31 ++-- Copilot/Utils/CodeRepository.ts | 165 ++++++++++-------- Copilot/Utils/ServiceFileTypes.ts | 34 +++- Copilot/Utils/ServiceRepository.ts | 5 +- 8 files changed, 228 insertions(+), 204 deletions(-) diff --git a/CommonServer/Utils/CodeRepository/CodeRepository.ts b/CommonServer/Utils/CodeRepository/CodeRepository.ts index 623cb9b3f5..a53cbdce64 100644 --- a/CommonServer/Utils/CodeRepository/CodeRepository.ts +++ b/CommonServer/Utils/CodeRepository/CodeRepository.ts @@ -5,36 +5,28 @@ import CodeRepositoryFile from './CodeRepositoryFile'; import Dictionary from 'Common/Types/Dictionary'; export default class CodeRepositoryUtil { - public static async createOrCheckoutBranch(data: { repoPath: string; branchName: string; }): Promise { - const command: string = `cd ${data.repoPath} && git checkout ${data.branchName} || git checkout -b ${data.branchName}`; - logger.debug("Executing command: " + command); + logger.debug('Executing command: ' + command); - const stdout = await Execute.executeCommand( - command - ); + const stdout: string = await Execute.executeCommand(command); logger.debug(stdout); } - // discard all changes in the working directory public static async discardChanges(data: { repoPath: string; }): Promise { - const command: string = `cd ${data.repoPath} && git checkout .`; - logger.debug("Executing command: " + command); + logger.debug('Executing command: ' + command); - const stdout = await Execute.executeCommand( - command - ); + const stdout: string = await Execute.executeCommand(command); logger.debug(stdout); } @@ -44,16 +36,15 @@ export default class CodeRepositoryUtil { repoPath: string; content: string; }): Promise { - - const totalPath: string = LocalFile.sanitizeFilePath(`${data.repoPath}/${data.filePath}`); + const totalPath: string = LocalFile.sanitizeFilePath( + `${data.repoPath}/${data.filePath}` + ); const command: string = `echo "${data.content}" > ${totalPath}`; - logger.debug("Executing command: " + command); + logger.debug('Executing command: ' + command); - const stdout = await Execute.executeCommand( - command - ); + const stdout: string = await Execute.executeCommand(command); logger.debug(stdout); } @@ -62,16 +53,15 @@ export default class CodeRepositoryUtil { repoPath: string; directoryPath: string; }): Promise { + const totalPath: string = LocalFile.sanitizeFilePath( + `${data.repoPath}/${data.directoryPath}` + ); - const totalPath: string = LocalFile.sanitizeFilePath(`${data.repoPath}/${data.directoryPath}`); - const command: string = `mkdir ${totalPath}`; - logger.debug("Executing command: " + command); - - const stdout = await Execute.executeCommand( - command - ); + logger.debug('Executing command: ' + command); + + const stdout: string = await Execute.executeCommand(command); logger.debug(stdout); } @@ -80,17 +70,15 @@ export default class CodeRepositoryUtil { repoPath: string; filePath: string; }): Promise { - - const totalPath: string = LocalFile.sanitizeFilePath(`${data.repoPath}/${data.filePath}`); - + const totalPath: string = LocalFile.sanitizeFilePath( + `${data.repoPath}/${data.filePath}` + ); const command: string = `rm ${totalPath}`; - logger.debug("Executing command: " + command); + logger.debug('Executing command: ' + command); - const stdout = await Execute.executeCommand( - command - ); + const stdout: string = await Execute.executeCommand(command); logger.debug(stdout); } @@ -99,33 +87,28 @@ export default class CodeRepositoryUtil { repoPath: string; directoryPath: string; }): Promise { - - const totalPath: string = LocalFile.sanitizeFilePath(`${data.repoPath}/${data.directoryPath}`); + const totalPath: string = LocalFile.sanitizeFilePath( + `${data.repoPath}/${data.directoryPath}` + ); const command: string = `rm -rf ${totalPath}`; - logger.debug("Executing command: " + command); + logger.debug('Executing command: ' + command); - const stdout = await Execute.executeCommand( - command - ); + const stdout: string = await Execute.executeCommand(command); logger.debug(stdout); } - public static async createBranch(data: { repoPath: string; branchName: string; }): Promise { - const command: string = `cd ${data.repoPath} && git checkout -b ${data.branchName}`; - logger.debug("Executing command: " + command); + logger.debug('Executing command: ' + command); - const stdout = await Execute.executeCommand( - command - ); + const stdout: string = await Execute.executeCommand(command); logger.debug(stdout); } @@ -134,14 +117,11 @@ export default class CodeRepositoryUtil { repoPath: string; branchName: string; }): Promise { - const command: string = `cd ${data.repoPath} && git checkout ${data.branchName}`; - logger.debug("Executing command: " + command); + logger.debug('Executing command: ' + command); - const stdout = await Execute.executeCommand( - command - ); + const stdout: string = await Execute.executeCommand(command); logger.debug(stdout); } @@ -150,25 +130,25 @@ export default class CodeRepositoryUtil { repoPath: string; filePaths: Array; }): Promise { + const filePaths: Array = data.filePaths.map( + (filePath: string) => { + if (filePath.startsWith('/')) { + // remove the leading slash and return + return filePath.substring(1); + } - const filePaths = data.filePaths.map((filePath) => { - if(filePath.startsWith('/')){ - // remove the leading slash and return - return filePath.substring(1); - }else{ return filePath; } - }); - - - const command: string = `cd ${data.repoPath} && git add ${filePaths.join(' ')}`; - - logger.debug("Executing command: " + command); - - const stdout = await Execute.executeCommand( - command ); + const command: string = `cd ${ + data.repoPath + } && git add ${filePaths.join(' ')}`; + + logger.debug('Executing command: ' + command); + + const stdout: string = await Execute.executeCommand(command); + logger.debug(stdout); } @@ -176,14 +156,11 @@ export default class CodeRepositoryUtil { repoPath: string; message: string; }): Promise { - const command: string = `cd ${data.repoPath} && git commit -m "${data.message}"`; - logger.debug("Executing command: " + command); + logger.debug('Executing command: ' + command); - const stdout = await Execute.executeCommand( - command - ); + const stdout: string = await Execute.executeCommand(command); logger.debug(stdout); } @@ -204,11 +181,9 @@ export default class CodeRepositoryUtil { const command: string = `cd ${repoPath} && git log -1 --pretty=format:"%H" ".${filePath}"`; - logger.debug("Executing command: " + command); + logger.debug('Executing command: ' + command); - const hash: string = await Execute.executeCommand( - command - ); + const hash: string = await Execute.executeCommand(command); logger.debug(hash); @@ -250,15 +225,14 @@ export default class CodeRepositoryUtil { continue; } - const filePath: string = LocalFile.sanitizeFilePath( `${directoryPath}/${fileName}` ); - if(data.ignoreFilesOrDirectories.includes(fileName)){ + if (data.ignoreFilesOrDirectories.includes(fileName)) { continue; } - + const isDirectory: boolean = ( await Execute.executeCommand( `file "${LocalFile.sanitizeFilePath( @@ -272,24 +246,22 @@ export default class CodeRepositoryUtil { LocalFile.sanitizeFilePath(`${directoryPath}/${fileName}`) ); continue; - }else{ - if ( - data.acceptedFileExtensions && - data.acceptedFileExtensions.length > 0 - ) { - let shouldSkip: boolean = true; - - for (const fileExtension of data.acceptedFileExtensions) { - if (fileName.endsWith(fileExtension)) { - shouldSkip = false; - break; - } - } - - if (shouldSkip) { - continue; + } else if ( + data.acceptedFileExtensions && + data.acceptedFileExtensions.length > 0 + ) { + let shouldSkip: boolean = true; + + for (const fileExtension of data.acceptedFileExtensions) { + if (fileName.endsWith(fileExtension)) { + shouldSkip = false; + break; } } + + if (shouldSkip) { + continue; + } } const gitCommitHash: string = await this.getGitCommitHashForFile({ diff --git a/CommonServer/Utils/CodeRepository/GitHub/GitHub.ts b/CommonServer/Utils/CodeRepository/GitHub/GitHub.ts index 64039aec13..2afa4225a6 100644 --- a/CommonServer/Utils/CodeRepository/GitHub/GitHub.ts +++ b/CommonServer/Utils/CodeRepository/GitHub/GitHub.ts @@ -1,3 +1,5 @@ +import Execute from '../../Execute'; +import logger from '../../Logger'; import HostedCodeRepository from '../HostedCodeRepository/HostedCodeRepository'; import HTTPErrorResponse from 'Common/Types/API/HTTPErrorResponse'; import HTTPResponse from 'Common/Types/API/HTTPResponse'; @@ -7,11 +9,8 @@ import PullRequestState from 'Common/Types/CodeRepository/PullRequestState'; import OneUptimeDate from 'Common/Types/Date'; import { JSONArray, JSONObject } from 'Common/Types/JSON'; import API from 'Common/Utils/API'; -import Execute from '../../Execute'; -import logger from '../../Logger'; export default class GitHubUtil extends HostedCodeRepository { - private getPullRequestFromJSONObject(data: { pullRequest: JSONObject; organizationName: string; @@ -153,45 +152,48 @@ export default class GitHubUtil extends HostedCodeRepository { return allPullRequests; } - - public override async addRemote(data: { remoteName: string; organizationName: string; repositoryName: string; }): Promise { - + public override async addRemote(data: { + remoteName: string; + organizationName: string; + repositoryName: string; + }): Promise { const url: URL = URL.fromString( `https://github.com/${data.organizationName}/${data.repositoryName}.git` ); - const command: string = `git remote add ${data.remoteName} ${url.toString()}`; + const command: string = `git remote add ${ + data.remoteName + } ${url.toString()}`; - logger.debug("Executing command: " + command); + logger.debug('Executing command: ' + command); const result: string = await Execute.executeCommand(command); logger.debug(result); } - public override async pushChanges(data: { branchName: string; organizationName: string; - repoName: string; - }){ - + repositoryName: string; + }): Promise { const branchName: string = data.branchName; - const username: string = this.username; + const username: string = this.username; const password: string = this.authToken; - logger.debug("Pushing changes to remote repository with username: " + username); + logger.debug( + 'Pushing changes to remote repository with username: ' + username + ); const command: string = `git push -u https://${username}:${password}@github.com/${data.organizationName}/${data.repositoryName}.git ${branchName}`; - logger.debug("Executing command: " + command); + logger.debug('Executing command: ' + command); const result: string = await Execute.executeCommand(command); logger.debug(result); } - public override async createPullRequest(data: { baseBranchName: string; headBranchName: string; diff --git a/CommonServer/Utils/CodeRepository/HostedCodeRepository/HostedCodeRepository.ts b/CommonServer/Utils/CodeRepository/HostedCodeRepository/HostedCodeRepository.ts index 852ea5b347..8066fc00a7 100644 --- a/CommonServer/Utils/CodeRepository/HostedCodeRepository/HostedCodeRepository.ts +++ b/CommonServer/Utils/CodeRepository/HostedCodeRepository/HostedCodeRepository.ts @@ -5,10 +5,7 @@ import NotImplementedException from 'Common/Types/Exception/NotImplementedExcept import ServiceRepository from 'Model/Models/ServiceRepository'; export default class HostedCodeRepository { - public constructor(data: { - authToken: string, - username: string, - }) { + public constructor(data: { authToken: string; username: string }) { if (!data.authToken) { throw new BadDataException('authToken is required'); } @@ -103,7 +100,7 @@ export default class HostedCodeRepository { public async pushChanges(_data: { branchName: string; organizationName: string; - repoName: string; + repositoryName: string; }): Promise { throw new NotImplementedException(); } diff --git a/Copilot/Config.ts b/Copilot/Config.ts index 58f77f67a8..faab4c9414 100644 --- a/Copilot/Config.ts +++ b/Copilot/Config.ts @@ -28,4 +28,4 @@ export const GetGitHubToken: GetStringOrNullFunction = (): string | null => { export const GetGitHubUsername: GetStringOrNullFunction = (): string | null => { const username: string | null = process.env['GITHUB_USERNAME'] || null; return username; -} +}; diff --git a/Copilot/Index.ts b/Copilot/Index.ts index c2d236f153..b92c16c952 100644 --- a/Copilot/Index.ts +++ b/Copilot/Index.ts @@ -1,4 +1,6 @@ -import CodeRepositoryUtil, { 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,30 +24,31 @@ const init: PromiseVoidFunction = async (): Promise => { }); logger.info( - `Files found in ${serviceRepository.serviceCatalog?.name}: ${Object.keys(filesInService).length + `Files found in ${serviceRepository.serviceCatalog?.name}: ${ + Object.keys(filesInService).length }` ); - const branchName = 'test-branch-4'; - + const branchName: string = 'test-branch-5'; await CodeRepositoryUtil.createOrCheckoutBranch({ serviceRepository: serviceRepository, branchName: branchName, }); - // test code from here. - const file = filesInService[Object.keys(filesInService)[0]!]; + // test code from here. + const file: CodeRepositoryFile | undefined = + filesInService[Object.keys(filesInService)[0]!]; await CodeRepositoryUtil.writeToFile({ - filePath: file?.filePath!, + filePath: file!.filePath!, content: 'Hello World', }); // commit the changes await CodeRepositoryUtil.addFilesToGit({ - filePaths: [file?.filePath!], + filePaths: [file!.filePath!], }); await CodeRepositoryUtil.commitChanges({ @@ -54,6 +57,7 @@ const init: PromiseVoidFunction = async (): Promise => { await CodeRepositoryUtil.pushChanges({ branchName: branchName, + serviceRepository: serviceRepository, }); // create a pull request @@ -62,8 +66,8 @@ const init: PromiseVoidFunction = async (): Promise => { title: 'Test PR', body: 'Test PR body', branchName: branchName, + serviceRepository: serviceRepository, }); - } }; @@ -73,15 +77,12 @@ init() }) .catch(async (error: Error | HTTPErrorResponse) => { try { - await CodeRepositoryUtil.discardChanges(); - // change back to main branch. - await CodeRepositoryUtil.checkoutBranch({ - branchName: 'main', - }); + // change back to main branch. + await CodeRepositoryUtil.checkoutMainBranch(); } catch (e) { - // do nothing. + // do nothing. } logger.error('Error in starting OneUptime Copilot: '); diff --git a/Copilot/Utils/CodeRepository.ts b/Copilot/Utils/CodeRepository.ts index eb3306c076..82c9127933 100644 --- a/Copilot/Utils/CodeRepository.ts +++ b/Copilot/Utils/CodeRepository.ts @@ -9,16 +9,16 @@ import HTTPErrorResponse from 'Common/Types/API/HTTPErrorResponse'; import HTTPResponse from 'Common/Types/API/HTTPResponse'; import URL from 'Common/Types/API/URL'; import CodeRepositoryType from 'Common/Types/CodeRepository/CodeRepositoryType'; +import PullRequest from 'Common/Types/CodeRepository/PullRequest'; import PullRequestState from 'Common/Types/CodeRepository/PullRequestState'; import BadDataException from 'Common/Types/Exception/BadDataException'; import { JSONArray, JSONObject } from 'Common/Types/JSON'; import API from 'Common/Utils/API'; +import CodeRepositoryServerUtil from 'CommonServer/Utils/CodeRepository/CodeRepository'; 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; @@ -29,7 +29,6 @@ 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(); @@ -39,13 +38,11 @@ export default class CodeRepositoryUtil { if (!gitHubUsername) { throw new BadDataException('GitHub Username is required'); } - if (!gitHubToken) { throw new BadDataException('GitHub Token is required'); } - this.gitHubUtil = new GitHubUtil({ authToken: gitHubToken, username: gitHubUsername!, @@ -55,30 +52,54 @@ export default class CodeRepositoryUtil { return this.gitHubUtil; } + public static getBranchName(data: { + branchName: string; + serviceRepository: ServiceRepository; + }): string { + if (!data.serviceRepository.serviceCatalog) { + throw new BadDataException('Service Catalog is required'); + } + + if (!data.serviceRepository.serviceCatalog.name) { + throw new BadDataException('Service Catalog Name is required'); + } + + return ( + 'oneuptime-' + + data.serviceRepository.serviceCatalog?.name?.toLowerCase() + + '-' + + data.branchName + ); + } + public static async createBranch(data: { branchName: string; serviceRepository: ServiceRepository; }): Promise { - - const branchName = 'oneuptime-' + (data.serviceRepository.serviceCatalog?.name?.toLowerCase()) + '-' + data.branchName; + const branchName: string = this.getBranchName({ + branchName: data.branchName, + serviceRepository: data.serviceRepository, + }); await CodeRepositoryServerUtil.createBranch({ repoPath: GetLocalRepositoryPath(), branchName: branchName, - }) + }); } public static async createOrCheckoutBranch(data: { serviceRepository: ServiceRepository; branchName: string; }): Promise { - - const branchName = 'oneuptime-' + (data.serviceRepository.serviceCatalog?.name?.toLowerCase()) + '-' + data.branchName; + const branchName: string = this.getBranchName({ + branchName: data.branchName, + serviceRepository: data.serviceRepository, + }); await CodeRepositoryServerUtil.createOrCheckoutBranch({ repoPath: GetLocalRepositoryPath(), branchName: branchName, - }) + }); } public static async writeToFile(data: { @@ -89,7 +110,7 @@ export default class CodeRepositoryUtil { repoPath: GetLocalRepositoryPath(), filePath: data.filePath, content: data.content, - }) + }); } public static async createDirectory(data: { @@ -98,16 +119,14 @@ export default class CodeRepositoryUtil { await CodeRepositoryServerUtil.createDirectory({ repoPath: GetLocalRepositoryPath(), directoryPath: data.directoryPath, - }) + }); } - public static async deleteFile(data: { - filePath: string; - }): Promise { + public static async deleteFile(data: { filePath: string }): Promise { await CodeRepositoryServerUtil.deleteFile({ repoPath: GetLocalRepositoryPath(), filePath: data.filePath, - }) + }); } public static async deleteDirectory(data: { @@ -116,13 +135,13 @@ export default class CodeRepositoryUtil { await CodeRepositoryServerUtil.deleteDirectory({ repoPath: GetLocalRepositoryPath(), directoryPath: data.directoryPath, - }) + }); } public static async discardChanges(): Promise { await CodeRepositoryServerUtil.discardChanges({ repoPath: GetLocalRepositoryPath(), - }) + }); } public static async checkoutBranch(data: { @@ -131,11 +150,12 @@ export default class CodeRepositoryUtil { await CodeRepositoryServerUtil.checkoutBranch({ repoPath: GetLocalRepositoryPath(), branchName: data.branchName, - }) + }); } public static async checkoutMainBranch(): Promise { - const codeRepository: CodeRepositoryModel = await this.getCodeRepository(); + const codeRepository: CodeRepositoryModel = + await this.getCodeRepository(); if (!codeRepository.mainBranchName) { throw new BadDataException('Main Branch Name is required'); @@ -152,7 +172,7 @@ export default class CodeRepositoryUtil { await CodeRepositoryServerUtil.addFilesToGit({ repoPath: GetLocalRepositoryPath(), filePaths: data.filePaths, - }) + }); } public static async commitChanges(data: { @@ -161,45 +181,20 @@ export default class CodeRepositoryUtil { await CodeRepositoryServerUtil.commitChanges({ repoPath: GetLocalRepositoryPath(), message: data.message, - }) + }); } public static async pushChanges(data: { branchName: string; + serviceRepository: ServiceRepository; }): Promise { + const branchName: string = this.getBranchName({ + branchName: data.branchName, + serviceRepository: data.serviceRepository, + }); - 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().pushChanges({ - branchName: data.branchName, - organizationName: codeRepository.organizationName, - repoName: codeRepository.repositoryName, - }); - } - } - - - public static async createPullRequest(data: { - branchName: string; - title: string; - body: string; - }): Promise { - - const codeRepository: CodeRepositoryModel = await this.getCodeRepository(); + const codeRepository: CodeRepositoryModel = + await this.getCodeRepository(); if (!codeRepository.mainBranchName) { throw new BadDataException('Main Branch Name is required'); @@ -213,25 +208,54 @@ export default class CodeRepositoryUtil { throw new BadDataException('Repository Name is required'); } + if (codeRepository.repositoryHostedAt === CodeRepositoryType.GitHub) { + return await this.getGitHubUtil().pushChanges({ + branchName: branchName, + organizationName: codeRepository.organizationName, + repositoryName: codeRepository.repositoryName, + }); + } + } + + public static async createPullRequest(data: { + branchName: string; + title: string; + body: string; + serviceRepository: ServiceRepository; + }): Promise { + const branchName: string = this.getBranchName({ + branchName: data.branchName, + serviceRepository: data.serviceRepository, + }); + + 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, + headBranchName: 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'); } - + throw new BadDataException('Code Repository type not supported'); } - - public static async getServicesToImproveCode(data: { codeRepository: CodeRepositoryModel; services: Array; @@ -268,13 +292,16 @@ export default class CodeRepositoryUtil { } const numberOfPullRequestForThisService: number = - await this.getGitHubUtil().getNumberOfPullRequestsExistForService({ - serviceRepository: service, - pullRequestState: PullRequestState.Open, - baseBranchName: data.codeRepository.mainBranchName, - organizationName: data.codeRepository.organizationName, - repositoryName: data.codeRepository.repositoryName, - }); + await this.getGitHubUtil().getNumberOfPullRequestsExistForService( + { + serviceRepository: service, + pullRequestState: PullRequestState.Open, + baseBranchName: data.codeRepository.mainBranchName, + organizationName: + data.codeRepository.organizationName, + repositoryName: data.codeRepository.repositoryName, + } + ); if ( numberOfPullRequestForThisService < diff --git a/Copilot/Utils/ServiceFileTypes.ts b/Copilot/Utils/ServiceFileTypes.ts index e687d07ab4..244c127416 100644 --- a/Copilot/Utils/ServiceFileTypes.ts +++ b/Copilot/Utils/ServiceFileTypes.ts @@ -1,9 +1,20 @@ import ServiceLanguage from 'Common/Types/ServiceCatalog/ServiceLanguage'; export default class ServiceFileTypesUtil { - private static getCommonDirectoriesToIgnore(): string[] { - return ['node_modules', '.git', 'build', 'dist', 'coverage', 'logs', 'tmp', 'temp', 'temporal', 'tempfiles', 'tempfiles']; + return [ + 'node_modules', + '.git', + 'build', + 'dist', + 'coverage', + 'logs', + 'tmp', + 'temp', + 'temporal', + 'tempfiles', + 'tempfiles', + ]; } private static getCommonFilesToIgnore(): string[] { @@ -38,7 +49,12 @@ export default class ServiceFileTypesUtil { filesToIgnore = ['packages', 'bin', 'obj']; break; case ServiceLanguage.CPlusPlus: - filesToIgnore = ['build', 'CMakeFiles', 'CMakeCache.txt', 'Makefile']; + filesToIgnore = [ + 'build', + 'CMakeFiles', + 'CMakeCache.txt', + 'Makefile', + ]; break; case ServiceLanguage.Rust: filesToIgnore = ['Cargo.lock']; @@ -47,7 +63,13 @@ export default class ServiceFileTypesUtil { filesToIgnore = ['Podfile.lock']; break; case ServiceLanguage.Kotlin: - filesToIgnore = ['gradle', 'build', 'gradlew', 'gradlew.bat', 'gradle.properties']; + filesToIgnore = [ + 'gradle', + 'build', + 'gradlew', + 'gradlew.bat', + 'gradle.properties', + ]; break; case ServiceLanguage.TypeScript: filesToIgnore = ['node_modules', 'package-lock.json']; @@ -68,7 +90,9 @@ export default class ServiceFileTypesUtil { filesToIgnore = []; } - return filesToIgnore.concat(this.getCommonFilesToIgnore()).concat(this.getCommonDirectoriesToIgnore()); + return filesToIgnore + .concat(this.getCommonFilesToIgnore()) + .concat(this.getCommonDirectoriesToIgnore()); } private static getCommonFilesExtentions(): string[] { diff --git a/Copilot/Utils/ServiceRepository.ts b/Copilot/Utils/ServiceRepository.ts index 562b0a5f02..b8a97a4e52 100644 --- a/Copilot/Utils/ServiceRepository.ts +++ b/Copilot/Utils/ServiceRepository.ts @@ -26,9 +26,10 @@ export default class ServiceRepositoryUtil { ServiceFileTypesUtil.getFileExtentionsByServiceLanguage( serviceRepository.serviceCatalog!.serviceLanguage! ), - ignoreFilesOrDirectories: ServiceFileTypesUtil.getCommonFilesToIgnoreByServiceLanguage( + ignoreFilesOrDirectories: + ServiceFileTypesUtil.getCommonFilesToIgnoreByServiceLanguage( serviceRepository.serviceCatalog!.serviceLanguage! - ) + ), }); return allFiles; From c15b8bb951798b2f03e318f9cd7083404741684e Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Thu, 13 Jun 2024 14:11:09 +0100 Subject: [PATCH 09/11] refactor: Update CodeRepositoryUtil to include serviceRepository parameter in createBranch and createOrCheckoutBranch --- CommonServer/Docs/CodeRepository.md | 43 +++++++++++++++++++ .../Utils/CodeRepository/CodeRepository.ts | 21 +++++++++ .../Utils/CodeRepository/GitHub/GitHub.ts | 9 ++-- Copilot/Index.ts | 40 ----------------- Copilot/Utils/CodeRepository.ts | 15 +++++++ 5 files changed, 84 insertions(+), 44 deletions(-) create mode 100644 CommonServer/Docs/CodeRepository.md diff --git a/CommonServer/Docs/CodeRepository.md b/CommonServer/Docs/CodeRepository.md new file mode 100644 index 0000000000..8297a33832 --- /dev/null +++ b/CommonServer/Docs/CodeRepository.md @@ -0,0 +1,43 @@ +# Code Repository + +```javascript +const branchName: string = 'test-branch-11'; + +await CodeRepositoryUtil.createOrCheckoutBranch({ + serviceRepository: serviceRepository, + branchName: branchName, +}); + +// test code from here. +const file: CodeRepositoryFile | undefined = + 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: branchName, + serviceRepository: serviceRepository, +}); + +// create a pull request + +await CodeRepositoryUtil.createPullRequest({ + title: 'Test PR', + body: 'Test PR body', + branchName: branchName, + serviceRepository: serviceRepository, +}); +``` \ No newline at end of file diff --git a/CommonServer/Utils/CodeRepository/CodeRepository.ts b/CommonServer/Utils/CodeRepository/CodeRepository.ts index a53cbdce64..fb9893dd60 100644 --- a/CommonServer/Utils/CodeRepository/CodeRepository.ts +++ b/CommonServer/Utils/CodeRepository/CodeRepository.ts @@ -152,10 +152,31 @@ export default class CodeRepositoryUtil { logger.debug(stdout); } + public static async setUsername(data: { + repoPath: string; + username: string; + }): Promise { + const command: string = `cd ${data.repoPath} && git config user.name "${data.username}"`; + + logger.debug('Executing command: ' + command); + + const stdout: string = await Execute.executeCommand(command); + + logger.debug(stdout); + } + public static async commitChanges(data: { repoPath: string; message: string; + username: string; }): Promise { + // set the username and email + + await this.setUsername({ + repoPath: data.repoPath, + username: data.username, + }); + const command: string = `cd ${data.repoPath} && git commit -m "${data.message}"`; logger.debug('Executing command: ' + command); diff --git a/CommonServer/Utils/CodeRepository/GitHub/GitHub.ts b/CommonServer/Utils/CodeRepository/GitHub/GitHub.ts index 2afa4225a6..b5aa9900e9 100644 --- a/CommonServer/Utils/CodeRepository/GitHub/GitHub.ts +++ b/CommonServer/Utils/CodeRepository/GitHub/GitHub.ts @@ -137,9 +137,7 @@ export default class GitHubUtil extends HostedCodeRepository { // Fetch all pull requests by paginating through the results // 100 pull requests per page is the limit of the GitHub API - while (pullRequests.length === page * 100) { - allPullRequests.push(...pullRequests); - page++; + while (pullRequests.length === page * 100 || page === 1) { pullRequests = await this.getPullRequestsByPage({ organizationName: data.organizationName, repositoryName: data.repositoryName, @@ -147,6 +145,8 @@ export default class GitHubUtil extends HostedCodeRepository { baseBranchName: data.baseBranchName, page: page, }); + page++; + allPullRequests.push(...pullRequests); } return allPullRequests; @@ -176,6 +176,7 @@ export default class GitHubUtil extends HostedCodeRepository { branchName: string; organizationName: string; repositoryName: string; + repoPath: string; }): Promise { const branchName: string = data.branchName; @@ -186,7 +187,7 @@ export default class GitHubUtil extends HostedCodeRepository { 'Pushing changes to remote repository with username: ' + username ); - const command: string = `git push -u https://${username}:${password}@github.com/${data.organizationName}/${data.repositoryName}.git ${branchName}`; + const command: string = `cd ${data.repoPath} && git push -u https://${username}:${password}@github.com/${data.organizationName}/${data.repositoryName}.git ${branchName}`; logger.debug('Executing command: ' + command); const result: string = await Execute.executeCommand(command); diff --git a/Copilot/Index.ts b/Copilot/Index.ts index b92c16c952..2b3e439518 100644 --- a/Copilot/Index.ts +++ b/Copilot/Index.ts @@ -28,46 +28,6 @@ const init: PromiseVoidFunction = async (): Promise => { Object.keys(filesInService).length }` ); - - const branchName: string = 'test-branch-5'; - - await CodeRepositoryUtil.createOrCheckoutBranch({ - serviceRepository: serviceRepository, - branchName: branchName, - }); - - // test code from here. - const file: CodeRepositoryFile | undefined = - 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: branchName, - serviceRepository: serviceRepository, - }); - - // create a pull request - - await CodeRepositoryUtil.createPullRequest({ - title: 'Test PR', - body: 'Test PR body', - branchName: branchName, - serviceRepository: serviceRepository, - }); } }; diff --git a/Copilot/Utils/CodeRepository.ts b/Copilot/Utils/CodeRepository.ts index 82c9127933..3c702238ce 100644 --- a/Copilot/Utils/CodeRepository.ts +++ b/Copilot/Utils/CodeRepository.ts @@ -178,9 +178,23 @@ export default class CodeRepositoryUtil { public static async commitChanges(data: { message: string; }): Promise { + let username: string | null = null; + + if ( + this.codeRepositoryResult?.codeRepository.repositoryHostedAt === + CodeRepositoryType.GitHub + ) { + username = GetGitHubUsername(); + } + + if (!username) { + throw new BadDataException('Username is required'); + } + await CodeRepositoryServerUtil.commitChanges({ repoPath: GetLocalRepositoryPath(), message: data.message, + username: username, }); } @@ -210,6 +224,7 @@ export default class CodeRepositoryUtil { if (codeRepository.repositoryHostedAt === CodeRepositoryType.GitHub) { return await this.getGitHubUtil().pushChanges({ + repoPath: GetLocalRepositoryPath(), branchName: branchName, organizationName: codeRepository.organizationName, repositoryName: codeRepository.repositoryName, From 170b79e4cf5cf01d7fac1ca5a84ad46abd9919cd Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Thu, 13 Jun 2024 14:39:26 +0100 Subject: [PATCH 10/11] refactor: Add CopilotEventStatus column to CopilotEvent model This commit adds a new column, CopilotEventStatus, to the CopilotEvent model in the CopilotEvent.ts file. The CopilotEventStatus column represents the status of a Copilot event that was triggered for a file in the code repository. This enhancement improves the functionality and flexibility of the CopilotEvent model. --- Common/Types/Copilot/CopilotEventStatus.ts | 6 +++ .../1718285877004-MigrationName.ts | 23 +++++++++ .../Postgres/SchemaMigrations/Index.ts | 2 + Model/Models/CopilotEvent.ts | 48 +++++++++++++++++++ 4 files changed, 79 insertions(+) create mode 100644 Common/Types/Copilot/CopilotEventStatus.ts create mode 100644 CommonServer/Infrastructure/Postgres/SchemaMigrations/1718285877004-MigrationName.ts diff --git a/Common/Types/Copilot/CopilotEventStatus.ts b/Common/Types/Copilot/CopilotEventStatus.ts new file mode 100644 index 0000000000..5053491a3b --- /dev/null +++ b/Common/Types/Copilot/CopilotEventStatus.ts @@ -0,0 +1,6 @@ +enum CopilotEventStatus { + PR_CREATED = 'Pull Request Created', // PR created and waiting for review + NO_ACTION_REQUIRED = 'No Action Required', // No PR needed. All is good. +} + +export default CopilotEventStatus; diff --git a/CommonServer/Infrastructure/Postgres/SchemaMigrations/1718285877004-MigrationName.ts b/CommonServer/Infrastructure/Postgres/SchemaMigrations/1718285877004-MigrationName.ts new file mode 100644 index 0000000000..a61903838a --- /dev/null +++ b/CommonServer/Infrastructure/Postgres/SchemaMigrations/1718285877004-MigrationName.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class MigrationName1718285877004 implements MigrationInterface { + public name = 'MigrationName1718285877004'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "CopilotEvent" ADD "pullRequestId" character varying` + ); + await queryRunner.query( + `ALTER TABLE "CopilotEvent" ADD "copilotEventStatus" character varying NOT NULL` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "CopilotEvent" DROP COLUMN "copilotEventStatus"` + ); + await queryRunner.query( + `ALTER TABLE "CopilotEvent" DROP COLUMN "pullRequestId"` + ); + } +} diff --git a/CommonServer/Infrastructure/Postgres/SchemaMigrations/Index.ts b/CommonServer/Infrastructure/Postgres/SchemaMigrations/Index.ts index 5395689c1e..c377a26e96 100644 --- a/CommonServer/Infrastructure/Postgres/SchemaMigrations/Index.ts +++ b/CommonServer/Infrastructure/Postgres/SchemaMigrations/Index.ts @@ -11,6 +11,7 @@ import { MigrationName1718124277321 } from './1718124277321-MigrationName'; import { MigrationName1718126316684 } from './1718126316684-MigrationName'; import { MigrationName1718188920011 } from './1718188920011-MigrationName'; import { MigrationName1718203144945 } from './1718203144945-MigrationName'; +import { MigrationName1718285877004 } from './1718285877004-MigrationName'; export default [ InitialMigration, @@ -26,4 +27,5 @@ export default [ MigrationName1718126316684, MigrationName1718188920011, MigrationName1718203144945, + MigrationName1718285877004, ]; diff --git a/Model/Models/CopilotEvent.ts b/Model/Models/CopilotEvent.ts index 6bf3395746..ad17806a7d 100644 --- a/Model/Models/CopilotEvent.ts +++ b/Model/Models/CopilotEvent.ts @@ -5,6 +5,7 @@ import ServiceRepository from './ServiceRepository'; import User from './User'; import BaseModel from 'Common/Models/BaseModel'; import Route from 'Common/Types/API/Route'; +import CopilotEventStatus from 'Common/Types/Copilot/CopilotEventStatus'; import CopilotEventType from 'Common/Types/Copilot/CopilotEventType'; import ColumnAccessControl from 'Common/Types/Database/AccessControl/ColumnAccessControl'; import TableAccessControl from 'Common/Types/Database/AccessControl/TableAccessControl'; @@ -293,6 +294,7 @@ export default class CopilotEvent extends BaseModel { @TableColumn({ type: TableColumnType.LongText, title: 'File Path in Code Repository', + required: true, description: 'File Path in Code Repository where this event was triggered', }) @@ -461,4 +463,50 @@ export default class CopilotEvent extends BaseModel { transformer: ObjectID.getDatabaseTransformer(), }) public serviceRepositoryId?: ObjectID = undefined; + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadCopilotEvent, + ], + update: [], + }) + @TableColumn({ + type: TableColumnType.ShortText, + required: false, + isDefaultValueColumn: false, + title: 'Pull Request ID', + description: + 'ID of Pull Request in the repository where this event was executed and then PR was created.', + }) + @Column({ + type: ColumnType.ShortText, + nullable: true, + }) + public pullRequestId?: string = undefined; + + @ColumnAccessControl({ + create: [], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadCopilotEvent, + ], + update: [], + }) + @TableColumn({ + type: TableColumnType.ShortText, + title: 'Copilot Event Status', + description: + 'Status of Copilot Event that was triggered for this file in Code Repository', + }) + @Column({ + type: ColumnType.ShortText, + nullable: false, + }) + public copilotEventStatus?: CopilotEventStatus = undefined; } From 2a3c34af95e4bc97079b56c7157be1194a36b3d7 Mon Sep 17 00:00:00 2001 From: Simon Larsen Date: Thu, 13 Jun 2024 14:55:31 +0100 Subject: [PATCH 11/11] refactor: Add endpoint to retrieve Copilot events by file This commit adds a new endpoint to the CodeRepositoryAPI class in CodeRepositoryAPI.ts. The endpoint allows users to retrieve Copilot events for a specific file in the code repository. It takes in a secret key, file path, and service catalog ID as parameters and returns a list of Copilot events. This enhancement improves the functionality and usability of the API. --- CommonServer/API/CodeRepositoryAPI.ts | 90 +++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/CommonServer/API/CodeRepositoryAPI.ts b/CommonServer/API/CodeRepositoryAPI.ts index 538d7b744f..6102ae4b9a 100644 --- a/CommonServer/API/CodeRepositoryAPI.ts +++ b/CommonServer/API/CodeRepositoryAPI.ts @@ -2,6 +2,7 @@ import UserMiddleware from '../Middleware/UserAuthorization'; import CodeRepositoryService, { Service as CodeRepositoryServiceType, } from '../Services/CodeRepositoryService'; +import CopilotEventService from '../Services/CopilotEventService'; import ServiceRepositoryService from '../Services/ServiceRepositoryService'; import { ExpressRequest, @@ -14,6 +15,7 @@ import { LIMIT_PER_PROJECT } from 'Common/Types/Database/LimitMax'; import BadDataException from 'Common/Types/Exception/BadDataException'; import ObjectID from 'Common/Types/ObjectID'; import CodeRepository from 'Model/Models/CodeRepository'; +import CopilotEvent from 'Model/Models/CopilotEvent'; import ServiceRepository from 'Model/Models/ServiceRepository'; export default class CodeRepositoryAPI extends BaseAPI< @@ -100,5 +102,93 @@ export default class CodeRepositoryAPI extends BaseAPI< } } ); + + this.router.get( + `${new this.entityType() + .getCrudApiPath() + ?.toString()}/get-copilot-events-by-file/:secretkey`, + UserMiddleware.getUserMiddleware, + async ( + req: ExpressRequest, + res: ExpressResponse, + next: NextFunction + ) => { + try { + const secretkey: string = req.params['secretkey']!; + + if (!secretkey) { + throw new BadDataException('Secret key is required'); + } + + const filePath: string = req.body['filePath']!; + + if (!filePath) { + throw new BadDataException('File path is required'); + } + + const serviceCatalogId: string = + req.body['serviceCatalogId']!; + + if (!serviceCatalogId) { + throw new BadDataException( + 'Service catalog id is required' + ); + } + + const codeRepository: CodeRepository | null = + await CodeRepositoryService.findOneBy({ + query: { + secretToken: new ObjectID(secretkey), + }, + select: { + _id: true, + }, + props: { + isRoot: true, + }, + }); + + if (!codeRepository) { + throw new BadDataException( + 'Code repository not found. Secret key is invalid.' + ); + } + + const copilotEvents: Array = + await CopilotEventService.findBy({ + query: { + codeRepositoryId: codeRepository.id!, + filePath: filePath, + serviceCatalogId: new ObjectID( + serviceCatalogId + ), + }, + select: { + _id: true, + codeRepositoryId: true, + serviceCatalogId: true, + filePath: true, + copilotEventStatus: true, + copilotEventType: true, + createdAt: true, + }, + skip: 0, + limit: LIMIT_PER_PROJECT, + props: { + isRoot: true, + }, + }); + + return Response.sendJsonObjectResponse(req, res, { + copilotEvents: CopilotEvent.toJSONArray( + copilotEvents, + CopilotEvent + ), + }); + } catch (err) { + next(err); + } + } + ); } }