code-suggester
Version:
Library to propose code changes
226 lines • 8.97 kB
JavaScript
;
// 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