Merge branch 'ai-copilot-main'

This commit is contained in:
Simon Larsen
2024-06-13 21:12:48 +01:00
20 changed files with 1098 additions and 48 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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<
@@ -48,6 +50,9 @@ export default class CodeRepositoryAPI extends BaseAPI<
select: {
name: true,
mainBranchName: true,
organizationName: true,
repositoryHostedAt: true,
repositoryName: true,
},
props: {
isRoot: true,
@@ -55,7 +60,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<ServiceRepository> =
@@ -68,6 +75,7 @@ export default class CodeRepositoryAPI extends BaseAPI<
serviceCatalog: {
name: true,
_id: true,
serviceLanguage: true,
},
servicePathInRepository: true,
limitNumberOfOpenPullRequestsCount: true,
@@ -94,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<CopilotEvent> =
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);
}
}
);
}
}

View File

@@ -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,
});
```

View File

@@ -0,0 +1,23 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class MigrationName1718285877004 implements MigrationInterface {
public name = 'MigrationName1718285877004';
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
await queryRunner.query(
`ALTER TABLE "CopilotEvent" DROP COLUMN "copilotEventStatus"`
);
await queryRunner.query(
`ALTER TABLE "CopilotEvent" DROP COLUMN "pullRequestId"`
);
}
}

View File

@@ -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,
];

View File

@@ -1,29 +1,238 @@
import Execute from '../Execute';
import LocalFile from '../LocalFile';
import logger from '../Logger';
import CodeRepositoryFile from './CodeRepositoryFile';
import Dictionary from 'Common/Types/Dictionary';
export default class CodeRepositoryUtil {
public static async createOrCheckoutBranch(data: {
repoPath: string;
branchName: string;
}): Promise<void> {
const command: string = `cd ${data.repoPath} && git checkout ${data.branchName} || git checkout -b ${data.branchName}`;
logger.debug('Executing command: ' + 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<void> {
const command: string = `cd ${data.repoPath} && git checkout .`;
logger.debug('Executing command: ' + command);
const stdout: string = await Execute.executeCommand(command);
logger.debug(stdout);
}
public static async writeToFile(data: {
filePath: string;
repoPath: string;
content: string;
}): Promise<void> {
const totalPath: string = LocalFile.sanitizeFilePath(
`${data.repoPath}/${data.filePath}`
);
const command: string = `echo "${data.content}" > ${totalPath}`;
logger.debug('Executing command: ' + command);
const stdout: string = await Execute.executeCommand(command);
logger.debug(stdout);
}
public static async createDirectory(data: {
repoPath: string;
directoryPath: string;
}): Promise<void> {
const totalPath: string = LocalFile.sanitizeFilePath(
`${data.repoPath}/${data.directoryPath}`
);
const command: string = `mkdir ${totalPath}`;
logger.debug('Executing command: ' + command);
const stdout: string = await Execute.executeCommand(command);
logger.debug(stdout);
}
public static async deleteFile(data: {
repoPath: string;
filePath: string;
}): Promise<void> {
const totalPath: string = LocalFile.sanitizeFilePath(
`${data.repoPath}/${data.filePath}`
);
const command: string = `rm ${totalPath}`;
logger.debug('Executing command: ' + command);
const stdout: string = await Execute.executeCommand(command);
logger.debug(stdout);
}
public static async deleteDirectory(data: {
repoPath: string;
directoryPath: string;
}): Promise<void> {
const totalPath: string = LocalFile.sanitizeFilePath(
`${data.repoPath}/${data.directoryPath}`
);
const command: string = `rm -rf ${totalPath}`;
logger.debug('Executing command: ' + command);
const stdout: string = await Execute.executeCommand(command);
logger.debug(stdout);
}
public static async createBranch(data: {
repoPath: string;
branchName: string;
}): Promise<void> {
const command: string = `cd ${data.repoPath} && git checkout -b ${data.branchName}`;
logger.debug('Executing command: ' + command);
const stdout: string = await Execute.executeCommand(command);
logger.debug(stdout);
}
public static async checkoutBranch(data: {
repoPath: string;
branchName: string;
}): Promise<void> {
const command: string = `cd ${data.repoPath} && git checkout ${data.branchName}`;
logger.debug('Executing command: ' + command);
const stdout: string = await Execute.executeCommand(command);
logger.debug(stdout);
}
public static async addFilesToGit(data: {
repoPath: string;
filePaths: Array<string>;
}): Promise<void> {
const filePaths: Array<string> = data.filePaths.map(
(filePath: string) => {
if (filePath.startsWith('/')) {
// remove the leading slash and return
return filePath.substring(1);
}
return filePath;
}
);
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);
}
public static async setUsername(data: {
repoPath: string;
username: string;
}): Promise<void> {
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<void> {
// 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);
const stdout: string = await Execute.executeCommand(command);
logger.debug(stdout);
}
public static async getGitCommitHashForFile(data: {
repoPath: string;
filePath: string;
}): Promise<string> {
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}"`
);
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: {
directoryPath: string;
repoPath: string;
acceptedFileExtensions?: Array<string>;
ignoreFilesOrDirectories: Array<string>;
}): Promise<{
files: Dictionary<CodeRepositoryFile>;
subDirectories: Array<string>;
}> {
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<CodeRepositoryFile> = {};
const output: string = await Execute.executeCommand(`ls ${totalPath}`);
@@ -37,23 +246,55 @@ export default class CodeRepositoryUtil {
continue;
}
const isDirectory: boolean = (
await Execute.executeCommand(`file "${totalPath}/${fileName}"`)
).includes('directory');
const filePath: string = LocalFile.sanitizeFilePath(
`${directoryPath}/${fileName}`
);
if (isDirectory) {
subDirectories.push(`${totalPath}/${fileName}`);
if (data.ignoreFilesOrDirectories.includes(fileName)) {
continue;
}
const filePath: string = `${totalPath}/${fileName}`;
const isDirectory: boolean = (
await Execute.executeCommand(
`file "${LocalFile.sanitizeFilePath(
`${totalPath}/${fileName}`
)}"`
)
).includes('directory');
if (isDirectory) {
subDirectories.push(
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({
filePath,
repoPath,
});
const fileExtension: string = fileName.split('.').pop() || '';
files[filePath] = {
filePath,
filePath: LocalFile.sanitizeFilePath(
`${directoryPath}/${fileName}`
),
gitCommitHash,
fileExtension,
fileName,
@@ -69,6 +310,8 @@ export default class CodeRepositoryUtil {
public static async getFilesInDirectoryRecursive(data: {
repoPath: string;
directoryPath: string;
acceptedFileExtensions: Array<string>;
ignoreFilesOrDirectories: Array<string>;
}): Promise<Dictionary<CodeRepositoryFile>> {
let files: Dictionary<CodeRepositoryFile> = {};
@@ -76,6 +319,8 @@ export default class CodeRepositoryUtil {
await this.getFilesInDirectory({
directoryPath: data.directoryPath,
repoPath: data.repoPath,
acceptedFileExtensions: data.acceptedFileExtensions,
ignoreFilesOrDirectories: data.ignoreFilesOrDirectories,
});
files = {
@@ -89,6 +334,8 @@ export default class CodeRepositoryUtil {
...(await this.getFilesInDirectoryRecursive({
repoPath: data.repoPath,
directoryPath: subDirectory,
acceptedFileExtensions: data.acceptedFileExtensions,
ignoreFilesOrDirectories: data.ignoreFilesOrDirectories,
})),
};
}

View File

@@ -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';
@@ -135,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,
@@ -145,8 +145,94 @@ export default class GitHubUtil extends HostedCodeRepository {
baseBranchName: data.baseBranchName,
page: page,
});
page++;
allPullRequests.push(...pullRequests);
}
return allPullRequests;
}
public override async addRemote(data: {
remoteName: string;
organizationName: string;
repositoryName: string;
}): Promise<void> {
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;
repositoryName: string;
repoPath: string;
}): Promise<void> {
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 = `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);
logger.debug(result);
}
public override async createPullRequest(data: {
baseBranchName: string;
headBranchName: string;
organizationName: string;
repositoryName: string;
title: string;
body: string;
}): Promise<PullRequest> {
const gitHubToken: string = this.authToken;
const url: URL = URL.fromString(
`https://api.github.com/repos/${data.organizationName}/${data.repositoryName}/pulls`
);
const result: HTTPErrorResponse | HTTPResponse<JSONObject> =
await API.post(
url,
{
base: data.baseBranchName,
head: data.headBranchName,
title: data.title,
body: data.body,
},
{
Authorization: `Bearer ${gitHubToken}`,
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
}
);
if (result instanceof HTTPErrorResponse) {
throw result;
}
return this.getPullRequestFromJSONObject({
pullRequest: result.data,
organizationName: data.organizationName,
repositoryName: data.repositoryName,
});
}
}

View File

@@ -5,15 +5,22 @@ 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;
@@ -78,4 +85,31 @@ export default class HostedCodeRepository {
}): Promise<Array<PullRequest>> {
throw new NotImplementedException();
}
public async createPullRequest(_data: {
baseBranchName: string;
headBranchName: string;
organizationName: string;
repositoryName: string;
title: string;
body: string;
}): Promise<PullRequest> {
throw new NotImplementedException();
}
public async pushChanges(_data: {
branchName: string;
organizationName: string;
repositoryName: string;
}): Promise<void> {
throw new NotImplementedException();
}
public async addRemote(_data: {
remoteName: string;
organizationName: string;
repositoryName: string;
}): Promise<void> {
throw new NotImplementedException();
}
}

