UNPKG

code-suggester

Version:
226 lines 8.97 kB
"use strict"; // Copyright 2020 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. Object.defineProperty(exports, "__esModule", { value: true }); exports.getDiffString = exports.getChanges = exports.parseChanges = exports.getAllDiffs = exports.getGitFileData = exports.findRepoRoot = exports.resolvePath = void 0; const child_process_1 = require("child_process"); const types_1 = require("../types"); const logger_1 = require("../logger"); const fs_1 = require("fs"); const path = require("path"); class InstallationError extends Error { constructor(message) { super(message); this.name = 'InstallationError'; } } /** * Get the absolute path of a relative path * @param {string} dir the wildcard directory containing git change, not necessarily the root git directory * @returns {string} the absolute path relative to the path that the user executed the bash command in */ function resolvePath(dir) { const absoluteDir = path.resolve(process.cwd(), dir); return absoluteDir; } exports.resolvePath = resolvePath; /** * Get the git root directory. * Errors if the directory provided is not a git directory. * @param {string} dir an absolute directory * @returns {string} the absolute path of the git directory root */ function findRepoRoot(dir) { try { return (0, child_process_1.execSync)('git rev-parse --show-toplevel', { cwd: dir }) .toString() .trimRight(); // remove the trailing \n } catch (err) { logger_1.logger.error(`The directory provided is not a git directory: ${dir}`); throw err; } } exports.findRepoRoot = findRepoRoot; /** * Returns the git diff old/new mode, status, and path. Given a git diff. * Errors if there is a parsing error * @param {string} gitDiffPattern A single file diff. Renames and copies are broken up into separate diffs. See https://git-scm.com/docs/git-diff#Documentation/git-diff.txt-git-diff-filesltpatterngt82308203 for more details * @returns indexable git diff fields: old/new mode, status, and path */ function parseGitDiff(gitDiffPattern) { try { const fields = gitDiffPattern.split(' '); const newMode = fields[1]; const oldMode = fields[0].substring(1); const statusAndPath = fields[4].split('\t'); const status = statusAndPath[0]; const relativePath = statusAndPath[1]; return { oldMode, newMode, status, relativePath }; } catch (err) { logger_1.logger.warn(`\`git diff --raw\` may have changed formats: \n ${gitDiffPattern}`); throw err; } } /** * Get the GitHub mode, file content, and relative path asynchronously * Rejects if there is a git diff error, or if the file contents could not be loaded. * @param {string} gitRootDir the root of the local GitHub repository * @param {string} gitDiffPattern A single file diff. Renames and copies are broken up into separate diffs. See https://git-scm.com/docs/git-diff#Documentation/git-diff.txt-git-diff-filesltpatterngt82308203 for more details * @returns {Promise<GitFileData>} the current mode, the relative path of the file in the Git Repository, and the file status. */ function getGitFileData(gitRootDir, gitDiffPattern) { return new Promise((resolve, reject) => { try { const { oldMode, newMode, status, relativePath } = parseGitDiff(gitDiffPattern); // if file is deleted, do not attempt to read it if (status === 'D') { resolve({ path: relativePath, fileData: new types_1.FileData(null, oldMode) }); } else { // else read the file (0, fs_1.readFile)(gitRootDir + '/' + relativePath, { encoding: 'utf-8', }, (err, content) => { if (err) { logger_1.logger.error(`Error loading file ${relativePath} in git directory ${gitRootDir}`); reject(err); } resolve({ path: relativePath, fileData: new types_1.FileData(content, newMode), }); }); } } catch (err) { reject(err); } }); } exports.getGitFileData = getGitFileData; /** * Get all the diffs using `git diff` of a git directory. * Errors if the git directory provided is not a git directory. * @param {string} gitRootDir a git directory * @returns {string[]} a list of git diffs */ function getAllDiffs(gitRootDir) { (0, child_process_1.execSync)('git add -A', { cwd: gitRootDir }); const diffs = (0, child_process_1.execSync)('git diff --raw --staged --no-renames', { cwd: gitRootDir, }) .toString() // strictly return buffer for mocking purposes. sinon ts doesn't infer {encoding: 'utf-8'} .trimRight() // remove the trailing new line .split('\n') .filter(line => !!line.trim()); (0, child_process_1.execSync)('git reset .', { cwd: gitRootDir }); return diffs; } exports.getAllDiffs = getAllDiffs; /** * Get the git changes of the current project asynchronously. * Rejects if any of the files fails to load (if not deleted), * or if there is a git diff parse error * @param {string[]} diffs the git diff raw output (which only shows relative paths) * @param {string} gitDir the root of the local GitHub repository * @returns {Promise<Changes>} the changeset */ async function parseChanges(diffs, gitDir) { try { // get updated file contents const changes = new Map(); const changePromises = []; for (let i = 0; i < diffs.length; i++) { // TODO - handle memory constraint changePromises.push(getGitFileData(gitDir, diffs[i])); } const gitFileDatas = await Promise.all(changePromises); for (let i = 0; i < gitFileDatas.length; i++) { changes.set(gitFileDatas[i].path, gitFileDatas[i].fileData); } return changes; } catch (err) { logger_1.logger.error('Error parsing git changes'); throw err; } } exports.parseChanges = parseChanges; /** * Throws an error if git is not installed * @returns {void} void if git is installed */ function validateGitInstalled() { try { (0, child_process_1.execSync)('git --version'); } catch (err) { logger_1.logger.error('git not installed'); throw new InstallationError('git command is not recognized. Make sure git is installed.'); } } /** * Load the change set asynchronously. * @param dir the directory containing git changes * @returns {Promise<Changes>} the change set */ function getChanges(dir) { try { validateGitInstalled(); const absoluteDir = resolvePath(dir); const gitRootDir = findRepoRoot(absoluteDir); const diffs = getAllDiffs(gitRootDir); return parseChanges(diffs, gitRootDir); } catch (err) { if (!(err instanceof InstallationError)) { logger_1.logger.error('Error loadng git changes.'); } throw err; } } exports.getChanges = getChanges; /** * Get the git changes of the current project asynchronously. * Rejects if any of the files fails to load (if not deleted), * or if there is a git diff parse error * @param {string[]} diffs the git diff raw output (which only shows relative paths) * @param {string} gitDir the root of the local GitHub repository * @returns {string} the diff */ function getDiffString(dir) { try { validateGitInstalled(); const absoluteDir = resolvePath(dir); const gitRootDir = findRepoRoot(absoluteDir); (0, child_process_1.execSync)('git add -A', { cwd: gitRootDir }); const diff = (0, child_process_1.execSync)('git diff --staged --no-renames', { cwd: gitRootDir, }) .toString() // strictly return buffer for mocking purposes. sinon ts doesn't infer {encoding: 'utf-8'} .trimRight(); // remove the trailing new line (0, child_process_1.execSync)('git reset .', { cwd: gitRootDir }); return diff; } catch (err) { if (!(err instanceof InstallationError)) { logger_1.logger.error('Error loadng git changes.'); } throw err; } } exports.getDiffString = getDiffString; //# sourceMappingURL=handle-git-dir-change.js.map