import path from "path"; import Execute from "../Execute"; import LocalFile from "../LocalFile"; import logger from "../Logger"; import CaptureSpan from "../Telemetry/CaptureSpan"; import CodeRepositoryFile from "./CodeRepositoryFile"; import Dictionary from "../../../Types/Dictionary"; import BadDataException from "../../../Types/Exception/BadDataException"; export default class CodeRepositoryUtil { @CaptureSpan() public static getCurrentCommitHash(data: { repoPath: string; }): Promise { const command: string = `cd ${data.repoPath} && git rev-parse HEAD`; logger.debug("Executing command: " + command); return Execute.executeCommand(command); } @CaptureSpan() public static async addAllChangedFilesToGit(data: { repoPath: string; }): Promise { const command: string = `cd ${data.repoPath} && git add -A`; logger.debug("Executing command: " + command); const stdout: string = await Execute.executeCommand(command); logger.debug(stdout); } @CaptureSpan() public static async setAuthorIdentity(data: { repoPath: string; authorName: string; authorEmail: string; }): Promise { const command: string = `cd ${data.repoPath} && git config --global user.name "${data.authorName}" && git config --global user.email "${data.authorEmail}"`; logger.debug("Executing command: " + command); const stdout: string = await Execute.executeCommand(command); logger.debug(stdout); } @CaptureSpan() public static async discardAllChangesOnCurrentBranch(data: { repoPath: string; }): Promise { const command: string = `cd ${data.repoPath} && git checkout .`; logger.debug("Executing command: " + command); const stdout: string = await Execute.executeCommand(command); logger.debug(stdout); } // returns the folder name of the cloned repository @CaptureSpan() public static async cloneRepository(data: { repoPath: string; repoUrl: string; }): Promise { const command: string = `cd ${data.repoPath} && git clone ${data.repoUrl}`; logger.debug("Executing command: " + command); const stdout: string = await Execute.executeCommand(command); logger.debug(stdout); // get the folder name of the repository from the disk. const getFolderNameCommand: string = `cd ${data.repoPath} && ls`; const folderName: string = await Execute.executeCommand(getFolderNameCommand); return folderName.trim(); } @CaptureSpan() public static async pullChanges(data: { repoPath: string }): Promise { const command: string = `cd ${data.repoPath} && git pull`; logger.debug("Executing command: " + command); const stdout: string = await Execute.executeCommand(command); logger.debug(stdout); } @CaptureSpan() 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); const stdout: string = await Execute.executeCommand(command); logger.debug(stdout); } @CaptureSpan() public static getFileContent(data: { repoPath: string; filePath: string; }): Promise { const absolutePath: string = this.resolvePathWithinRepo( data.repoPath, data.filePath, ); return LocalFile.read(absolutePath); } // discard all changes in the working directory @CaptureSpan() public static async discardChanges(data: { repoPath: string; }): Promise { const command: string = `cd ${data.repoPath} && git checkout .`; logger.debug("Executing command: " + command); const stdout: string = await Execute.executeCommand(command); logger.debug(stdout); } @CaptureSpan() public static async writeToFile(data: { filePath: string; repoPath: string; content: string; }): Promise { const totalPath: string = LocalFile.sanitizeFilePath( `${data.repoPath}/${data.filePath}`, ); await LocalFile.write(totalPath, data.content); } @CaptureSpan() public static async createDirectory(data: { repoPath: string; directoryPath: string; }): Promise { const totalPath: string = LocalFile.sanitizeFilePath( `${data.repoPath}/${data.directoryPath}`, ); await LocalFile.makeDirectory(totalPath); } @CaptureSpan() public static async deleteFile(data: { repoPath: string; filePath: string; }): Promise { const totalPath: string = LocalFile.sanitizeFilePath( `${data.repoPath}/${data.filePath}`, ); await LocalFile.deleteFile(totalPath); } @CaptureSpan() public static async deleteDirectory(data: { repoPath: string; directoryPath: string; }): Promise { const totalPath: string = LocalFile.sanitizeFilePath( `${data.repoPath}/${data.directoryPath}`, ); logger.debug("Deleting directory: " + totalPath); await LocalFile.deleteDirectory(totalPath); } @CaptureSpan() 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); const stdout: string = await Execute.executeCommand(command); logger.debug(stdout); } @CaptureSpan() public static async checkoutBranch(data: { repoPath: string; branchName: string; }): Promise { 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); } @CaptureSpan() public static async addFilesToGit(data: { 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); } 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); } @CaptureSpan() 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); } @CaptureSpan() public static async commitChanges(data: { repoPath: string; message: string; }): Promise { logger.debug("Executing git commit"); const stdout: string = await Execute.executeCommandFile({ command: "git", args: ["commit", "-m", data.message], cwd: data.repoPath, }); logger.debug(stdout); } @CaptureSpan() public static async getGitCommitHashForFile(data: { 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; 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; } @CaptureSpan() public static async listFilesInDirectory(data: { directoryPath: string; repoPath: string; }): Promise> { if (!data.directoryPath.startsWith("/")) { data.directoryPath = "/" + data.directoryPath; } if (!data.repoPath.startsWith("/")) { data.repoPath = "/" + data.repoPath; } const { directoryPath, repoPath } = data; let totalPath: string = `${repoPath}/${directoryPath}`; totalPath = LocalFile.sanitizeFilePath(totalPath); // clean up the path const entries = await LocalFile.readDirectory(totalPath); return entries.map((entry) => { return entry.name; }); } @CaptureSpan() public static async getFilesInDirectory(data: { directoryPath: string; repoPath: string; acceptedFileExtensions?: Array; ignoreFilesOrDirectories: Array; }): Promise<{ files: Dictionary; subDirectories: Array; }> { if (!data.directoryPath.startsWith("/")) { data.directoryPath = "/" + data.directoryPath; } if (!data.repoPath.startsWith("/")) { data.repoPath = "/" + data.repoPath; } const { directoryPath, repoPath } = data; let totalPath: string = `${repoPath}/${directoryPath}`; totalPath = LocalFile.sanitizeFilePath(totalPath); // clean up the path const files: Dictionary = {}; const subDirectories: Array = []; const entries = await LocalFile.readDirectory(totalPath); for (const entry of entries) { const fileName: string = entry.name; const filePath: string = LocalFile.sanitizeFilePath( `${directoryPath}/${fileName}`, ); if (data.ignoreFilesOrDirectories.includes(fileName)) { continue; } if (entry.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; } } files[filePath] = { fileContent: await this.getFileContent({ filePath: LocalFile.sanitizeFilePath(`${directoryPath}/${fileName}`), repoPath, }), filePath: filePath, }; } return { files, subDirectories: subDirectories, }; } @CaptureSpan() public static async getFilesInDirectoryRecursive(data: { repoPath: string; directoryPath: string; acceptedFileExtensions: Array; ignoreFilesOrDirectories: Array; }): Promise> { let files: Dictionary = {}; const { files: filesInDirectory, subDirectories } = await this.getFilesInDirectory({ directoryPath: data.directoryPath, repoPath: data.repoPath, acceptedFileExtensions: data.acceptedFileExtensions, ignoreFilesOrDirectories: data.ignoreFilesOrDirectories, }); files = { ...files, ...filesInDirectory, }; for (const subDirectory of subDirectories) { files = { ...files, ...(await this.getFilesInDirectoryRecursive({ repoPath: data.repoPath, directoryPath: subDirectory, acceptedFileExtensions: data.acceptedFileExtensions, ignoreFilesOrDirectories: data.ignoreFilesOrDirectories, })), }; } return files; } private static resolvePathWithinRepo( repoPath: string, targetPath: string, ): string { const root: string = path.resolve(repoPath); const sanitizedTarget: string = LocalFile.sanitizeFilePath( targetPath, ).replace(/^\/+/, ""); const absoluteTarget: string = path.resolve(root, sanitizedTarget); if ( absoluteTarget !== root && !absoluteTarget.startsWith(root + path.sep) ) { throw new BadDataException("File path is outside the repository"); } return absoluteTarget; } }