View File

@@ -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<void> {
return new Promise(
(resolve: VoidFunction, reject: PromiseRejectErrorFunction) => {

View File

@@ -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=

View File

@@ -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;
};

View File

@@ -1,6 +1,9 @@
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';
import Dictionary from 'Common/Types/Dictionary';
import { PromiseVoidFunction } from 'Common/Types/FunctionTypes';
import CodeRepositoryFile from 'CommonServer/Utils/CodeRepository/CodeRepositoryFile';
@@ -32,8 +35,25 @@ init()
.then(() => {
process.exit(0);
})
.catch((error: Error) => {
.catch(async (error: Error | HTTPErrorResponse) => {
try {
await CodeRepositoryUtil.discardChanges();
// change back to main branch.
await CodeRepositoryUtil.checkoutMainBranch();
} catch (e) {
// do nothing.
}
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);
});

View File

@@ -1,5 +1,7 @@
import {
GetGitHubToken,
GetGitHubUsername,
GetLocalRepositoryPath,
GetOneUptimeURL,
GetRepositorySecretKey,
} from '../Config';
@@ -7,10 +9,12 @@ 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';
@@ -23,6 +27,249 @@ 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();
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!,
});
}
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<void> {
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<void> {
const branchName: string = this.getBranchName({
branchName: data.branchName,
serviceRepository: data.serviceRepository,
});
await CodeRepositoryServerUtil.createOrCheckoutBranch({
repoPath: GetLocalRepositoryPath(),
branchName: branchName,
});
}
public static async writeToFile(data: {
filePath: string;
content: string;
}): Promise<void> {
await CodeRepositoryServerUtil.writeToFile({
repoPath: GetLocalRepositoryPath(),
filePath: data.filePath,
content: data.content,
});
}
public static async createDirectory(data: {
directoryPath: string;
}): Promise<void> {
await CodeRepositoryServerUtil.createDirectory({
repoPath: GetLocalRepositoryPath(),
directoryPath: data.directoryPath,
});
}
public static async deleteFile(data: { filePath: string }): Promise<void> {
await CodeRepositoryServerUtil.deleteFile({
repoPath: GetLocalRepositoryPath(),
filePath: data.filePath,
});
}
public static async deleteDirectory(data: {
directoryPath: string;
}): Promise<void> {
await CodeRepositoryServerUtil.deleteDirectory({
repoPath: GetLocalRepositoryPath(),
directoryPath: data.directoryPath,
});
}
public static async discardChanges(): Promise<void> {
await CodeRepositoryServerUtil.discardChanges({
repoPath: GetLocalRepositoryPath(),
});
}
public static async checkoutBranch(data: {
branchName: string;
}): Promise<void> {
await CodeRepositoryServerUtil.checkoutBranch({
repoPath: GetLocalRepositoryPath(),
branchName: data.branchName,
});
}
public static async checkoutMainBranch(): Promise<void> {
const codeRepository: CodeRepositoryModel =
await this.getCodeRepository();
if (!codeRepository.mainBranchName) {
throw new BadDataException('Main Branch Name is required');
}
await this.checkoutBranch({
branchName: codeRepository.mainBranchName!,
});
}
public static async addFilesToGit(data: {
filePaths: Array<string>;
}): Promise<void> {
await CodeRepositoryServerUtil.addFilesToGit({
repoPath: GetLocalRepositoryPath(),
filePaths: data.filePaths,
});
}
public static async commitChanges(data: {
message: string;
}): Promise<void> {
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,
});
}
public static async pushChanges(data: {
branchName: string;
serviceRepository: ServiceRepository;
}): Promise<void> {
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({
repoPath: GetLocalRepositoryPath(),
branchName: branchName,
organizationName: codeRepository.organizationName,
repositoryName: codeRepository.repositoryName,
});
}
}
public static async createPullRequest(data: {
branchName: string;
title: string;
body: string;
serviceRepository: ServiceRepository;
}): Promise<PullRequest> {
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: branchName,
baseBranchName: codeRepository.mainBranchName,
organizationName: codeRepository.organizationName,
repositoryName: codeRepository.repositoryName,
title: data.title,
body: data.body,
});
}
throw new BadDataException('Code Repository type not supported');
}
public static async getServicesToImproveCode(data: {
codeRepository: CodeRepositoryModel;
@@ -60,15 +307,16 @@ export default class CodeRepositoryUtil {
}
const numberOfPullRequestForThisService: number =
await new GitHubUtil({
authToken: gitHuhbToken,
}).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 <

View File

@@ -0,0 +1,165 @@
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',
];
}
private static getCommonFilesToIgnore(): string[] {
return ['.DS_Store', 'Thumbs.db', '.gitignore', '.gitattributes'];
}
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'];
}
public static getFileExtentionsByServiceLanguage(
serviceLanguage: ServiceLanguage
): string[] {
let fileExtentions: Array<string> = [];
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());
}
}

View File

@@ -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,24 @@ export default class ServiceRepositoryUtil {
}): Promise<Dictionary<CodeRepositoryFile>> {
const { serviceRepository } = data;
if (!serviceRepository.serviceCatalog?.serviceLanguage) {
throw new BadDataException(
'Service language is not defined in the service catalog'
);
}
const allFiles: Dictionary<CodeRepositoryFile> =
await CodeRepositoryCommonServerUtil.getFilesInDirectoryRecursive({
repoPath: GetLocalRepositoryPath(),
directoryPath: serviceRepository.servicePathInRepository || '.',
acceptedFileExtensions:
ServiceFileTypesUtil.getFileExtentionsByServiceLanguage(
serviceRepository.serviceCatalog!.serviceLanguage!
),
ignoreFilesOrDirectories:
ServiceFileTypesUtil.getCommonFilesToIgnoreByServiceLanguage(
serviceRepository.serviceCatalog!.serviceLanguage!
),
});
return allFiles;

View File

@@ -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<PageComponentProps> = (

View File

@@ -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<PageComponentProps> = (

View File

@@ -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;
}

View File

@@ -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')